Rilevati frammenti di pila

Sto eseguendo il mio file a.out. Dopo l’esecuzione, il programma viene eseguito per un certo tempo quindi esce con il messaggio:

**** stack smashing detected ***: ./a.out terminated* *======= Backtrace: =========* */lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)Aborted* 

Quali potrebbero essere le possibili ragioni per questo e come posso correggerlo?

Lo Stack Smashing è in realtà causato da un meccanismo di protezione utilizzato da gcc per rilevare errori di overflow del buffer. Ad esempio nel seguente snippet:

 #include  void func() { char array[10]; gets(array); } int main(int argc, char **argv) { func(); } 

Il compilatore (in questo caso gcc) aggiunge variabili di protezione (chiamate canarini) che hanno valori noti. Una stringa di input di dimensioni superiori a 10 provoca il danneggiamento di questa variabile, con conseguente SIGABRT per terminare il programma.

Per avere un’idea, puoi provare a disabilitare questa protezione di gcc usando l’opzione -fno-stack-protector durante la compilazione. In tal caso, si verificherà un errore diverso, molto probabilmente un errore di segmentazione mentre si sta tentando di accedere a una posizione di memoria illegale. Si noti che -fstack-protector dovrebbe sempre essere triggersto per i build di rilascio in quanto è una funzionalità di sicurezza.

È ansible ottenere alcune informazioni sul punto di overflow eseguendo il programma con un debugger. Valgrind non funziona bene con gli errori relativi allo stack, ma come un debugger, può aiutarti a localizzare il punto e il motivo dell’incidente.

Si prega di guardare la seguente situazione:

 [email protected]:$ cat test_overflow.c #include  #include  int check_password(char *password){ int flag = 0; char buffer[20]; strcpy(buffer, password); if(strcmp(buffer, "mypass") == 0){ flag = 1; } if(strcmp(buffer, "yourpass") == 0){ flag = 1; } return flag; } int main(int argc, char *argv[]){ if(argc >= 2){ if(check_password(argv[1])){ printf("%s", "Access granted\n"); }else{ printf("%s", "Access denied\n"); } }else{ printf("%s", "Please enter password!\n"); } } [email protected]:$ gcc -g -fno-stack-protector test_overflow.c [email protected]:$ ./a.out mypass Access granted [email protected]:$ ./a.out yourpass Access granted [email protected]:$ ./a.out wepass Access denied [email protected]:$ ./a.out wepassssssssssssssssss Access granted [email protected]:$ gcc -g -fstack-protector test_overflow.c [email protected]:$ ./a.out wepass Access denied [email protected]:$ ./a.out mypass Access granted [email protected]:$ ./a.out yourpass Access granted [email protected]:$ ./a.out wepassssssssssssssssss *** stack smashing detected ***: ./a.out terminated ======= Backtrace: ========= /lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)[0xce0ed8] /lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x0)[0xce0e90] ./a.out[0x8048524] ./a.out[0x8048545] /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6)[0xc16b56] ./a.out[0x8048411] ======= Memory map: ======== 007d9000-007f5000 r-xp 00000000 08:06 5776 /lib/libgcc_s.so.1 007f5000-007f6000 r--p 0001b000 08:06 5776 /lib/libgcc_s.so.1 007f6000-007f7000 rw-p 0001c000 08:06 5776 /lib/libgcc_s.so.1 0090a000-0090b000 r-xp 00000000 00:00 0 [vdso] 00c00000-00d3e000 r-xp 00000000 08:06 1183 /lib/tls/i686/cmov/libc-2.10.1.so 00d3e000-00d3f000 ---p 0013e000 08:06 1183 /lib/tls/i686/cmov/libc-2.10.1.so 00d3f000-00d41000 r--p 0013e000 08:06 1183 /lib/tls/i686/cmov/libc-2.10.1.so 00d41000-00d42000 rw-p 00140000 08:06 1183 /lib/tls/i686/cmov/libc-2.10.1.so 00d42000-00d45000 rw-p 00000000 00:00 0 00e0c000-00e27000 r-xp 00000000 08:06 4213 /lib/ld-2.10.1.so 00e27000-00e28000 r--p 0001a000 08:06 4213 /lib/ld-2.10.1.so 00e28000-00e29000 rw-p 0001b000 08:06 4213 /lib/ld-2.10.1.so 08048000-08049000 r-xp 00000000 08:05 1056811 /dos/hacking/test/a.out 08049000-0804a000 r--p 00000000 08:05 1056811 /dos/hacking/test/a.out 0804a000-0804b000 rw-p 00001000 08:05 1056811 /dos/hacking/test/a.out 08675000-08696000 rw-p 00000000 00:00 0 [heap] b76fe000-b76ff000 rw-p 00000000 00:00 0 b7717000-b7719000 rw-p 00000000 00:00 0 bfc1c000-bfc31000 rw-p 00000000 00:00 0 [stack] Aborted [email protected]:$ 

