Rilevato danneggiamento dello stack


246

Sto eseguendo il mio file a.out. Dopo l'esecuzione il programma viene eseguito per un po 'di 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 di ciò e come posso correggerlo?


2
Potresti forse identificare quali parti del tuo codice causano lo smashing dello stack e pubblicarlo? Quindi probabilmente saremo in grado di indicare esattamente perché ciò accade e come correggerlo.
Bjarke Freund-Hansen,

Penso che sia sinonimo di errore di overflow. Ad esempio, se si inizializza e un array di 5 elementi questo errore verrà visualizzato quando si tenta di scrivere il sesto elemento o qualsiasi elemento al di fuori dei limiti dell'array.
DorinPopescu,

Risposte:


349

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

#include <stdio.h>

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 interruzione del programma da parte di SIGABRT.

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 durante il tentativo di accedere a una posizione di memoria illegale. Nota che -fstack-protectordovrebbe essere sempre attivato per build di rilascio in quanto è una funzionalità di sicurezza.

È possibile ottenere alcune informazioni sul punto di overflow eseguendo il programma con un debugger. Valgrind non funziona bene con errori relativi allo stack, ma come un debugger, può aiutarti a individuare la posizione e il motivo dell'incidente.


3
grazie per questa risposta! Ho scoperto che nel mio caso non avevo inizializzato la variabile che stavo cercando di scrivere
Ted Pennings,

5
Valgrind non funziona bene per errori relativi allo stack, poiché non può aggiungere zone rosse lì
toasted_flakes

7
Questa risposta è errata e fornisce consigli pericolosi. Innanzitutto, rimuovere la protezione dello stack non è la soluzione giusta: se si verifica un errore di distruzione dello stack, è probabile che nel codice sia presente una grave vulnerabilità della sicurezza. La risposta corretta è correggere il codice errato . In secondo luogo, come sottolinea grasGendarme, la raccomandazione di provare Valgrind non sarà efficace. Valgrind in genere non funziona per rilevare accessi di memoria illegali a dati allocati nello stack.
DW

22
L'OP chiede possibili motivi per questo comportamento, la mia risposta fornisce un esempio e come si collega a un errore ragionevolmente noto. Inoltre, rimuovere lo stack-protector non è una soluzione, è una specie di esperimento che si potrebbe fare per ottenere ulteriori approfondimenti sul problema. Il consiglio in realtà è quello di correggere l'errore in qualche modo, grazie per indicare su valgrind, modificherò la mia risposta per riflettere questo.
sud03r

4
@DW la protezione dello stack dovrebbe essere disattivata in una versione di rilascio, perché all'inizio - il messaggio rilevato di smash dello stack è un aiuto solo per gli sviluppatori; in secondo luogo, un'applicazione potrebbe avere ancora possibilità di sopravvivere; e in terzo luogo, questa è una piccola ottimizzazione.
Hi-Angel,

33

Esempio di riproduzione minima con analisi di smontaggio

main.c

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;
}

GitHub a monte .

Compila ed esegui:

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

fallisce come desiderato:

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

Testato su Ubuntu 16.04, GCC 6.4.0.

Smontaggio

Ora guardiamo allo smontaggio:

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 <myfunc>
    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 <main+0x5b>
  4005cf:       e8 4c fe ff ff          callq  400420 <__stack_chk_fail@plt>

  # 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 

Notate i commenti a portata di mano aggiunti automaticamente dal objdump's modulo di intelligenza artificiale .

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

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

Canarino randomizzato impostandolo con %fs:0x28, che contiene un valore casuale come spiegato a:

Tentativi di debug

D'ora in poi, modifichiamo il codice:

    myfunc(arr, len + 1);

essere invece:

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

per essere più interessante.

Proveremo quindi a vedere se siamo in grado di individuare la + 1chiamata del colpevole con un metodo più automatizzato della semplice lettura e comprensione dell'intero codice sorgente.

gcc -fsanitize=address abilitare Sanitizer indirizzo di Google (ASan)

