0xnhl

Cosmic Rings

/ Update
7 min read

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 havok
bash

Key findings:

  • Binary is PIE, not stripped, has debug symbols.
  • Uses custom libc.so.6 and ld-linux-x86-64.so.2.
  • There is a function cosmic_release() that does system("/bin/sh").
  • Seccomp is installed in setup_seccomp().

The app flow:

  1. Two times ring calibration (calibrate_rings)
  2. Read plasma signature (read_plasma_signature, 256 bytes to global buffer)
  3. Injection stage (inject_plasma) with a vulnerable stack read

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 havok
bash
  • 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 havok
bash
  • 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_rings and inject_plasma.
nm -n havok
bash
  • 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 havok
bash
  • Full reverse of control flow and memory access.
  • Critical findings:
    • calibrate_rings: user index parsed with atoi, negative check on full int, then truncated into short; large positive values wrap negative and bypass intended bounds.
    • inject_plasma: read(0, confirm, 0x30) into confirm[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 for pop rdi; ret, pop rsi; ret, and helper gadgets to control rdx.
  • Impact: remote exploit must target this exact libc, not host libc.
./havok
bash
  • 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 65534 and 65535 in the two calibration passes.

    • 65534 wraps to -2 in short, leaking a libc pointer (resolved puts).
    • 65535 wraps 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; ret into plasma_sig.
  • Needed to avoid signature containing 0x0f\x05 bytes.
  • Needed ORW chain compatible with seccomp, using libc calls.
  • Needed robustness for remote fd/path variance (successful combo: ./flag.txt, fd 6).

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, -2 in short.
  • These pass <=3 and index a local stack array out-of-bounds:
    • -2 leaks a libc pointer (GOT-resolved puts address stored on stack)
    • -1 leaks main pointer (PIE code pointer)

So we can derive both bases:

  • libc_base = leak_puts - puts_offset
  • pie_base = leak_main - main_offset

B) Stack overflow in inject_plasma#

In inject_plasma:

char confirm[0x20];
read(0, confirm, 0x30);
c

So 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 resolved puts address
  • send index 65535 (short -> -1) -> leak main address

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 rbp with plasma_sig
  • overwrite saved RIP with leave; ret gadget in binary (inject_plasma epilogue)
  • leave; ret pivots stack into controlled global memory and starts ROP.

Stage 4: ORW chain#

ROP does:

  1. open("./flag.txt", O_RDONLY, 0)
  2. read(fd, buf, 0x40)
  3. 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:

  • main offset: 0x17e0
  • puts offset in libc: 0x82e00
  • plasma_sig global: 0x4060
  • flag_store global: 0x4280
  • leave; ret in binary: 0x1657
  • libc:
    • open: 0x10cb30
    • read: 0x10d310
    • write: 0x10dde0
    • pop rdi; ret: 0x10269a
    • pop rsi; ret: 0x53887
    • pop rax; ret: 0xd47d7
    • xchg rdx, rax; ret: 0xb29bd
    • ret: 0x10269b

(rdx is set via pop rax; ret + xchg rdx, rax; ret.)


5) Solver Script (Remote)#


6) Why This Works#

  • Integer truncation bug gives reliable libc+PIE leaks.
  • Stack overflow gives control of rbp + rip.
  • leave; ret pivots execution to global controlled memory.
  • ROP calls libc open/read/write directly, 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}

Cosmic Rings
https://nahil.xyz/vault/writeups/apoorvctf2026/be/cosmic-rings/
Author Nahil Rasheed
Published at March 24, 2026
Disclaimer This content is provided strictly for educational purposes only.