Quando ho distriggersto la protezione per lo smashing della pila non sono stati rilevati errori, il che dovrebbe essere successo quando ho usato “./a.out wepassssssssssssssssss”

Quindi, per rispondere alla tua domanda di cui sopra, è stato visualizzato il messaggio “** stack smashing detected: xxx” perché il tuo stack smashing protector era attivo e ha scoperto che vi è uno stack overflow nel tuo programma.

Basta scoprire dove si verifica e risolverlo.

Puoi provare a eseguire il debug del problema usando valgrind :

La distribuzione di Valgrind include attualmente sei strumenti di qualità di produzione: un rilevatore di errori di memoria, due rilevatori di errori di thread, un profiler di cache e di previsione delle branch, un profiler della cache che genera un grafico delle chiamate e un profiler dell’heap. Include anche due strumenti sperimentali: un rilevatore di sovraccarico di array / stack / array globale e un generatore di vettori di blocchi di base di SimPoint. Funziona sulle seguenti piattaforms: X86 / Linux, AMD64 / Linux, PPC32 / Linux, PPC64 / Linux e X86 / Darwin (Mac OS X).

Significa che hai scritto alcune variabili sullo stack in modo illegale, molto probabilmente come risultato di un overflow del buffer .

Esempio minimo con analisi di sassembly

AC:

 void myfunc(char *const src, int len) { int i; for (i = 0; i < len; ++i) { src[i] = 42; } } int main(void) { char arr[] = {'a', 'b', 'c', 'd'}; int len = sizeof(arr); myfunc(arr, len + 1); return 0; } 

Compilare ed eseguire:

 gcc -fstack-protector -g -O0 -std=c99 ac ulimit -c unlimited && rm -f core ./a.out 

fallisce come desiderato:

 *** stack smashing detected ***: ./a.out terminated Aborted (core dumped) 

Sassembly

Ora guardiamo allo sassembly:

 objdump -D a.out 

