¿Cómo se registran las contaminaciones canarias de pila?

11

La bandera GCC -fstack-protector flag permite el uso de canarios de pila para la protección de desbordamiento de pila. El uso de esta bandera por defecto ha sido más destacado en los últimos años.

Si un paquete se compila con -fstack-protector, y desbordamos un búfer en el programa, es probable que recibamos un error como:

*** buffer overflow detected ***: /xxx/xxx terminated

Sin embargo, ¿"quién" está a cargo de estos mensajes de error? ¿Dónde se registran estos mensajes? ¿El demonio syslog selecciona estos mensajes?

aedcv
fuente

Respuestas:

10

Stack Smashing es detectado por libssp, que es parte de gcc. Se esfuerza mucho para enviar el mensaje a un terminal, y solo si eso falla se registra en el registro del sistema, por lo que en la práctica verá mensajes de desbordamiento de búfer en los registros de demonios y tal vez aplicaciones GUI.

Una vez que muestra su mensaje, libsspintenta salir de varias formas, incluido el bloqueo de la aplicación; esto podría ser detectado por uno de los registradores de salida anormales, pero eso no está garantizado.

Stephen Kitt
fuente
1
Permítanme presentar un ejemplo concreto como una forma de explorar más esta explicación. Vamos a elegir nginx para este ejemplo. He compilado nginx con canarios de pila. Cuando ejecuto nginx, inicia un proceso pero no genera nada en el shell. En cambio, cualquier mensaje se registra en sus múltiples archivos de registro. Si nginx detecta el aplastamiento de la pila, libsspenviará su mensaje por la salida stderr utilizada por nginx. Entonces, libssppodría intentar salir del proceso (o proceso hijo para nginx). Si "no necesita" bloquear la aplicación, los registradores de salida anormales no lo detectarán. ¿Es esta una interpretación correcta?
aedcv
No del todo - que va a tratar de bloquear la aplicación, utilizando __builtin_trap()primero, y luego, si esto falla, tratando de provocar una violación de segmento, y sólo si eso no funciona, salir con el estado 127.
Stephen Kitt
La parte de impresión de mensajes no tiene mejores garantías de éxito que la salida a través de un método de rendimiento central (por ejemplo abort()).
maxschlepzig
7

Las distribuciones modernas de Linux como CentOS / Fedora configuran un daemon de manejo de fallas (por ejemplo, systemd-coredumpor abortd), de manera predeterminada.

Por lo tanto, cuando su programa finaliza de manera anormal (por defecto, excepción no detectada, aborto, instrucción ilegal, etc.), este evento es registrado y registrado por ese demonio. Por lo tanto, encontrará algunos mensajes en el diario del sistema y posiblemente una referencia a un directorio con algunos detalles adicionales (por ejemplo, archivo central, registros, etc.).

Ejemplo

$ cat test_stack_protector.c 
#include <string.h>

int f(const char *q)
{
  char s[10];
  strcpy(s, q);
  return s[0] + s[1];
}

int main(int argc, char **argv)
{
  return f(argv[1]);
}

Compilar:

$ gcc -Wall -fstack-protector test_stack_protector.c -o test_stack_protector

Ejecutar:

$ ./test_stack_protector 'hello world'
*** stack smashing detected ***: ./test_stack_protector terminated
======= Backtrace: =========
/lib64/libc.so.6(+0x7c8dc)[0x7f885b4388dc]
/lib64/libc.so.6(__fortify_fail+0x37)[0x7f885b4dfaa7]
/lib64/libc.so.6(__fortify_fail+0x0)[0x7f885b4dfa70]
./test_stack_protector[0x400599]
./test_stack_protector[0x4005bd]
/lib64/libc.so.6(__libc_start_main+0xea)[0x7f885b3dc50a]
./test_stack_protector[0x40049a]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:28 1151979                            /home/juser/program/stackprotect/test_stack_protector
00600000-00601000 r--p 00000000 00:28 1151979                            /home/juser/program/stackprotect/test_stack_protector
00601000-00602000 rw-p 00001000 00:28 1151979                            /home/juser/program/stackprotect/test_stack_protector
0067c000-0069d000 rw-p 00000000 00:00 0                                  [heap]
7f885b1a5000-7f885b1bb000 r-xp 00000000 00:28 1052100                    /usr/lib64/libgcc_s-7-20170915.so.1
7f885b1bb000-7f885b3ba000 ---p 00016000 00:28 1052100                    /usr/lib64/libgcc_s-7-20170915.so.1
7f885b3ba000-7f885b3bb000 r--p 00015000 00:28 1052100                    /usr/lib64/libgcc_s-7-20170915.so.1
7f885b3bb000-7f885b3bc000 rw-p 00016000 00:28 1052100                    /usr/lib64/libgcc_s-7-20170915.so.1
7f885b3bc000-7f885b583000 r-xp 00000000 00:28 945348                     /usr/lib64/libc-2.25.so
7f885b583000-7f885b783000 ---p 001c7000 00:28 945348                     /usr/lib64/libc-2.25.so
7f885b783000-7f885b787000 r--p 001c7000 00:28 945348                     /usr/lib64/libc-2.25.so
7f885b787000-7f885b789000 rw-p 001cb000 00:28 945348                     /usr/lib64/libc-2.25.so
7f885b789000-7f885b78d000 rw-p 00000000 00:00 0 
7f885b78d000-7f885b7b4000 r-xp 00000000 00:28 945341                     /usr/lib64/ld-2.25.so
7f885b978000-7f885b97b000 rw-p 00000000 00:00 0 
7f885b9b0000-7f885b9b3000 rw-p 00000000 00:00 0 
7f885b9b3000-7f885b9b4000 r--p 00026000 00:28 945341                     /usr/lib64/ld-2.25.so
7f885b9b4000-7f885b9b6000 rw-p 00027000 00:28 945341                     /usr/lib64/ld-2.25.so
7ffc59966000-7ffc59987000 rw-p 00000000 00:00 0                          [stack]
7ffc5999c000-7ffc5999f000 r--p 00000000 00:00 0                          [vvar]
7ffc5999f000-7ffc599a1000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
zsh: abort (core dumped)  ./test_stack_protector 'hello world'

