TL; DR Questo è un modo per eseguire shellcode che non funziona più.
Che cos'è una funzione?
Lo shellcode è solo codice macchina in luoghi in cui non è normalmente trovato, come una variabile di tipo char
. In C, non c'è distinzione tra funzioni e variabili. Una funzione è solo una variabile che punta al codice eseguibile. Ciò significa che, se si crea una variabile che punta al codice eseguibile e la si chiama come se fosse una funzione, verrà eseguita. Per illustrare come è solo una variabile, vedere questo semplice programma:
#include <stdio.h>
#include <stdint.h>
void print_hello(void)
{
printf("Hello, world!\n");
}
void main(void)
{
uintptr_t new_print_hello;
printf("print_hello = %p\n", print_hello);
new_print_hello = (uintptr_t)print_hello;
(*(void(*)())new_print_hello)();
print_hello();
}
Quando compilato ed eseguito, questo programma fornisce un output come questo:
$ ./a.out
print_hello = 0x28bc4bf6da
Hello, world!
Hello, world!
Questo rende facile vedere che una funzione non è altro che un indirizzo in memoria, compatibile con il tipo uintptr_t
. Puoi vedere come una funzione può essere referenziata semplicemente come una variabile, in questo caso stampandone il valore, o copiandola su un'altra variabile di un tipo compatibile e chiamando la variabile come una funzione, anche se con un po 'di magia di cast in ordine rendere felice il compilatore C. Una volta che vedi come una funzione non è altro che una variabile che punta a una memoria eseguibile, non è un modo per vedere come può essere eseguita anche una variabile che punta a un certo bytecode che si definisce manualmente.
Come funzionano le funzioni?
Ora che sai che una funzione è solo un indirizzo in memoria, devi sapere come viene effettivamente eseguita una funzione. Una volta che si chiama una funzione, in genere con l'istruzione call
, il puntatore dell'istruzione (che punta all'istruzione in esecuzione) cambia in puntare alla prima istruzione della funzione. La posizione appena prima che la funzione venga richiamata viene salvata nello stack di call
. Una volta che la funzione è terminata, viene terminata con l'istruzione ret
, che la preleva dallo stack, salvandola sull'IP. Quindi una vista (un po 'semplificata) è che call
spinge l'IP allo stack e ret
lo restituisce.
A seconda dell'architettura e del sistema operativo in uso, gli argomenti della funzione possono essere passati nei registri o nello stack e il valore restituito può trovarsi in registri diversi o nello stack. Questa è chiamata la funzione chiamata ABI ed è specifica per ciascun tipo di sistema. Lo shellcode progettato per un tipo di sistema potrebbe non funzionare su un altro, anche se l'architettura è la stessa e il sistema operativo diverso, o viceversa.
Che cosa fa il codice shell?
Diamo un'occhiata allo smontaggio dello shellcode che hai fornito:
0000000000201010 <shellcode>:
201010: bb 00 00 00 00 mov ebx,0x0
201015: b8 01 00 00 00 mov eax,0x1
20101a: cd 80 int 0x80
Questo fa tre cose. Innanzitutto, imposta ebx
su 0. In secondo luogo, imposta il registro eax
su 1. Infine, attiva l'interrupt 0x80 che, nei sistemi a 32 bit, è l'interrupt syscall. Nel SysV che chiama ABI, il numero di syscall è posto in eax
e fino a 6 argomenti sono passati in ebx
, ecx
, edx
, esi
, edi
e ebp
. In questo caso, viene impostato solo ebx
, il che significa che syscall accetta solo un argomento. Una volta chiamato l'interrupt 0x80, il kernel prende il sopravvento e guarda questi valori, eseguendo la chiamata di sistema corretta. I numeri di chiamata di sistema sono definiti in /usr/include/asm/unistd_32.h
. Guardando a ciò, vediamo che syscall 1 è exit()
. Da ciò, possiamo vedere le tre cose che questo shellcode fa:
- Imposta il primo argomento di syscall su 0 (che significa successo dell'uscita).
- Imposta il numero di syscall su 1, che è la chiamata di uscita.
- Invoca syscall, causando l'uscita del programma con lo stato 0.
Quando guardi l'immagine grande, vediamo che lo shellcode è essenzialmente equivalente a exit(0)
. Non ha bisogno di ret
perché non ritorna mai e invece fa terminare il programma. Se si desidera che la funzione venga restituita, sarà necessario aggiungere ret
alla fine. Se non lo fai, almeno utilizza ret
, quindi il programma si blocca a meno che non termini prima che raggiunga la fine della funzione, come nel tuo esempio con exit()
syscall.
Cosa c'è che non va con il tuo shellcode?
Il metodo per chiamare lo shellcode che stai mostrando non funziona più . Un tempo, ma ora Linux non consente l'esecuzione di dati arbitrari, il che richiede un lancio arcano. Questa vecchia tecnica è spiegata bene nel famoso articolo Smashing The Stack For Fun And Profit :
Lets try to modify our first example so that it overwrites the return
address, and demonstrate how we can make it execute arbitrary code. Just
before buffer1[] on the stack is SFP, and before it, the return address.
That is 4 bytes pass the end of buffer1[]. But remember that buffer1[] is
really 2 word so its 8 bytes long. So the return address is 12 bytes from
the start of buffer1[]. We'll modify the return value in such a way that the
assignment statement 'x = 1;' after the function call will be jumped. To do
so we add 8 bytes to the return address. Our code is now:
example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
------------------------------------------------------------------------------
What we have done is add 12 to buffer1[]'s address. This new address is
where the return address is stored. We want to skip pass the assignment to
the printf call. How did we know to add 8 to the return address? We used a
test value first (for example 1), compiled the program, and then started gdb
La versione corretta del tuo shellcode per i sistemi più recenti potrebbe essere:
const char shellcode[] = “\xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80”;
int main(){
int (*ret)() = (int(*)())shellcode;
ret();
}