Se si ricompila con questo flag ed si esegue il programma, viene emesso:

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

seguito da un output più colorato.

Questo individua chiaramente la linea problematica 12.

Il codice sorgente per questo è su: https://github.com/google/sanitizers ma come abbiamo visto dall'esempio è già a monte in GCC.

ASan può anche rilevare altri problemi di memoria come perdite di memoria: come trovare la perdita di memoria in un codice / progetto C ++?

Valgrind SGCheck

Come menzionato 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 probabili forme di accesso allo stack e agli array globali.

Quindi non sono stato molto sorpreso quando non ha trovato l'errore:

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

Apparentemente il messaggio di errore dovrebbe apparire così: errore mancante di Valgrind

GDB

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

gdb -nh -q a.out core

quindi, come abbiamo visto nell'assemblea, GDB dovrebbe indicarti la fine della funzione che ha effettuato il controllo canarino:

(gdb) bt
#0  0x00007f0f66e20428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007f0f66e2202a in __GI_abort () at abort.c:89
#2  0x00007f0f66e627ea in __libc_message (do_abort=do_abort@entry=1, fmt=fmt@entry=0x7f0f66f7a49f "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:175
#3  0x00007f0f66f0415c in __GI___fortify_fail (msg=<optimized out>, msg@entry=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 main.c:15
(gdb) f 5
#5  0x00000000004005f6 in main () at main.c:15
15      }
(gdb)

E quindi il problema è probabilmente in una delle chiamate effettuate da questa funzione.

Quindi proviamo a individuare l'esatta chiamata non riuscita dal primo singolo incremento subito dopo aver impostato il canarino:

  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", <incomplete sequence \355\216>, len=5) at main.c:3
3           for (i = 0; i < len; ++i) {
(gdb) p len
$2 = 5
(gdb) p i
$3 = 4
(gdb) bt
#0  myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
#1  0x00000000004005cc in main () at main.c:12

Ora, questo ci lascia alla giusta istruzione offensiva: len = 5e i = 4, in questo caso particolare, ci ha indicato la linea colpevole 12.

Tuttavia, il backtrace è danneggiato e contiene alcuni rifiuti. Un backtrace corretto sarebbe simile a:

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

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

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


16

Si prega di guardare la seguente situazione:

ab@cd-x:$ cat test_overflow.c 
#include <stdio.h>
#include <string.h>

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");
    }
}
ab@cd-x:$ gcc -g -fno-stack-protector test_overflow.c 
ab@cd-x:$ ./a.out mypass
Access granted
ab@cd-x:$ ./a.out yourpass
Access granted
ab@cd-x:$ ./a.out wepass
Access denied
ab@cd-x:$ ./a.out wepassssssssssssssssss
Access granted

ab@cd-x:$ gcc -g -fstack-protector test_overflow.c 
ab@cd-x:$ ./a.out wepass
Access denied
ab@cd-x:$ ./a.out mypass
Access granted
ab@cd-x:$ ./a.out yourpass
Access granted
ab@cd-x:$ ./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
ab@cd-x:$ 

Quando ho disabilitato lo stack smashing protector non sono stati rilevati errori, cosa che avrebbe dovuto accadere quando ho usato "./a.out wepassssssssssssssssss"

Quindi, per rispondere alla domanda precedente, è stato visualizzato il messaggio "** Rottura dello stack rilevata: xxx" perché la protezione dello smashing dello stack era attiva e ha rilevato la presenza di un overflow dello stack nel programma.

Scopri dove succede e risolvilo.


7

Puoi provare a eseguire il debug del problema usando valgrind :

La distribuzione di Valgrind comprende attualmente sei strumenti di qualità di produzione: un rilevatore di errori di memoria, due rilevatori di errori di thread, un profiler di cache e previsione dei rami, un grafico di chiamata che genera un profiler di cache e un profilatore di heap. Include anche due strumenti sperimentali: un rilevatore di sovraccarico heap / stack / global array e un generatore vettoriale di blocchi di base SimPoint. Funziona sulle seguenti piattaforme: X86 / Linux, AMD64 / Linux, PPC32 / Linux, PPC64 / Linux e X86 / Darwin (Mac OS X).