El estado de salida es 134, que es 128 + 6, es decir, 128 más el número de señal de cancelación.

El diario del sistema:

Oct 16 20:57:59 example.org audit[17645]: ANOM_ABEND auid=1000 uid=1000 gid=1000 ses=3 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 pid=17645 comm="test_stack_prot" exe="/home/juser/program/stackprotect/test_stack_protector" sig=6 res=1
Oct 16 20:57:59 example.org systemd[1]: Started Process Core Dump (PID 17646/UID 0).
Oct 16 20:57:59 example.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='unit=systemd-coredump@21-17646-0 comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
Oct 16 20:57:59 example.org systemd-coredump[17647]: Process 17645 (test_stack_prot) of user 1000 dumped core.

                           Stack trace of thread 17645:
                           #0  0x00007f885b3f269b raise (libc.so.6)
                           #1  0x00007f885b3f44a0 abort (libc.so.6)
                           #2  0x00007f885b4388e1 __libc_message (libc.so.6)
                           #3  0x00007f885b4dfaa7 __fortify_fail (libc.so.6)
                           #4  0x00007f885b4dfa70 __stack_chk_fail (libc.so.6)
                           #5  0x0000000000400599 f (test_stack_protector)
                           #6  0x00000000004005bd main (test_stack_protector)
                           #7  0x00007f885b3dc50a __libc_start_main (libc.so.6)
                           #8  0x000000000040049a _start (test_stack_protector)
Oct 16 20:57:59 example.org audit[1]: SERVICE_STOP pid=1 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='unit=systemd-coredump@21-17646-0 comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
Oct 16 20:58:00 example.org abrt-notification[17696]: Process 17645 (test_stack_protector) crashed in __fortify_fail()

Eso significa que obtiene el registro del auditddemonio de auditoría y el systemd-coredumpcontrolador de fallas.

Para verificar si un demonio de manejo de fallas está configurado, puede verificar /proc, por ejemplo:

$ cat /proc/sys/kernel/core_pattern
|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %e

(todo probado en Fedora 26, x86-64)

maxschlepzig
fuente
1
Estoy muy contento de que hayas publicado este ejemplo. Los canarios se ponen en su lugar por gcc. (Corríjame si me equivoco) Supongo que lo que sucede es algo como: gcc pone "código extra" en el programa para implementar la funcionalidad canaria; durante la ejecución y antes de que regrese una función, se verifica el valor; si está contaminado, el programa mostrará el mensaje "aplastamiento de pila detectado" y generará un error. Este error es detectado por el sistema operativo, reconoce un error de segmentación e imprime la traza inversa y el mapa de memoria que publicó. Por último, el sistema operativo mata la aplicación, genera un volcado de núcleo y registra en el diario sys
aedcv
@aedcv, esta es más o menos la historia, para ser más precisos: la pila rompe las llamadas de código de verificación abort()que produce una señal de aborto, es decir, no hay una falla de segmentación. Es solo que los manejadores de señal predeterminados para la falla de aborto / segmentación, etc. producen la misma acción: escribir núcleo y salir del proceso con un estado de salida de cero desigual que también codifica el número de señal. El núcleo realiza la escritura y su comportamiento es configurable a través de /proc/.../core_pattern. En el ejemplo anterior, se configura un asistente de espacio de usuario y, por lo tanto, se lo llama. El núcleo también desencadena la auditoría.
maxschlepzig
@maxschlepzig no es del todo abort(), el código SSP usa __builtin_trap()(pero el efecto es el mismo).
Stephen Kitt
1
@StephenKitt, bueno, eche un vistazo a la traza de la pila en el ejemplo anterior. Ahí ves claramente cómo abort()se llama.
maxschlepzig
1
@maxschlepzig sí, por supuesto, pero ese es un detalle de implementación (el código GCC se usa __builtin_trap()para evitar tener una dependencia explícita abort()). Otras distribuciones tienen diferentes trazas de pila.
Stephen Kitt