Rotura de pila detectada

246

Estoy ejecutando mi archivo a.out. Después de la ejecución, el programa se ejecuta durante un tiempo y luego sale con el mensaje:

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

¿Cuáles podrían ser las posibles razones para esto y cómo lo rectifico?

Biswajyoti Das
fuente
2
¿Podrías quizás identificar qué partes de tu código causan la destrucción de la pila y publicarla? Entonces probablemente podamos señalar exactamente por qué sucede y cómo corregirlo.
Bjarke Freund-Hansen
Creo que es sinónimo de error de desbordamiento. Por ejemplo, si inicializa una matriz de 5 elementos, este error aparecerá cuando intente escribir el sexto elemento, o cualquier elemento fuera de los límites de la matriz.
DorinPopescu

Respuestas:

349

Stack Smashing aquí en realidad se debe a un mecanismo de protección utilizado por gcc para detectar errores de desbordamiento del búfer. Por ejemplo en el siguiente fragmento:

#include <stdio.h>

void func()
{
    char array[10];
    gets(array);
}

int main(int argc, char **argv)
{
    func();
}

El compilador (en este caso gcc) agrega variables de protección (llamadas canarias) que tienen valores conocidos. Una cadena de entrada de tamaño superior a 10 provoca la corrupción de esta variable, lo que hace que SIGABRT finalice el programa.

Para obtener una idea, puede intentar deshabilitar esta protección de gcc usando la opción -fno-stack-protector mientras compila. En ese caso, obtendrá un error diferente, muy probablemente un error de segmentación al intentar acceder a una ubicación de memoria ilegal. Tenga en cuenta que -fstack-protectorsiempre debe estar activado para las versiones de lanzamiento, ya que es una característica de seguridad.

Puede obtener información sobre el punto de desbordamiento ejecutando el programa con un depurador. Valgrind no funciona bien con los errores relacionados con la pila, pero como un depurador, puede ayudarlo a determinar la ubicación y el motivo del bloqueo.

sud03r
fuente
3
gracias por esta respuesta! Descubrí que en mi caso no había inicializado la variable en la que estaba tratando de escribir
Ted Pennings,
55
Valgrind no funciona bien para los errores relacionados con la pila, ya que no puede agregar zonas rojas allí
toasted_flakes
77
Esta respuesta es incorrecta y proporciona consejos peligrosos. En primer lugar, eliminar el protector de pila no es la solución correcta: si recibe un error de aplastamiento de la pila, probablemente tenga una vulnerabilidad de seguridad grave en su código. La respuesta correcta es corregir el código con errores . En segundo lugar, como señala grasGendarme, la recomendación de probar Valgrind no será efectiva. Valgrind generalmente no funciona para detectar accesos ilegales de memoria a datos asignados a la pila.
DW
22
El OP pregunta por posibles razones para este comportamiento, mi respuesta proporciona un ejemplo y cómo se relaciona con un error razonablemente conocido. Además, quitar el protector de pila no es una solución, es una especie de experimento que uno podría hacer para obtener más información sobre el problema. El consejo en realidad es corregir el error de alguna manera, gracias por señalar valgrind, editaré mi respuesta para reflejar esto.
sud03r
44
@DW, la protección de la pila debe estar desactivada en una versión de lanzamiento, porque al principio, el mensaje de aplastamiento de la pila detectado es una ayuda solo para desarrolladores; en segundo lugar, una aplicación podría tener posibilidades de sobrevivir; y en tercer lugar, esta es una pequeña optimización.
Hola Ángel
33

Ejemplo de reproducción mínima con análisis de desmontaje

C Principal

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 aguas arriba .

Compilar y ejecutar:

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

falla como se desea:

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

Probado en Ubuntu 16.04, GCC 6.4.0.

Desmontaje

Ahora nos fijamos en el desmontaje:

objdump -D a.out

que 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 

Observe los prácticos comentarios agregados automáticamente por objdump's módulo de inteligencia artificial .

Si ejecuta este programa varias veces a través de GDB, verá que:

  • el canario obtiene un valor aleatorio diferente cada vez
  • el último bucle de myfunces exactamente lo que modifica la dirección del canario