2
Sì, ma Valgrind non funziona bene per gli overflow dei buffer allocati nello stack, che è la situazione indicata da questo messaggio di errore.
DW

4
Come potremmo usare quel rivelatore di sovraccarico di array di stack ? Puoi elaborare?
Craig McQueen,

@CraigMcQueen Ho provato a utilizzare il rilevatore euristico sperimentale di stack SGCheck di Valgrind su un esempio minimo: stackoverflow.com/a/51897264/895245 ma non è riuscito.
Ciro Santilli 17 冠状 病 六四 事件 法轮功

4

Significa che hai scritto su alcune variabili nello stack in modo illegale, molto probabilmente a causa di un overflow del buffer .


9
Stack Overflow è lo stack che si rompe in qualcos'altro. Qui è il contrario: qualcosa si è schiantato nello stack.
Peter Mortensen,

5
Non proprio. È una parte della pila che si frantuma in un'altra parte. Quindi è davvero un buffer overflow, non solo sopra lo stack, ma "solo" in un'altra parte dello stack.
Bas Wijnen,

2

Quali potrebbero essere le possibili ragioni di ciò e come posso correggerlo?

Uno scenario sarebbe nell'esempio seguente:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void swap ( char *a , char *b );
void revSTR ( char *const src );

int main ( void ){
    char arr[] = "A-B-C-D-E";

    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 puoi invertire una stringa o una parte della stringa se ad esempio chiami reverse()con qualcosa del genere:

reverse( arr + 2 );

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void swap ( char *a , char *b );
void revSTR ( char *const src, size_t len );

int main ( void ){
    char arr[] = "A-B-C-D-E";
    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 arrviene verificata all'interno del revSTR()quale va bene, ma nel secondo codice dove 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 ).


1
Mi piace questo esempio perché non si basa su funzioni di libreria standard come getse scrcpy. Mi chiedo se potremmo minimizzare se ulteriormente. Vorrei almeno sbarazzarsi di string.hcon size_t len = sizeof( arr );. Testato su gcc 6.4, Ubuntu 16.04. Vorrei anche dare l'esempio negativo con il arr + 2per minimizzare incollare copia.
Ciro Santilli 17 冠状 病 六四 事件 法轮功

1

Stack corruzioni solitamente causate da overflow del buffer. Puoi difenderti programmando in modo difensivo.

Ogni volta che accedi a un array, metti un'asserzione davanti ad esso per assicurarti che l'accesso non sia fuori dai limiti. Per esempio:

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

Questo ti fa pensare ai limiti dell'array e ti fa anche pensare all'aggiunta di test per attivarli, se possibile. Se alcune di queste affermazioni possono fallire durante il normale utilizzo, trasformale in normali if.


0

Ho riscontrato questo errore durante l'utilizzo di malloc () per allocare un po 'di memoria in uno struct * dopo aver speso un po' questo debug del codice, ho finalmente usato la funzione free () per liberare la memoria allocata e successivamente il messaggio di errore è andato :)


0

Un'altra fonte di distruzione dello stack è l'uso (errato) vfork()invece di fork().

Ho appena eseguito il debug di un caso di questo, 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()aveva generato quel figlio, è tornato mentre in realtà era ancora in esecuzione nello spazio del processo del genitore, non solo corrompendo lo stack del genitore, ma causando la stampa di due serie disparate di diagnostica mediante il codice "downstream".

Cambiare vfork()per fork()risolvere entrambi i problemi, così come cambiare invece l' returnaffermazione del bambino _exit().

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

(Si noti che l' returnistruzione problematica in realtà non è stata codificata come tale - invece, è stata invocata una macro e quella macro ha deciso se _exit()o in returnbase a una variabile globale. Quindi non era immediatamente ovvio che il codice figlio non era conforme per l' vfork()uso. )

Per ulteriori informazioni, vedere:

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.