¿Qué es la destrucción de pilas (C)?

7 minutos de lectura

Avatar de usuario de González
González

Código:

int str_join(char *a,  const char *b) {
   int sz =0; 
   while(*a++) sz++;  
   char *st = a -1, c;  
   *st = (char) 32;
   while((c = *b++)) *++st = c;  
   *++st = 0;
   return sz;
}

....

char a[] = "StringA"; 
printf("string-1 length = %d, String a = %s\n", str_join(&a[0],"StringB"), a);

Producción:

cadena-1 longitud = 7, char *a = CadenaA CadenaB

*** superación de pila detectada ****: /T02 terminado

Anulado (núcleo volcado)

no entiendo porque se muestra apilar rompiendo? ¿Y qué es *romper pilas? ¿O es un error de mi compilador?

  • Se detectó un posible duplicado de la destrucción de la pila

    – piyushj

    4 de noviembre de 2016 a las 6:24

Avatar de usuario de Sourav Ghosh
Sourav Ghosh

Bien, apilar rompiendo o desbordamiento de búfer de pila es un tema bastante detallado para ser discutido aquí, puede consultar este artículo de la wiki para más información.

En cuanto al código que se muestra aquí, el problema es que su matriz a no es lo suficientemente grande para contener el resultado concatenado final.

Por lo tanto, al decir

 while((c = *b++)) *++st = c;

esencialmente está accediendo a la memoria fuera de límite que invoca comportamiento indefinido. Esta es la razón por la que está teniendo el problema de “romper la pila” porque está tratando de acceder a la memoria que no pertenece a su proceso.

Para resolver esto, debe asegurarse de que la matriz a contiene suficiente espacio para contener tanto el primero como el segundo cadena concatenados juntos. En resumen, debe proporcionar una matriz de destino más grande.

avatar de usuario de mtraceur
mtraceur

La destrucción de la pila significa que ha escrito fuera del espacio de almacenamiento de la función para las variables locales (esta área se denomina “pila” en la mayoría de los sistemas y lenguajes de programación). También puede encontrar este tipo de error llamado “desbordamiento de pila” y/o “desbordamiento de pila”.

En su código, C probablemente esté colocando la cadena a la que apunta a en la pila En su caso, el lugar que causa que la pila se “aplaste” es cuando incrementa st más allá del original a puntero y escribe donde apunta, estás escribiendo fuera del área que el compilador de C garantiza haber reservado para la cadena original asignada en a.

Cada vez que escribe fuera de un área de memoria que ya está correctamente “reservada” en C, eso es un “comportamiento indefinido” (lo que simplemente significa que el lenguaje/estándar C no dice lo que sucede): por lo general, termina sobrescribiendo algo más en la memoria de su programa (los programas generalmente colocan otra información justo al lado de sus variables en la pila, como direcciones de retorno y otros detalles internos), o su programa intenta escribir fuera de la memoria que el sistema operativo le ha “permitido” usar. De cualquier manera, el programa normalmente falla, a veces de manera inmediata y evidente (por ejemplo, con un error de “falla de segmentación”), a veces de formas muy ocultas que no se vuelven obvias hasta mucho más tarde.

En este caso, su compilador está construyendo su programa con protecciones especiales para detectar este problema, por lo que sus programas salen con un mensaje de error. Si el compilador no hiciera eso, su programa intentaría continuar ejecutándose, excepto que podría terminar haciendo lo incorrecto y/o fallando.

La solución se reduce a la necesidad de decirle explícitamente a su código que tenga suficiente memoria para su cadena combinada. Puede hacer esto especificando explícitamente la longitud de la matriz “a” para que sea lo suficientemente larga para ambas cadenas, pero eso generalmente solo es suficiente para usos simples en los que sabe de antemano cuánto espacio necesita. Para una solución de propósito general, usaría una función como malloc para obtener un puntero a una nueva porción de memoria del sistema operativo que tenga el tamaño que necesita/desea una vez que haya calculado cuál será el tamaño completo (solo recuerde llamar free en los punteros que obtienes de malloc y funciones similares una vez que haya terminado con ellos).

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 ascendente.

Compilar y ejecutar:

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

falla como se desea:

*** stack smashing detected ***: terminated
Aborted (core dumped)

Probado en Ubuntu 20.04, GCC 10.2.0.

En Ubuntu 16.04, GCC 6.4.0, pude reproducir con -fstack-protector en lugar de -fstack-protector-allpero dejó de explotar cuando probé en GCC 10.2.0 según el comentario de Geng Jiawen. man gcc aclara que como sugiere el nombre de la opción, el -all La versión agrega verificaciones de manera más agresiva y, por lo tanto, presumiblemente incurre en una mayor pérdida de rendimiento:

-fstack-protector

Emita código adicional para comprobar si hay desbordamientos de búfer, como ataques de destrucción de pila. Esto se hace agregando una variable de protección a las funciones con objetos vulnerables. Esto incluye funciones que llaman a “alloca” y funciones con búfer mayores o iguales a 8 bytes. Las protecciones se inicializan cuando se ingresa a una función y luego se verifican cuando la función sale. Si falla una verificación de guardia, se imprime un mensaje de error y el programa sale. Solo se consideran las variables que están realmente asignadas en la pila, las variables optimizadas o las variables asignadas en los registros no cuentan.

-fstack-protector-todos

Como -fstack-protector excepto que todas las funciones están protegidas.

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 modulo 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 myfunc es exactamente lo que modifica la dirección del canario

El canario aleatorizó poniéndolo con %fs:0x28que contiene un valor aleatorio como se explica en:

¿Cómo depurarlo?

Ver: Superación de pila detectada

¿Ha sido útil esta solución?