Protección de ejecutables: Stack Protector (Canaries)

Los canaries, stack guard o stack protector, son valores conocidos (security cookies) que se colocan entre un buffer y los datos de control en la pila para monitorizar los desbordamientos de buffer. Cuando el buffer se desborda, los primeros datos que se corromperán generalmente serán el security cookie y, por lo tanto, una verificación fallida de estos datos alertarán sobre un desbordamiento que luego se puede manejar, por ejemplo, invalidando los datos dañados o finalizando de forma controlada.

Para poder entender como funcionan los canaries o stack protector tenemos que conocer bien el funcionamiento del stack. (ver El Segmento Stack).

Cuando se llama a una función desde una aplicación, se realizan una serie de acciones en el stack que podríamos resumir como:

  • Guardar la dirección de retorno
  • Guardar el marco de la pila actual desde donde se realiza la llamada a función (rbp)
  • Asignar el espacio de las variables locales

Vamos a ver el proceso usando una aplicación que llama a una función con un parámetro, un puntero a string. La función asigna una variable de un tamaño fijo asignado y luego copia el contenido del parámetro en la variable:

#include <string.h> 
#include <stdio.h> 
 
void fun_copy(char *str) { 
    char buffer[16]; 
    strcpy(buffer, str); 
} 
 
int main (int argc, char *argv[]) { 
    if(argc == 2) 
        fun_copy(argv[1]); 
    return 0; 
} 

Vamos a compilar el código primero sin la protección stack protector activada. Para evitar que el compilador utilice el protector de stack conocido como canaries, que añade datos de control en el stack para monitorizar los desbordamientos de buffer, tenemos que compilar el programa con la opción -fno-stack-protector:

./sinCanaries 123456

La parte que nos interesa es la función fun_copy. La llamada a la función se realiza desde main con callq. Lo que hace callq es guardar en la pila la dirección de retorno de main donde debe continuar la aplicación cuando finalice la rutina fun_copy(). La dirección a guardar en este caso es 0x0000555555554694 que es justo la dirección siguiente a la llamada callq.

Ahora se ejecuta el código de la función fun_copy():

Dump of assembler code for function fun_copy:
0x000055555555464a <+0>: push %rbp
0x000055555555464b <+1>: mov %rsp,%rbp
0x000055555555464e <+4>: sub $0x20,%rsp
0x0000555555554652 <+8>: mov %rdi,-0x18(%rbp)
0x0000555555554656 <+12>: mov -0x18(%rbp),%rdx
0x000055555555465a <+16>: lea -0x10(%rbp),%rax
0x000055555555465e <+20>: mov %rdx,%rsi
0x0000555555554661 <+23>: mov %rax,%rdi
0x0000555555554664 <+26>: callq 0x555555554520 <strcpy@plt>
0x0000555555554669 <+31>: nop
0x000055555555466a <+32>: leaveq
0x000055555555466b <+33>: retq
End of assembler dump.

push %rbp : Lo primero guarda en el stack el registro del anterior rbp, es decir el comienzo del marco de main en este caso:

1: $rsp = (void *) 0x7fffffffdd40
2: $rbp = (void *) 0x7fffffffdd60

mov %rsp,%rbp : Copia rsp a rbp:

1: $rsp = (void *) 0x7fffffffdd40
2: $rbp = (void *) 0x7fffffffdd40

sub $0x20,%rsp : añade espacio en el stack para las variables locales: 8 bytes para el registro rbp que ya hemos guardado, 8 bytes para el puntero str y 16 bytes para el buffer = 32 bytes (0x20 en hexadecimal)

1: $rsp = (void *) 0x7fffffffdd20
2: $rbp = (void *) 0x7fffffffdd40

mov %rdi,-0x18(%rbp) : Guardamos el valor que pasamos a la función, el puntero str, en el stack (el primer argumento de una función reside en el registro %rdi). Como vemos la dirección donde copiamos es rbp (que usamos como puntero base) menos 0x18.

Ahora la aplicación se prepara para llamar a strcpy. Esta función necesita dos argumentos: el origen y el destino de la copia en los registros %rdi y %rsi respectivamente. Primero copia el origen que es el puntero str a rdx: mov -0x18(%rbp),%rdx. Luego el destino que es buffer a rax: lea -0x10(%rbp),%rax. En ambos casos utiliza nuevamente el puntero rbp como base. Finalmente pasa estos valors a rdi y rsi: mov %rdx,%rsi y mov %rax,%rdi antes de llamar a la función: callq 0x555555554520 <strcpy@plt>.

Es en este punto donde podemos tener problemas de desbordamiento. Si la variable que le pasamos a la función es más grande que el espacio asignado para la variable local buffer[16] (16 bytes) sobrescribiremos hacia arriba (según esta representado en las imágenes), pudiendo sobrescribir el rbp guardado y la dirección de retorno.

Primer caso, sin stack protector activado y valor pasado menor a la variable de fun_copy:

Al regreso de strcpy, si todo fue correctamente tendremos copiado el valor de str '123456' en buffer ( rbp - 0x10 ).

La función esta preparada para regresar, así que utiliza leaveq para restaurar los valores de main para %rsp y %rbp. Esto es exactamente equivalente a primero copiar rbp a rsp (mov %rbp, %rsp):

1: $rsp = (void *) 0x7fffffffdd40
2: $rbp = (void *) 0x7fffffffdd40

Luego extraer del stack el anterior rbp (pop %rbp):

1: $rsp = (void *) 0x7fffffffdd48
2: $rbp = (void *) 0x7fffffffdd60

