Writeup: deaDr3con'in - ApoorvCTF (Hardware)
Challenge Description
A damaged embedded CNC controller was discovered from an abandoned research facility. The machine was mid-job when the power got cut. The engineers said the machine was engraving something important before it died. We're given controller_fw.bin — the last binary loaded onto the embedded controller — and tasked with recovering the flag.
Flag format: apoorvctf{f'...}
Analysis
File Overview
controller_fw.bin: 20496 bytes (0x5010)
Architecture: ARM Cortex-M (Thumb mode, vector table at 0x0000)
Running strings on the binary reveals key information:
AXIOM-CNC fw v2.3.1
AXIOM-EMB-32 (c) 2021 Axiom Precision Ltd
job_buffer: packet format [4B:length][1B:seg_id][NB:data] x4 segments
FAULT: watchdog timeout during job_exec at flash+0x00001000
[AXIOM DEBUG] config struct OK (sizeof=34 bytes, magic=0xAA104D43): ...
cal_reserved (0x0C18): DO NOT MODIFY
JOB BUFFER FRAGMENTED
JBUFHDR5SEG4
AXIOM_END
These strings tell us:
- The firmware is for an AXIOM CNC controller
- The job buffer starts at flash offset
0x1000 - Job data is split into 4 segments with a specific packet format
- The job buffer is fragmented (segments are out of order)
- There's a config struct at
0x0C00with acal_reservedfield at0x0C18
Step 1: Parse the Config Struct (XOR Key)
The config struct at 0x0C00 (34 bytes total):
| Field | Offset | Size | Value |
|---|---|---|---|
| magic | 0x0C00 | 4B | 0xAA104D43 |
| baud | 0x0C04 | 4B | 115200 |
| x_max | 0x0C08 | 4B | 200.0 (float) |
| y_max | 0x0C0C | 4B | 200.0 (float) |
| z_max | 0x0C10 | 4B | 50.0 (float) |
| feed_max | 0x0C14 | 2B | 400 |
| spindle_max | 0x0C16 | 2B | 24000 |
| cal_reserved | 0x0C18 | 10B | f1 4c 3b a7 2e 91 c4 08 04 de |
The cal_reserved field marked "DO NOT MODIFY" is suspicious — it contains the XOR encryption key. Through analysis of expected plaintext patterns (G-code segment headers like (seg:2/4)\n), the actual key is 8 bytes: f1 4c 3b a7 2e 91 c4 08.
Step 2: Parse the Fragmented Job Buffer
The job buffer at 0x1000 starts with the header JBUFHDR5SEG4 (12 bytes), followed by 4 segments in the format [4B:length][1B:seg_id][NB:data]:
| # | Offset | Length | Seg ID | Order |
|---|---|---|---|---|
| 1 | 0x100C | 3986 | 3 | 4th |
| 2 | 0x1FA3 | 1580 | 0 | 1st |
| 3 | 0x25D4 | 2767 | 2 | 3rd |
| 4 | 0x30A8 | 246 | 1 | 2nd |
The segments are stored out of order (IDs: 3, 0, 2, 1) and must be reassembled by seg_id (0, 1, 2, 3).
Step 3: XOR Decrypt to Recover G-code
Each segment's data is XOR'd independently with the 8-byte key (key resets at the start of each segment). Decrypting reveals standard CNC G-code:
%
(AXIOM CNC CONTROLLER v2.3.1)
(job_id: 0x3F2A seg:1/4)
G21
M3
G00 Z5.000000
G00 X35.105656 Y72.065903
G01 Z-1.000000 F100.0
G01 X34.816730 Y71.796619 Z-1.000000 F400.000000
G03 X33.698985 Y71.186489 Z-1.000000 I8.989692 J-17.797826
...
The G-code contains:
G00— Rapid positioning (pen up / travel moves)G01— Linear interpolation (cutting moves)G02— Clockwise arcG03— Counter-clockwise arcZ-1.0— Tool engaged (cutting),Z5.0— Tool raised (not cutting)
Step 4: Plot the CNC Toolpath
Parsing the G-code and plotting only the cutting paths (Z < 0) with proper arc interpolation (G02/G03 use I/J center offsets) reveals the engraved text:
import struct, re, math
import matplotlib.pyplot as plt
import numpy as np
data = open("controller_fw.bin", "rb").read()
# Parse 4 segments
segments = {}
offset = 0x100C
for i in range(4):
length = struct.unpack_from('<I', data, offset)[0]
seg_id = data[offset + 4]
segments[seg_id] = data[offset + 5 : offset + 5 + length]
offset += 5 + length
# XOR decrypt each segment with 8-byte key
key = bytes([0xf1, 0x4c, 0x3b, 0xa7, 0x2e, 0x91, 0xc4, 0x08])
full_gcode = ""
for sid in sorted(segments.keys()):
sd = segments[sid]
decoded = bytes([sd[i] ^ key[i % 8] for i in range(len(sd))])
full_gcode += decoded.decode('ascii', errors='replace') + "\n"
# Parse G-code and plot cutting paths (with arc interpolation)
# ... (render G00/G01/G02/G03 commands, plot X,Y when Z < 0)
The resulting plot shows four characters engraved diagonally across the 200x200mm workspace:
| Shape | X Range | Y Range | Character |
|---|---|---|---|
| 1 | 18.6–60.5 | 16.5–77.0 | f |
| 2 | 34.8–55.5 | 66.0–87.6 | ' |
| 3 | 61.9–126.5 | 60.6–125.7 | G |
| 4 | 104.6–163.5 | 96.0–163.5 | S |
The CNC was engraving: f'GS
Flag
apoorvctf{f'GS}
Key Techniques
- Firmware reverse engineering — identifying ARM Cortex-M vector table, parsing embedded data structures
- XOR key recovery — using known-plaintext (expected G-code segment headers) to determine the 8-byte key from the
cal_reservedconfig field - Data reassembly — reordering fragmented segments by their seg_id
- G-code interpretation — parsing CNC toolpath commands including circular arc interpolation (G02/G03 with I/J offsets) and plotting the engraving path