Photo by Alexandre Debiève / Unsplash

Writeup: deaDr3con'in - ApoorvCTF (Hardware)

Feri Harjulianto

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 0x0C00 with a cal_reserved field at 0x0C18

Step 1: Parse the Config Struct (XOR Key)

The config struct at 0x0C00 (34 bytes total):

FieldOffsetSizeValue
magic0x0C004B0xAA104D43
baud0x0C044B115200
x_max0x0C084B200.0 (float)
y_max0x0C0C4B200.0 (float)
z_max0x0C104B50.0 (float)
feed_max0x0C142B400
spindle_max0x0C162B24000
cal_reserved0x0C1810Bf1 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 bytesf1 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]:

#OffsetLengthSeg IDOrder
10x100C398634th
20x1FA3158001st
30x25D4276723rd
40x30A824612nd

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 arc
  • G03 — Counter-clockwise arc
  • Z-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:

ShapeX RangeY RangeCharacter
118.6–60.516.5–77.0f
234.8–55.566.0–87.6'
361.9–126.560.6–125.7G
4104.6–163.596.0–163.5S

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_reserved config 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
CTFWriteupHardware