che contiene:

 int main (void){ 400579: 55 push %rbp 40057a: 48 89 e5 mov %rsp,%rbp # Allocate 0x10 of stack space. 40057d: 48 83 ec 10 sub $0x10,%rsp # Put the 8 byte canary from %fs:0x28 to -0x8(%rbp), # which is right at the bottom of the stack. 400581: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 400588: 00 00 40058a: 48 89 45 f8 mov %rax,-0x8(%rbp) 40058e: 31 c0 xor %eax,%eax char arr[] = {'a', 'b', 'c', 'd'}; 400590: c6 45 f4 61 movb $0x61,-0xc(%rbp) 400594: c6 45 f5 62 movb $0x62,-0xb(%rbp) 400598: c6 45 f6 63 movb $0x63,-0xa(%rbp) 40059c: c6 45 f7 64 movb $0x64,-0x9(%rbp) int len = sizeof(arr); 4005a0: c7 45 f0 04 00 00 00 movl $0x4,-0x10(%rbp) myfunc(arr, len + 1); 4005a7: 8b 45 f0 mov -0x10(%rbp),%eax 4005aa: 8d 50 01 lea 0x1(%rax),%edx 4005ad: 48 8d 45 f4 lea -0xc(%rbp),%rax 4005b1: 89 d6 mov %edx,%esi 4005b3: 48 89 c7 mov %rax,%rdi 4005b6: e8 8b ff ff ff callq 400546  return 0; 4005bb: b8 00 00 00 00 mov $0x0,%eax } 
  # Check that the canary at -0x8(%rbp) hasn't changed after calling myfunc. # If it has, jump to the failure point __stack_chk_fail. 4005c0: 48 8b 4d f8 mov -0x8(%rbp),%rcx 4005c4: 64 48 33 0c 25 28 00 xor %fs:0x28,%rcx 4005cb: 00 00 4005cd: 74 05 je 4005d4  4005cf: e8 4c fe ff ff callq 400420 <[email protected]> # Otherwise, exit normally. 4005d4: c9 leaveq 4005d5: c3 retq 4005d6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 4005dd: 00 00 00 

Notare i comodi commenti aggiunti automaticamente dal modulo di intelligenza artificiale di objdump .

Se esegui questo programma più volte tramite GDB, vedrai che:

  • il canarino ottiene un valore casuale diverso ogni volta
  • l'ultimo ciclo di myfunc è esattamente ciò che modifica l'indirizzo del canarino

Ora la grande domanda è come il canarino viene inizializzato a %fs:0x28 per iniziare, quindi vi lascio a:

Tentativi di debug

Da ora in poi, modifichiamo il codice:

  myfunc(arr, len + 1); 

essere invece:

  myfunc(arr); myfunc(arr, len + 1); /* line 12 */ myfunc(arr); 

essere più interessante

Cercheremo quindi di vedere se possiamo individuare la chiamata colpevole + 1 con un metodo più automatico della semplice lettura e comprensione dell'intero codice sorgente.

gcc -fsanitize=address

Con questa bandiera, funziona brutalmente e produce:

 #0 0x4008bf in myfunc /home/cirsan01/test/ac:4 #1 0x40099b in main /home/cirsan01/test/ac:12 #2 0x7fcd2e13d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f) #3 0x400798 in _start (/home/cirsan01/test/a.out+0x40079 

seguito da un output più colorato. Grazie a Google .

Valgrind SGCheck

Come detto da altri , Valgrind non è bravo a risolvere questo tipo di problema.

Ha uno strumento sperimentale chiamato SGCheck :

SGCheck è uno strumento per trovare sovraccarichi di stack e array globali. Funziona utilizzando un approccio euristico derivato da un'osservazione sulle possibili forms di stack e sugli accessi globali agli array.

Quindi non ero molto sorpreso quando non ha trovato l'errore:

 valgrind --tool=exp-sgcheck ./a.out 

Il messaggio di errore dovrebbe apparire così: Valgrind errore mancante

GDB

Un'osservazione importante è che se si esegue il programma tramite GDB o si esamina il file core dopo il fatto:

 gdb -nh -q a.out core 

poi, come abbiamo visto sull'assemblea, GDB dovrebbe indirizzarti alla fine della funzione che ha fatto il controllo delle canarie:

 (gdb) bt #0 0x00007f0f66e20428 in __GI_raise ([email protected]=6) at ../sysdeps/unix/sysv/linux/raise.c:54 #1 0x00007f0f66e2202a in __GI_abort () at abort.c:89 #2 0x00007f0f66e627ea in __libc_message ([email protected]=1, [email protected]=0x7f0f66f7a49f "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:175 #3 0x00007f0f66f0415c in __GI___fortify_fail (msg=, [email protected]=0x7f0f66f7a481 "stack smashing detected") at fortify_fail.c:37 #4 0x00007f0f66f04100 in __stack_chk_fail () at stack_chk_fail.c:28 #5 0x00000000004005f6 in main () at ac:15 (gdb) f 5 #5 0x00000000004005f6 in main () at ac:15 15 } (gdb) 

E quindi il problema è probabile in una delle chiamate fatte da questa funzione.

Quindi proviamo a individuare l'esatta chiamata fallita con il primo singolo passo dopo che è stato impostato il canary:

  400581: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 400588: 00 00 40058a: 48 89 45 f8 mov %rax,-0x8(%rbp) 

e guardando l'indirizzo:

 (gdb) p $rbp - 0x8 $1 = (void *) 0x7fffffffcf18 (gdb) watch 0x7fffffffcf18 Hardware watchpoint 2: *0x7fffffffcf18 (gdb) c Continuing. Hardware watchpoint 2: *0x7fffffffcf18 Old value = 1800814336 New value = 1800814378 myfunc (src=0x7fffffffcf14 "*****?Vk\266", , len=5) at ac:3 3 for (i = 0; i < len; ++i) { (gdb) p len $2 = 5 (gdb) pi $3 = 4 (gdb) bt #0 myfunc (src=0x7fffffffcf14 "*****?Vk\266", , len=5) at ac:3 #1 0x00000000004005cc in main () at ac:12 

Ora, questo ci lascia alle giuste istruzioni offensive: len = 5 e i = 4 , e in questo caso particolare, ci ha indirizzato alla linea colpevole 12.

Tuttavia, il backtrace è corrotto e contiene del cestino. Un backtrace corretto dovrebbe essere simile a:

 #0 myfunc (src=0x7fffffffcf14 "abcd", len=4) at ac:3 #1 0x00000000004005b8 in main () at ac:11 

quindi forse questo potrebbe corrompere lo stack e impedirti di vedere la traccia.

Inoltre, questo metodo richiede di sapere quale sia l'ultima chiamata della funzione di controllo del canarino altrimenti si avranno falsi positivi, che non saranno sempre fattibili, a meno che non si utilizzi il debug inverso .

Testato su Ubuntu 16.04, gcc 6.4.0.

Quali potrebbero essere le possibili ragioni per questo e come posso correggerlo?

Uno scenario potrebbe essere nel seguente esempio:

 #include  #include  #include  void swap ( char *a , char *b ); void revSTR ( char *const src ); int main ( void ){ char arr[] = "ABCDE"; revSTR( arr ); printf("ARR = %s\n", arr ); } void swap ( char *a , char *b ){ char tmp = *a; *a = *b; *b = tmp; } void revSTR ( char *const src ){ char *start = src; char *end = start + ( strlen( src ) - 1 ); while ( start < end ){ swap( &( *start ) , &( *end ) ); start++; end--; } } 

In questo programma è ansible invertire una stringa o una parte della stringa se per esempio si chiama reverse() con qualcosa del genere:

 reverse( arr + 2 ); 

Se decidi di superare la lunghezza dell'array in questo modo:

 #include  #include  #include  void swap ( char *a , char *b ); void revSTR ( char *const src, size_t len ); int main ( void ){ char arr[] = "ABCDE"; size_t len = strlen( arr ); revSTR( arr, len ); printf("ARR = %s\n", arr ); } void swap ( char *a , char *b ){ char tmp = *a; *a = *b; *b = tmp; } void revSTR ( char *const src, size_t len ){ char *start = src; char *end = start + ( len - 1 ); while ( start < end ){ swap( &( *start ) , &( *end ) ); start++; end--; } } 

Funziona bene anche.

Ma quando lo fai:

 revSTR( arr + 2, len ); 

Ottieni:

 ==7125== Command: ./program ==7125== ARR = A- *** stack smashing detected ***: ./program terminated ==7125== ==7125== Process terminating with default action of signal 6 (SIGABRT) ==7125== at 0x4E6F428: raise (raise.c:54) ==7125== by 0x4E71029: abort (abort.c:89) ==7125== by 0x4EB17E9: __libc_message (libc_fatal.c:175) ==7125== by 0x4F5311B: __fortify_fail (fortify_fail.c:37) ==7125== by 0x4F530BF: __stack_chk_fail (stack_chk_fail.c:28) ==7125== by 0x400637: main (program.c:14) 

E questo accade perché nel primo codice, la lunghezza di arr è controllata all'interno di revSTR() che va bene, ma nel secondo codice in cui si passa la lunghezza:

 revSTR( arr + 2, len ); 

la lunghezza ora è più lunga della lunghezza effettivamente passata quando dici arr + 2 .

Lunghezza di strlen ( arr + 2 ) ! = strlen ( arr ) .

Sovrascrive le corruzioni causate usualmente dai buffer overflow. Puoi difenderti contro di loro programmando in modo difensivo.

Ogni volta che accedi a un array, metti un asser prima di assicurarti che l’accesso non sia fuori limite. Per esempio:

 assert(i + 1 < N); assert(i < N); a[i + 1] = a[i]; 

Questo ti fa pensare ai limiti degli array e ti fa anche pensare di aggiungere test per triggersrli, se ansible. Se alcune di queste affermazioni possono fallire durante il normale uso, trasformarle in un normale if .

Ho ricevuto questo errore durante l’utilizzo di malloc () per allocare memoria a una struct * dopo aver passato un po ‘di questo debug al codice, infine ho usato la funzione free () per liberare la memoria allocata e successivamente il messaggio di errore sparito 🙂

Un’altra fonte di smashing è l’uso (scorretto) di vfork() invece di fork() .

Ho appena eseguito il debug di questo caso, in cui il processo figlio non è stato in grado di eseguire execve() l’eseguibile di destinazione e ha restituito un codice di errore anziché chiamare _exit() .

Poiché vfork() ha generato il figlio, esso è ritornato mentre è ancora in esecuzione nello spazio del processo del genitore, non solo corrompendo lo stack del genitore, ma causando la stampa di due diversi set di diagnostica con il codice “downstream”.

Cambiare vfork() in fork() risolse entrambi i problemi, come invece cambiando l’istruzione return del figlio in _exit() .

Ma poiché il codice figlio precede la chiamata execve() con chiamate ad altre routine (per impostare uid / gid, in questo caso particolare), tecnicamente non soddisfa i requisiti per vfork() , quindi cambiandolo per usare fork() è corretto qui.

(Si noti che l’affermazione problematica del reso non era effettivamente codificata come tale – invece, è stata invocata una macro e quella macro ha deciso se _exit() o return base a una variabile globale. Quindi non era immediatamente ovvio che il codice figlio era non conforms per l’uso di vfork() ).

Per ulteriori informazioni, vedere:

La differenza tra fork (), vfork (), exec () e clone ()