El canario se aleatorizó configurándolo con %fs:0x28, que contiene un valor aleatorio como se explica en:

Intentos de depuración

De ahora en adelante, modificamos el código:

    myfunc(arr, len + 1);

ser en su lugar:

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

para ser más interesante

Luego intentaremos ver si podemos identificar la + 1llamada del culpable con un método más automatizado que simplemente leer y comprender todo el código fuente.

gcc -fsanitize=address para habilitar el desinfectante de direcciones de Google (ASan)

Si vuelve a compilar con este indicador y ejecuta el programa, genera:

#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

seguido por algo más de salida de color.

Esto señala claramente la problemática línea 12.

El código fuente para esto está en: https://github.com/google/sanitizers pero, como vimos en el ejemplo, ya está integrado en GCC.

ASan también puede detectar otros problemas de memoria, como pérdidas de memoria: ¿Cómo encontrar la pérdida de memoria en un código / proyecto C ++?

Valgrind SGCheck

Como mencionaron otros , Valgrind no es bueno para resolver este tipo de problema.

Tiene una herramienta experimental llamada SGCheck :

SGCheck es una herramienta para encontrar desbordamientos de pila y matrices globales. Funciona utilizando un enfoque heurístico derivado de una observación sobre las formas probables de acceso a la pila y a la matriz global.

Así que no me sorprendió mucho cuando no encontró el error:

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

Aparentemente, el mensaje de error debería verse así: Valgrind falta error

GDB

Una observación importante es que si ejecuta el programa a través de GDB, o examina el corearchivo después del hecho:

gdb -nh -q a.out core

entonces, como vimos en el ensamblaje, GDB debería indicarle el final de la función que realizó la verificación canaria:

(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)

Y por lo tanto, el problema es probable en una de las llamadas que realizó esta función.

A continuación, tratamos de determinar la llamada fallida exacta al dar un solo paso adelante justo después de configurar el canario:

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

y mirando la dirección:

(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

Ahora, esto nos deja en la instrucción ofensiva correcta: len = 5y i = 4, en este caso particular, nos indicó la línea culpable 12.

Sin embargo, la traza inversa está dañada y contiene algo de basura. Una traza inversa correcta se vería así:

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

entonces tal vez esto podría corromper la pila y evitar que veas el rastro.

Además, este método requiere saber cuál es la última llamada de la función de verificación canaria; de lo contrario, tendrá falsos positivos, lo que no siempre será factible, a menos que utilice la depuración inversa .

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
16

Mire la siguiente situación:

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:$ 

Cuando deshabilité el protector de aplastamiento de la pila, no se detectaron errores, lo que debería haber sucedido cuando usé "./a.out wepassssssssssssssssss"

Entonces, para responder a su pregunta anterior, se mostró el mensaje "** stack smashing detectado: xxx" porque su protector de stack smashing estaba activo y descubrió que hay un desbordamiento de pila en su programa.

Solo averigua dónde ocurre eso y arréglalo.

wearetherock
fuente
7

Podrías intentar depurar el problema usando valgrind :

La distribución Valgrind actualmente incluye seis herramientas de calidad de producción: un detector de errores de memoria, dos detectores de errores de subprocesos, un generador de perfiles de caché y predicción de bifurcación, un generador de perfiles de caché generador de gráficos de llamadas y un generador de perfiles de montón. También incluye dos herramientas experimentales: un detector de desbordamiento de montón / pila / matriz global y un generador de vector de bloque básico SimPoint. Se ejecuta en las siguientes plataformas: X86 / Linux, AMD64 / Linux, PPC32 / Linux, PPC64 / Linux y X86 / Darwin (Mac OS X).

hlovdal
fuente
2
Sí, pero Valgrind no funciona bien para desbordamientos de buffers asignados a la pila, que es la situación que indica este mensaje de error.
DW
44
¿Cómo podríamos usar ese detector de desbordamiento de matriz de pila ? ¿Puedes elaborar?
Craig McQueen
@CraigMcQueen Intenté usar el detector de aplastamiento de pila SGCheck heurístico experimental de Valgrind en un ejemplo mínimo: stackoverflow.com/a/51897264/895245 pero falló.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
4

Significa que escribió en algunas variables en la pila de manera ilegal, probablemente como resultado de un desbordamiento de Buffer .

starblue
fuente
99
El desbordamiento de la pila es la pila que se estrella contra otra cosa. Aquí es al revés: algo se ha estrellado en la pila.
Peter Mortensen
55
Realmente no. Es una parte de la pila que se estrella contra otra parte. Por lo tanto, realmente es un desbordamiento del búfer, simplemente no sobre la parte superior de la pila, sino "solo" en otra parte de la pila.
Bas Wijnen
2

¿Cuáles podrían ser las posibles razones para esto y cómo lo rectifico?

Un escenario sería en el siguiente ejemplo:

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

En este programa, puede invertir una Cadena o una parte de la cadena si, por ejemplo, llama reverse()con algo como esto:

reverse( arr + 2 );

Si decide pasar la longitud de la matriz de esta manera:

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

Funciona bien también.

Pero cuando haces esto:

revSTR( arr + 2, len );

Obtienes:

==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)

