Dead Reckoning
Challenge#
Category: Reverse Engineering / Hardware Forensics
Difficulty: Medium
Challenge Description#
A damaged embedded CNC controller was discovered at 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. Can you recover what it was making and find the flag in the process?
The flag contains the charactersf'to identify it when you see it.
The only file the team was able to recover from the CNC machine is the binary file last loaded onto the embedded controller. Good luck.
We are given one file: controller_fw.bin.
Story hint says the CNC machine was engraving something important, and the flag content contains f'....
Expected final format: apoorvctf{...}.
1) Fast Triage#
File type and strings#
file controller_fw.bin
strings -n 6 controller_fw.binbashKey observations from strings:
- Firmware/banner-like strings:
AXIOM-CNC fw v2.3.1 - Job-related hints:
job_buffer: packet format [4B:length][1B:seg_id][NB:data] x4 segmentsJOB BUFFER FRAGMENTED
- Debug hint:
cal_reserved (0x0C18): DO NOT MODIFY
- Markers in binary:
JBUFHDR5SEG4AXIOM_END
This strongly suggests embedded job data exists in 4 fragmented packets, probably obfuscated/encrypted.
2) Locate Important Offsets#
I scanned offsets for known markers and found:
JBUFHDR5SEG4at0x1000AXIOM_ENDnear EOF- file size
0x5010(20496 bytes)
Hex around 0x1000 looked like:
0x1000: JBUFHDR5SEG4
0x100C: 92 0f 00 00 03 ...textInterpreting with hinted format [4B:length][1B:seg_id][NB:data]:
0x00000f92= 3986 bytes,seg_id = 3- followed by next packet, etc.
3) Parse Fragmented Job Packets#
I parsed 4 packets sequentially from 0x100C:
seg 3: length 3986seg 0: length 1580seg 2: length 2767seg 1: length 246
Reordered by segment ID (0,1,2,3) to reconstruct logical job stream.
4) Recover Obfuscation Key#
From strings, this value stood out:
cal_reserved (0x0C18): DO NOT MODIFY
Hex dump at 0x0C18:
f1 4c 3b a7 2e 91 c4 08textTesting this as repeating XOR key over packet payload produced clear ASCII G-code immediately.
Recovered XOR key:
f14c3ba72e91c408textThis is the core trick of the challenge: job packets are XOR-obfuscated with the cal_reserved bytes.
5) Decrypt Result = CNC G-code#
After XOR-decrypting each segment with the 8-byte repeating key, plaintext shows valid G-code, e.g.:
%
(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
...
M5
G00 X0.0000 Y0.0000
M2
%textSo firmware did indeed contain the engraving toolpath.
6) Reconstruct the Engraving Geometry#
I rendered the path by:
- treating
G01as line segments - approximating
G02/G03arcs with short line samples - only drawing moves while
Z < 0(cutting depth)
The rendered output clearly spells:
f'GStextTherefore flag content is f'GS.
7) Final Flag#
apoorvctf{f'GS}textIf the platform enforces typographic apostrophe from statement wording, alternate try:
apoorvctf{f’GS}textRepro Script (Single-File)#
#!/usr/bin/env python3
from pathlib import Path
import re, math
import numpy as np
import cv2
fw = Path("controller_fw.bin").read_bytes()
# 1) parse 4 packets from job buffer
off = 0x100C
segments = []
for _ in range(4):
ln = int.from_bytes(fw[off:off+4], "little")
sid = fw[off+4]
data = fw[off+5:off+5+ln]
segments.append((sid, data))
off += 5 + ln
# 2) key from cal_reserved offset
key = fw[0x0C18:0x0C20] # f1 4c 3b a7 2e 91 c4 08
def xor_rep(data, key):
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
dec = b"".join(xor_rep(d, key) for _, d in sorted(segments, key=lambda x: x[0]))
Path("controller_job_dec.bin").write_bytes(dec)
text = dec.decode("ascii", errors="ignore")
print("--- GCODE PREVIEW ---")
print("\n".join(text.splitlines()[:20]))
# 3) render toolpath
lines = [ln.strip() for ln in text.splitlines() if ln.strip() and not ln.strip().startswith("(") and ln.strip() != "%"]
x = y = z = 0.0
draw_segments = []
for ln in lines:
m = re.search(r"G(\d\d)", ln)
cmd = "G" + m.group(1) if m else None
p = {k: float(v) for k, v in re.findall(r"([XYZIJF])(-?\d+(?:\.\d+)?)", ln)}
nx, ny, nz = p.get("X", x), p.get("Y", y), p.get("Z", z)
if "Z" in p:
z = nz
cut = z < 0
if cmd in ("G00", "G01"):
if "X" in p or "Y" in p:
if cut:
draw_segments.append(((x, y), (nx, ny)))
x, y = nx, ny
elif cmd in ("G02", "G03"):
I, J = p.get("I", 0.0), p.get("J", 0.0)
cx, cy = x + I, y + J
r = math.hypot(x - cx, y - cy)
a0 = math.atan2(y - cy, x - cx)
a1 = math.atan2(ny - cy, nx - cx)
cw = cmd == "G02"
if cw and a1 >= a0:
a1 -= 2 * math.pi
if (not cw) and a1 <= a0:
a1 += 2 * math.pi
steps = max(16, int(abs(a1 - a0) * max(r, 1) / 0.5))
px, py = x, y
for i in range(1, steps + 1):
a = a0 + (a1 - a0) * i / steps
qx, qy = cx + r * math.cos(a), cy + r * math.sin(a)
if cut:
draw_segments.append(((px, py), (qx, qy)))
px, py = qx, qy
x, y = nx, ny
pts = np.array([p for s in draw_segments for p in s], dtype=np.float64)
minx, miny = pts.min(axis=0)
maxx, maxy = pts.max(axis=0)
scale, pad = 8, 20
W = int((maxx - minx) * scale + 2 * pad)
H = int((maxy - miny) * scale + 2 * pad)
img = np.full((H, W), 255, np.uint8)
def tr(pt):
xx = int(round((pt[0] - minx) * scale + pad))
yy = int(round((maxy - pt[1]) * scale + pad))
return xx, yy
for a, b in draw_segments:
cv2.line(img, tr(a), tr(b), 0, 2, cv2.LINE_AA)
cv2.imwrite("controller_job_render.png", img)
print("Saved controller_job_render.png")
print("Visually read text as: f'GS")
print("Flag: apoorvctf{f'GS}")pythonDead Ends / Notes#
- The string
Qkkballooked like Base64 bait; decoding it alone was not the path. - Searching for direct
flag/apoorvctfstrings in raw firmware fails (as expected due to obfuscation). - Key recovery depended on interpreting
cal_reserved (0x0C18)as a real decryption material location.