Para regresar, con retq saca la dirección de retorno del stack haciendo pop en el registro rip (instruction pointer) y salta a ella para volver a main.

Segundo caso, sin stack protector activado y valor pasado mayor a la variable de fun_copy:

./sinCanaries AAAAAAAAAAAAAAAAAAAAAAAAA

Al regreso de strcpy tendremos copiado el valor de str 'AAAAAAAAAAAAAAAAAAAAAAAAA' en buffer ( rbp - 0x10 ), pero como el tamaño de la cadena sobrepasa al de la variable buffer también sobreescribimos rbp y el valor de retorno.

La función esta preparada para regresar, así que utiliza leaveq para restaurar los valores de main para %rsp y %rbp. Esto es exactamente equivalente a primero copiar rbp a rsp (mov %rbp, %rsp):

1: $rsp = (void *) 0x7fffffffdd40
2: $rbp = (void *) 0x7fffffffdd40

Luego extraer del stack el anterior rbp (pop %rbp):

1: $rsp = (void *) 0x7fffffffdd48
2: $rbp = (void *) 0x4141414141414141

Podemos ver que el valor de rbp es una dirección 0x4141414141414141 que no es otra cosa que muchas A (0x41 en hexadecimal). Para regresar, con retq saca la dirección de retorno del stack haciendo pop en el registro rip (instruction pointer) y salta a ella para volver a main. En este caso intenta saltar a la dirección 0x4141414141414141 que se a sobrescrito como dirección de retorno. Esto produce una violación de sergmento:

Violación de segmento (`core' generado)

Para explotar este problema se calcula el tamaño de la cadena que se va a grabar en buffer de tal manera que conozcamos el valor con el que vamos a sobrescribir la dirección de retorno, de tal forma que apunte a una zona de memoria donde tendremos nuestro shellcode preparado para ser ejecutado.

Tercer caso, con stack protector activado y valor pasado mayor a la variable de fun_copy:

Compilamos ahora el programa con stack protector activado (por defecto gcc activa stack protector).

Podemos verificar que tenemos activado el stack protector con readef:

readelf -s conCanaries |grep '__stack_chk_fail'
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3)
49: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@@GLIBC_2

La función fun_copy incluye la llamada a __stack_chk_fail para comprobar el valor del security cookie:

Dump of assembler code for function fun_copy:
0x00005555555546aa <+0>: push %rbp
0x00005555555546ab <+1>: mov %rsp,%rbp
0x00005555555546ae <+4>: sub $0x30,%rsp
0x00005555555546b2 <+8>: mov %rdi,-0x28(%rbp)
0x00005555555546b6 <+12>: mov %fs:0x28,%rax
0x00005555555546bf <+21>: mov %rax,-0x8(%rbp)
0x00005555555546c3 <+25>: xor %eax,%eax
0x00005555555546c5 <+27>: mov -0x28(%rbp),%rdx
0x00005555555546c9 <+31>: lea -0x20(%rbp),%rax
0x00005555555546cd <+35>: mov %rdx,%rsi
0x00005555555546d0 <+38>: mov %rax,%rdi
0x00005555555546d3 <+41>: callq 0x555555554570 <strcpy@plt>
0x00005555555546d8 <+46>: nop
0x00005555555546d9 <+47>: mov -0x8(%rbp),%rax
0x00005555555546dd <+51>: xor %fs:0x28,%rax
0x00005555555546e6 <+60>: je 0x5555555546ed <fun_copy+67>
0x00005555555546e8 <+62>: callq 0x555555554580 <__stack_chk_fail@plt>
0x00005555555546ed <+67>: leaveq
0x00005555555546ee <+68>: retq
End of assembler dump.

Lanzamos el programa:

./conCanaries AAAAAAAAAAAAAAAAAAAAAAAAA

Al tener activado el stack protector, el marco de nuestra función fun_copy a cambiado ligeramente. Un security cookie generado dinamicamente se ha añadido en el stack antes de las variables locales.

Al regreso de strcpy tendremos copiado el valor de str 'AAAAAAAAAAAAAAAAAAAAAAAAA' en buffer, pero como el tamaño de la cadena sobrepasa al de la variable buffer también sobrescribimos rbp y el valor de retorno. El security cookie que se encuentra antes que el rbp y la dirección de retorno también ha sido sobrescrito.

La función esta preparada para regresar, pero antes del epilogo clásico de las funciones tenemos en este caso el código de comprobación del stack protector. Se saca de la pila el valor del security cookie y se comprueba a ver si ha sido modificado. Al comprobar que se ha sobrescrito se interrumpe el funcionamiento del programa, nos avisa del problema detectado y aborta la ejecución del programa:

*** stack smashing detected ***: <unknown> terminated
Abortado (`core' generado)

Opciones de GCC para el stack protector

Activar el protector del stack en muchas funciones puede provocar una degradación del rendimiento. Es por eso que hay varias formas de decirle a GCC que queremos activar el protector del stack:

  • -fstack-protector: comprueba si la pila se rompe en funciones con objetos vulnerables. Esto incluye funciones con buffers de más de 8 bytes o llamadas a alloca
  • -fstack-protector-strong: igual que -fstack-protector, pero también incluye funciones con matrices locales o referencias a direcciones de marcos locales
  • -fstack-protector-all: comprueba si la pila se rompe en cada función
  • -fstack-protector-explicit: igual que -fstack-protector pero solo protege las funciones que tienen el atributo stack_protect

 

Modificado por última vez enMiércoles, 29 Julio 2020 10:36
(0 votos)
Etiquetado como :

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.