CSSCTF REVERSE ENGINEERING CHALLENGES WRITEUP#
1. Challenge 1: Lamp Drill (Warm-up)#
1.1. Challenge Information#
- Challenge Name: Lamp Drill
- Category: Reverse Engineering (Warm-up)
- Provided Files:
lampDrill.png,lampDrill.svg - Description:
Warm-up. No spaces.
Flag Format:CSSCTF{...}
1.2. Detailed Analysis#
Opening the image lampDrill.png or inspecting the vector structures inside lampDrill.svg reveals two distinct visual sections:
1.2.1. Logic Rule Table (Upper Section)#
There are four transformation rules governing the lamp states (solid black circle ● and hollow white circle ○):
● ● -> ●● ○ -> ○○ ● -> ○○ ○ -> ○
In digital logic circuit design:
●(black circle / lamp ON) represents logic high: $1$ (True).○(white circle / lamp OFF) represents logic low: $0$ (False).
This lookup table represents the truth table of an AND logic gate:
$$\text{Output} = A \land B$$The output evaluates to $1$ if and only if both input operands $A$ and $B$ are $1$.
1.2.2. Logic Box Grid (Lower Section)#
The main grid consists of 3 rows, each containing 8 rectangular boxes connected sequentially from left to right with arrows ($\rightarrow$).
- The 8 boxes in each row represent 8 bits (1 ASCII byte), evaluated from the Most Significant Bit (MSB) on the left to the Least Significant Bit (LSB) on the right.
- Each box encloses a pair of lamp circles $[A, B]$. By evaluating the AND logic operation on each pair within a box, we determine the value of the corresponding bit.
1.2.2.1. Row 1#
- Box 0:
[●, ○]$\rightarrow 1 \land 0 = \mathbf{0}$ - Box 1:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 2:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 3:
[○, ●]$\rightarrow 0 \land 1 = \mathbf{0}$ - Box 4:
[○, ○]$\rightarrow 0 \land 0 = \mathbf{0}$ - Box 5:
[●, ○]$\rightarrow 1 \land 0 = \mathbf{0}$ - Box 6:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 7:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$
$\Rightarrow$ Binary sequence: 01100011$_2 = 0\text{x}63 = 99 = \mathbf{'c'}$
1.2.2.2. Row 2#
- Box 0:
[○, ●]$\rightarrow 0 \land 1 = \mathbf{0}$ - Box 1:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 2:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 3:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 4:
[●, ○]$\rightarrow 1 \land 0 = \mathbf{0}$ - Box 5:
[○, ●]$\rightarrow 0 \land 1 = \mathbf{0}$ - Box 6:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 7:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$
$\Rightarrow$ Binary sequence: 01110011$_2 = 0\text{x}73 = 115 = \mathbf{'s'}$
1.2.2.3. Row 3#
- Box 0:
[○, ○]$\rightarrow 0 \land 0 = \mathbf{0}$ - Box 1:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 2:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 3:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 4:
[●, ○]$\rightarrow 1 \land 0 = \mathbf{0}$ - Box 5:
[○, ●]$\rightarrow 0 \land 1 = \mathbf{0}$ - Box 6:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$ - Box 7:
[●, ●]$\rightarrow 1 \land 1 = \mathbf{1}$
$\Rightarrow$ Binary sequence: 01110011$_2 = 0\text{x}73 = 115 = \mathbf{'s'}$
Concatenating the 3 decoded characters: 'c', 's', 's' $\rightarrow$ "css".
1.3. Automated Decoder Script (Python)#
The script below parses lampDrill.svg directly, extracts bounding box coordinates for each container and circle, groups them by row, and calculates the resulting ASCII characters:
| |
1.4. Result & Flag#
$$\mathbf{CSSCTF\{css\}}$$2. Challenge 2: Silicon Snare (Hardware RE / SAT Solving)#
2.1. Challenge Information#
- Challenge Name: Silicon Snare
- Category: Reverse Engineering (Hardware / Circuit SAT)
- Provided Files:
schematic.png,schematic.svg - Description:
The Kuiper Belt relay fail-safe is a hardwired optical routing matrix.
Thanks to the bravery of a specific R2-series astromech droid, you have been provided the original schematic for its layout and proprietary analogue logic nodes. Trace the signal paths through the gates and find the one 32-bit input pattern that drives the centre node, Override, to logic high (1).Software won’t help you much here. The only thing to follow is the wire.
Some wires will intentionally overlap to hinder your efforts. Thankfully, you have its SVG; it will let you zoom in infinitely to inspect for detail. Push on!
SubmitCSSCTF{…}.Inside the braces, enter 32 bits (32 characters, 1s and 0s) with no spaces. I00, at 12 o’clock, is the first bit. Read clockwise through I31.
This challenge is inspired by the same work bare-metal hardware engineers conduct when reverse engineering proprietary dies.
2.2. Circuit Architecture Analysis#
The challenge simulates hardware die reverse engineering (silicon decap / hardware reverse engineering). The circuit forms a concentric optical routing matrix:
- 32 Primary Inputs: Labeled from
I00toI31, arranged along the outer ring in clockwise order, starting withI00at the 12 o’clock position. - Central Control Destination (
OVERRIDE): A large circle situated at the center of the circuit ($x \approx 419.08, y \approx 396.31$). The objective is to identify the 32-bit input vector that drives this node to logic high ($1$). - Logic Gates: A total of 74 gates categorized into 4 proprietary gate models:
- Flow gate (
f00tof13- 20 gates):
$Y = A \land B$ (Standard AND gate). - Merge gate (
m00tom22- 35 gates):
$Y = A \oplus B$ (Standard XOR gate). - Negate gate (
n00ton05- 6 gates):
$Y = \neg A$ (NOT gate / Inverter). - Shift gate (
s00tos0c- 13 gates):
A custom signal routing gate:- Pointed end: Input $A$, Output $Y_0 = A \lor B$.
- Flat end (marked with a heavy bar
|): Input $B$, Output $Y_1 = A \land \neg B$.
- Flow gate (
2.3. Wire Untangling Technique#
The author intentionally laid out overlapping, criss-crossing traces to thwart manual visual tracking. However, analyzing the SVG document structure exposes:
- Signal Net Routing Principles:
- Challenge description: “A node is where one wire splits. It is drawn as a filled dot: one wire arrives, and two or more leave. The dot is that wire’s colour, and every branch after the node carries the same signal.”
- By inspecting
<path>elements in the SVG, all colored wires are drawn using exactly 100 unique hex stroke colors. Each stroke color corresponds to exactly one signal net. A signal emitted from a source never changes color along its transmission line.
- Primary Input Mapping:
- In the SVG, the 32 small dots corresponding to
I00..I31(frompatch_111topatch_142) carry the exact first 32 hex colors (#9e1010,#3861c7,#7cc714, …,#c7c038).
- In the SVG, the 32 small dots corresponding to
- Negate Gates (Inverters):
- Six Negate gates (
n00ton05) are inserted directly onto traces to invert the signal without altering trace color. Each Negate gate divides a trace of identical color into two segments: the segment before the gate (larger radius from center) and the segment after the gate (smaller radius towards center), with the relation: $$\text{wire\_after} = \neg (\text{wire\_before})$$
- Six Negate gates (
- Shift Gate Analysis (
s00tos0c):- Inspecting physical pin connections on all 13 Shift gates reveals that the output pin $Y_0$ at the pointed end terminates with a terminator bar (no wire connected). Only output pin $Y_1$ at the flat end feeds into downstream stages: $$Y = A \land \neg B$$ Here, input $B$ enters adjacent to the heavy bar at the flat end, while input $A$ enters at the pointed end.
- Destination Node
OVERRIDE:- The final stage driving directly into
OVERRIDEis gatef13(Flow gate / AND): $$\text{OVERRIDE} = f_{13} = f_{11} \land f_{12} = 1 \iff f_{11} = 1 \text{ and } f_{12} = 1$$
- The final stage driving directly into
2.4. Boolean Modeling and Solving with Z3#
Because the circuit forms a Directed Acyclic Graph (DAG), the entire network corresponds to a 32-variable Boolean equation system. We automatically extract the entire netlist from the SVG file and submit it to the Z3 SAT Solver.
2.4.1. Complete Solver Script (solve_snare_final.py)#
| |
2.5. Solving Results#
Executing the script yields the SAT assignment:
| |
Evaluating the four 8-bit blocks against ASCII:
- Byte 1 ($I_{00} \to I_{07}$):
01010000$_2 = 0\text{x}50 = 80 = \mathbf{'P'}$ - Byte 2 ($I_{08} \to I_{15}$):
01000001$_2 = 0\text{x}41 = 65 = \mathbf{'A'}$ - Byte 3 ($I_{16} \to I_{23}$):
01010011$_2 = 0\text{x}53 = 83 = \mathbf{'S'}$ - Byte 4 ($I_{24} \to I_{31}$):
01010011$_2 = 0\text{x}53 = 83 = \mathbf{'S'}$
Combining into the bypass keyword: PASS (the override fail-safe bypass passphrase).
2.6. Flag#
$$\mathbf{CSSCTF\{01010000010000010101001101010011\}}$$3. Challenge 3: PRINCE WALK (RE / “PINCE Int32 memory-editing practice”)#
Flag:
CSSCTF{P12INC3_0R_P1NC3?}Core Concept: The binary is a TUI game (an infinite procedurally generated world). The player spawns at
(1,1), while the target beacon landmark is at(999999, 999999). Walking to the target is impossible/penalized — you must attach a debugger (PINCE/GDB) and modify the twoint32coordinate variables X/Y in RAM. Once the coordinates equal(999999, 999999)and the player presses1or2to “Reply”, the game triggersgen_flag()and displays the flag on screen.The flag can also be recovered offline by reimplementing
gen_flag()(an 8320-iteration state machine driven by an 8320-entry lookup table in.rodata) — without running the game or interacting with the terminal.
3.1. TL;DR#
| |
3.2. File Information & Security Mitigations#
| |
| Property | Value | Significance |
|---|---|---|
| Type | DYN (PIE) | Addresses require base offset (retrieved from /proc/<pid>/maps) |
| Symbols | stripped | Function names stripped -> identify functions by address |
| Canary | Present (fs:0x28 in all major functions) | Standard compiler stack protection; no stack exploitation needed |
| NX / RELRO | Full NX, Partial RELRO | Irrelevant: no memory corruption vulnerability exists |
| Vulnerability | None | Challenge is RE + memory editing, not pwn |
| Build | GCC, BuildID PIE | Modern Ubuntu, glibc 2.34+ |
Section Layout (crucial because critical data tables reside here):
| Section | Vaddr | File offset | Size | Notes |
|---|---|---|---|---|
.text | 0x1340 | 0x1340 | 0x20e2 | Executable code (~8KB) |
.rodata | 0x4000 | 0x4000 | 0x10e80 | Strings + 8320-entry lookup table |
.eh_frame_hdr | 0x14e80 | 0x14e80 | 0x104 | Exception handling frames |
.data.rel.ro | 0x16ba0 | 0x15ba0 | 0x170 | Landmark table + achievement table |
.data | 0x17000 | 0x16000 | 0x18 | Player coordinates X, Y |
.bss | 0x17020 | — | 0xa0 | stdout, stderr, termios, signal flags |
Note:
.rodata(vaddr 0x4000=offset 0x4000), but.data.rel.ro(vaddr 0x16ba0=offset 0x15ba0) — an offset delta of0x1000. When parsing with Python orxxd, file offsets must be adjusted accordingly, otherwisestruct.erroror invalid data will occur.
3.3. Recon via strings — Disclosing Challenge Mechanics#
| |
Two decisive clues reveal the solution path:
A terminal sandbox for PINCE Int32 memory-editing practice.-> Modifyint32variables in RAM using PINCE (PINCE is a Linux memory editing frontend similar to Cheat Engine).Attach opt-in unavailable; OS debugger rules apply.-> The game explicitly enables debugger attachment viaprctl(PR_SET_PTRACER, -1)(see Section 5).
In the tail of strings, an extensive block of pseudorandom-looking data (mH>36, r_?q?, ,8/b_9>, …) appears at the end of .rodata — this is the lookup table utilized by the flag generator, rather than an encrypted string.
3.4. Function Map (Named by Behavioral Analysis)#
Locating main from _start (lea rdi,[rip+0x7ef] # 1b4e -> first argument to __libc_start_main):
| Address | Temporary Name | Functionality |
|---|---|---|
0x1429 | now_sec() | clock_gettime(CLOCK_MONOTONIC) -> double (used for 2.5s notification timer) |
0x1495 | puts_line() | printf("\x1b[2K%s\n", s) — clear line + print |
0x14c3 | render() | Render frame: map / landmark dialog / achievements screen / HUD |
0x1b4e | main() | Game loop, keyboard input handling, landmark_lookup |
0x2144 | mix64() | splitmix64 finalizer (multiplies 0xBF58476D1CE4E5B9, 0x94D049BB133111EB) |
0x21a2 | world_get(x,y) | Procedural terrain generation based on coordinates (hash) + landmark/player/target overrides |
0x22d0 | is_walkable(x,y) | world_get(x,y) != 'T' |
0x22ff | clamp_add(a,b) | Addition clamped to [INT32_MIN, INT32_MAX] (prevents integer overflow) |
0x234e | key_to_delta(key) | W=+Y, S=-Y, A=-X, D=+X (case-insensitive) |
0x2431 / 0x252b | tty_raw() / tty_restore() | tcsetattr terminal mode configuration |
0x25b1 | tty_setup() | Validates isatty(0/1), TERM != dumb, registers signal handlers |
0x27e7 | get_winsize() | ioctl(TIOCGWINSZ); warns if terminal is smaller than 58x24 |
0x2880 / 0x28ae | clear() / cursor() | \x1b[2K, \x1b[?25l / \x1b[?25h terminal escape sequences |
0x28eb | read_key(timeout_ms) | poll() + read(0,...,1) input reader |
0x29ca | landmark_lookup(x,y,dirty,cnt) | Returns landmark record at (x,y) — implements anti-cheat logic |
0x2a86 | nag_message(cnt) | Input milestones (10/50/100/500/1000 inputs) -> developer nagging messages |
0x2b0c | unlock_ach(arr,rec) | arr[rec->ach] = 1; if ach == 9, also unlocks arr[0] |
0x2b4c | walk_ach(arr,cnt) | If cnt > 99 -> unlock achievement #8 |
0x2b6e | sum10(arr) | Sum 10 bytes -> ACHIEVEMENTS %u / 10 |
0x2baa | ach_name(i) | Achievement name (displays "???" if locked) |
0x2bf8 | rol32(v,n) | 32-bit left rotate, n & 31 |
0x2c18 | mix32(v) | v^=v>>16; v*=0x7feb352d; v^=v>>15; v*=0x846ca68b; v^=v>>16 |
0x2c52 | gen_flag(x,y,out,cap) | Flag generator function — core problem logic |
3.5. Runtime Mechanics (Identifying Target Variables)#
3.5.1. Global Variables Governing Game State#
.data (0x17000, 24 bytes) contains only three items:
| |
| Address | Type | Initial Value | Meaning |
|---|---|---|---|
base+0x17008 | void* | base+0x17008 | Self-referencing pointer (destructor registration) |
base+0x17010 | int32 | 1 | PLAYER X |
base+0x17014 | int32 | 1 | PLAYER Y |
Inside main:
| |
And upon each WASD keystroke (after obstacle verification):
| |
-> Modifying the two int32 values at base+0x17010 and base+0x17014 instantly teleports the player anywhere. The game maintains no encryption, hashing, or checksum protection on these variables.
3.5.2. Procedural World Generation — world_get(x,y) @ 0x21a2#
| |
- Magic constant
mul 0xAAAAAAAAAAAAAAAB+shr 3=>h*3,shl 2=>h*12, subtracted =>h % 12. - The table
"......,,TT~#"@0x4450provides: 6 grass, 2 dirt, 2 trees, 1 water, 1 rock => 1/6 of all tiles are impassable trees (T). - The player position is overlaid by
render()with'O'(whendx==0 && dy==0), while the target is'*'.
3.5.3. Landmark Table (.data.rel.ro @ 0x16ba0) — Where the Flag Resides#
Each record occupies 0x20 bytes. There are 2 special records (hardcoded for the beacon coordinates) and 7 standard coordinate records:
| # | Coordinates | Title | hidden | ach |
|---|---|---|---|---|
special 0x16ba0 | (999999, 999999) | THE END OF THE WORLD | 1 <- invokes gen_flag() | 0 The End of the World |
special 0x16bc0 | (999999, 999999) | YOU... ACTUALLY... WALKED HERE? | 0 | 9 PINCE Tutorial Failed Successfully |
| 0 | (69, 420) | AN ANCIENT MONUMENT (HONK) | 0 | 1 Nice. |
| 1 | (404, 404) | ERROR: LANDMARK NOT FOUND | 0 | 2 404: Achievement Not Found |
| 2 | (1337, 1337) | A MYSTERIOUS HACKER APPEARS (“how to modify an int32”) | 0 | 3 1337 |
| 3 | (0, 0) | THE ORIGIN | 0 | 4 The Origin |
| 4 | (-1, -1) | WELCOME TO NEGATIVE LAND | 0 | 5 Welcome to Negative Land |
| 5 | (999999, 999998) | A WOODEN SIGN STANDS HERE (“Almost there.”) | 0 | 6 Almost There |
| 6 | (9999999, 9999999) | THE REAL END OF THE WORLD | 0 | 7 The Real End of the World |
Memory layout parsed from file (accounting for vaddr - 0x1000 file offset):
- Standard records:
+0x00 x(int32),+0x04 y(int32),+0x08 title(char*),+0x10 desc(char*),+0x18 zero,+0x1c ach(int32);landmark_lookupreturnsbase+0x08(pointing to title). - Special records:
+0x00 title(char*),+0x08 desc(char*),+0x10 hidden(int32),+0x14 ach(int32); returnsbase+0x00.
Because rec->hidden is at rec+0x10 and rec->ach is at rec+0x14, both layouts share identical access offsets in code.
Achievement array @ 0x16cc0 (char*[10]):
| |
3.5.4. “Anti-Cheat” in landmark_lookup @ 0x29ca — Walking Yields NO Flag#
| |
- Threshold
0x1E847B = 1,999,995. The Manhattan distance from(1,1)to(999999,999999)is999998 + 999998 = 1,999,996WASD keystrokes => threshold = minimum theoretical steps minus one. If you actually walk (even on an optimal path without dodging trees), your step counter exceeds this threshold. Conversely, memory editing leaves the counter near 0. - Flag
dirty([rbp-0x81]inmain) is only set to 1 when(X,Y)changes without passing through WASD handlers (1d1e,1e82). Modifying memory while the game blocks inpoll()causes the next frame to observe coordinate divergence => setsdirty = 1=> always returns record0x16ba0(with flag). - Walking manually updates
last_X/last_Y(1ff0,2006), keepingdirty = 0. Upon reaching the beacon, the conditionwasd_inputs > 1999995triggers => returns “You actually walked here” => no flag, awarding only achievementPINCE Tutorial Failed Successfully.
Displaying the Flag on Screen (inside render() @ 0x14c3):
| |
dialog_expanded ([rbp-0x7f]) is activated when pressing 1 or 2 while standing on a landmark where hidden != 0 (0x20ba), when the HUD prompts 1 / 2: Reply | WASD: Keep walking | Q: Quit.
-> Intended Game Flow: Attach debugger -> Modify X = Y = 999999 -> Dialog THE END OF THE WORLD appears -> Press 1 -> gen_flag() runs -> Flag is printed.
3.6. Flag Generator gen_flag() @ 0x2c52 — Detailed Analysis#
3.6.1. Function Signature & Entry Gate#
| |
| |
Function prologue:
| |