Y esto sucede porque en el primer código, la longitud de arrse verifica dentro de la revSTR()cual está bien, pero en el segundo código donde pasa la longitud:

revSTR( arr + 2, len );

la longitud ahora es más larga que la longitud real que pasas cuando dices arr + 2.

Longitud de strlen ( arr + 2 )! = strlen ( arr ).

Michi
fuente
1
Me gusta este ejemplo porque no se basa en funciones de biblioteca estándar como getsy scrcpy. Me pregunto si podríamos minimizar si es más. Yo al menos deshacerse de string.hcon size_t len = sizeof( arr );. Probado en gcc 6.4, Ubuntu 16.04. También daría el ejemplo erróneo con el arr + 2fin de minimizar el pegado de copias.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1

Las corrupciones de la pila usualmente causadas por desbordamientos del búfer. Puedes defenderte de ellos programando a la defensiva.

Siempre que acceda a una matriz, coloque una aserción antes para asegurarse de que el acceso no esté fuera de los límites. Por ejemplo:

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

Esto te hace pensar en los límites de la matriz y también te hace pensar en agregar pruebas para activarlas si es posible. Si algunas de estas afirmaciones pueden fallar durante el uso normal, conviértalas en algo normal if.

Calmarius
fuente
0

Recibí este error mientras usaba malloc () para asignar algo de memoria a una estructura * después de gastar algo de esto en la depuración del código, finalmente utilicé la función free () para liberar la memoria asignada y posteriormente desapareció el mensaje de error :)

djangodude
fuente
0

Otra fuente de aplastamiento de la pila es el uso (incorrecto) de en vfork()lugar de fork().

Acabo de depurar un caso de esto, donde el proceso secundario no pudo execve()ejecutar el ejecutable de destino y devolvió un código de error en lugar de llamar _exit().

Debido a que vfork()había engendrado a ese hijo, regresó mientras aún se estaba ejecutando dentro del espacio de proceso del padre, no solo corrompiendo la pila del padre, sino que causó que dos conjuntos dispares de diagnósticos se imprimieran mediante código "aguas abajo".

Cambiar vfork()para fork()solucionar ambos problemas, al igual que cambiar la returndeclaración del niño a _exit()cambio.

Pero dado que el código secundario precede a la execve()llamada con llamadas a otras rutinas (para establecer el uid / gid, en este caso particular), técnicamente no cumple con los requisitos vfork(), por lo que cambiarlo para usarlo fork()es correcto aquí.

(Tenga en cuenta que la returndeclaración problemática en realidad no estaba codificada como tal; en cambio, se invocó una macro y esa macro decidió si se basaba _exit()o no returnen una variable global. Por lo tanto, no era obvio de inmediato que el código secundario no era conforme paravfork() uso. )

Para más información, ver:

La diferencia entre fork (), vfork (), exec () y clone ()

James Craig Burley
fuente