$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: GPIO signal capture CSV and Gerber PCB files for an 8×8 LED matrix displaying a message too fast to read. Solution: Trace copper routes in Gerber files to determine GPIO pin mapping, decode column-scanning multiplexed signals (active LOW row data), and reconstruct 42 character bitmaps to reveal the flag.
"One of our embedded devices has been compromised. It was flashing a message on the debug matrix that was too fast to read, although we managed to capture one iteration of it. We must find out what was displayed. To help you with your mission, we will also provide you with the fabrication files of the PCB module the matrix was on."
Provided files:
traces.csv — GPIO signal capture: 16 channels (GPIO 5,6,12,13,16,17,18,19,20,21,22,23,24,25,26,27), 337 data rows with timestampsGerber_module/ — PCB fabrication files (Gerber format): TopLayer, BottomLayer, TopSilkLayer, SolderMask, Drill files, BoardOutline, etc. Created in EasyEDA v6.4.17Gerber files describe a PCB module with an 8×8 LED matrix and a 20-pin connector (dual-row, 2×10).
The key task is to trace the copper routes from connector pins to matrix rows and columns to determine the GPIO → matrix pin mapping.
The CSV contains 337 data rows. Data is grouped into clusters of 8 rows, separated by ~100ms pauses. Total of 42 frames (42 message characters).
Within each frame — 8 sequential column scans with ~0.5ms intervals.
16 GPIO pins control the 8×8 matrix:
At any given moment, exactly one column is active (one of 8 column-select pins = HIGH). The other 8 pins determine which LEDs in that column are lit (LOW = lit).
Over 8 scan cycles, one complete 8×8 bitmap is formed — one character.
For each of the 42 frames:
#!/usr/bin/env python3 """ Solve HackTheBox "Trace" challenge. Decodes 8x8 LED matrix multiplexed signal from GPIO capture. """ import csv gpio_pins = [5, 6, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27] pin_to_idx = {pin: i for i, pin in enumerate(gpio_pins)} # Column select pins (active HIGH, one-hot scanning), reversed for correct order col_select_pins = list(reversed([23, 18, 17, 27, 22, 24, 25, 12])) # Row data pins (active LOW = LED ON) row_pin_order = [21, 20, 26, 13, 19, 6, 5, 16] # Read CSV data rows_data = [] with open("tasks/hackthebox/Trace/traces.csv", "r") as f: reader = csv.reader(f) next(reader) # skip header for row in reader: time = float(row[0]) values = [int(x) for x in row[1:]] rows_data.append((time, values)) # Group into frames by time gaps (>50ms = new frame) frames = [] current_frame = [] for i, (time, values) in enumerate(rows_data): if i > 0 and (time - rows_data[i - 1][0]) > 0.05: if current_frame: frames.append(current_frame) current_frame = [] current_frame.append((time, values)) if current_frame: frames.append(current_frame) def extract_bitmap(frame): """Extract 8x8 bitmap from one frame (8 column scans).""" bitmap = [[0] * 8 for _ in range(8)] for _, vals in frame: active_col = -1 for col_num, cs_pin in enumerate(col_select_pins): if vals[pin_to_idx[cs_pin]] == 1: active_col = col_num break if active_col == -1: continue for phys_row in range(8): rd_pin = row_pin_order[phys_row] # Active LOW: 0 = LED ON = pixel set bitmap[phys_row][active_col] = 1 - vals[pin_to_idx[rd_pin]] return bitmap def bitmap_to_bytes(bitmap): """Convert 8x8 bitmap to tuple of 8 bytes (MSB = leftmost pixel).""" result = [] for row in bitmap: byte = 0 for col in range(8): if row[col]: byte |= 1 << (7 - col) result.append(byte) return tuple(result) # Character lookup table: 8-byte bitmap tuple → character char_map = { (0xC6, 0xC6, 0xC6, 0xFE, 0xFE, 0xC6, 0xC6, 0xC6): "H", (0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18): "T", (0xF8, 0xFC, 0xC6, 0xFE, 0xFE, 0xC6, 0xFC, 0xF8): "B", (0x0C, 0x10, 0x10, 0x30, 0x30, 0x10, 0x10, 0x0C): "{", (0x30, 0x08, 0x08, 0x0C, 0x0C, 0x08, 0x08, 0x30): "}", (0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C): "_", (0x10, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38): "1", (0x38, 0x04, 0x04, 0x04, 0x38, 0x04, 0x38, 0x00): "3", (0x42, 0xE7, 0x7E, 0x3C, 0x3C, 0x7E, 0xE7, 0x42): "X", (0xE3, 0xF3, 0xFB, 0xCF, 0xDF, 0xC7, 0xC3, 0xC3): "N", (0xFE, 0xFE, 0xC0, 0xFE, 0xFE, 0xC0, 0xC0, 0xC0): "F", (0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0x7E): "U", (0xFC, 0xFA, 0xC6, 0xC6, 0xC6, 0xC6, 0xFA, 0xFC): "D", (0x3E, 0x7E, 0xE0, 0xC0, 0xC0, 0xE0, 0x7E, 0x3E): "C", (0xC3, 0xFF, 0xFF, 0xC3, 0xDB, 0xC3, 0xC3, 0xC3): "M", (0x3C, 0x04, 0x04, 0x10, 0x08, 0x10, 0x10, 0x10): "7", (0xFC, 0xFE, 0xC6, 0xFC, 0xC6, 0xF8, 0xC0, 0xC0): "P", (0x3C, 0x24, 0x24, 0x04, 0x3C, 0x04, 0x3C, 0x00): "9", (0x00, 0x08, 0x18, 0x7C, 0x28, 0x08, 0x08, 0x00): "4", (0x3C, 0x24, 0x24, 0x24, 0x3C, 0x24, 0x3C, 0x00): "8", (0x3C, 0x20, 0x20, 0x04, 0x3C, 0x04, 0x3C, 0x00): "5", } # Decode all frames flag = [] for fi, frame in enumerate(frames): bm = extract_bitmap(frame) bt = bitmap_to_bytes(bm) char = char_map.get(bt, "?") flag.append(char) print(f"Flag: {''.join(flag)}") # HTB{MU171P13X1N9_4ND_PC85_FUND1M3N7415_37}
HTB{MU171P13X1N9_4ND_PC85_FUND1M3N7415_37}The critical step is determining which GPIO pins control columns vs rows. This is done in two ways:
From Gerber Files: Trace copper routes from the 20-pin connector to the LED matrix. Each connector pin is connected to either a column or a row of the matrix.
From Signal Analysis: Column-select pins have a characteristic one-hot pattern (exactly one HIGH in each row of a frame). Row-data pins change arbitrarily depending on the displayed character.
The flag uses leet-speak encoding:
MU171P13X1N9 → MULTIPLEXING (1=L/I, 7=T, 3=E, 9=G)4ND → AND (4=A)PC85 → PCBs (8=B, 5=S)FUND1M3N7415 → FUNDAMENTALS (1=I/A/L, 3=E, 7=T, 4=A, 5=S)37 → 37 (leet ending, 3=E, 7=T)Use this technique when:
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar