hardwarefreeeasy/medium

Trace

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.

$ ls tags/ techniques/
gerber_trace_routingled_matrix_demultiplexinggpio_pin_mappingbitmap_font_recognitioncolumn_scan_decodingactive_low_signal_interpretation

Trace — HackTheBox

Description

"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 timestamps
  • Gerber_module/ — PCB fabrication files (Gerber format): TopLayer, BottomLayer, TopSilkLayer, SolderMask, Drill files, BoardOutline, etc. Created in EasyEDA v6.4.17

Analysis

1. Reconnaissance — Gerber Files

Gerber files describe a PCB module with an 8×8 LED matrix and a 20-pin connector (dual-row, 2×10).

  • TopSilkLayer (GTO) — physical layout: 8×8 LED footprint grid with markings
  • TopLayer (GTL) — copper traces from the 20-pin connector (bottom of the board) to the LED matrix
  • LED columns are positioned along X with ~230mil spacing (8 positions)
  • LED rows — 8 positions along Y, each LED has a pair of pads (anode/cathode)

The key task is to trace the copper routes from connector pins to matrix rows and columns to determine the GPIO → matrix pin mapping.

2. Reconnaissance — traces.csv

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.

3. Working Principle — Column Scanning Multiplexing

16 GPIO pins control the 8×8 matrix:

  • 8 column select pins (active HIGH, one-hot): GPIO 23, 18, 17, 27, 22, 24, 25, 12
  • 8 row data pins (active LOW = LED ON): GPIO 21, 20, 26, 13, 19, 6, 5, 16

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.

4. Decoding

For each of the 42 frames:

  1. Determine the active column (which column-select pin = HIGH)
  2. Read the 8 row-data pins (LOW = pixel ON)
  3. Assemble the 8×8 bitmap (8 bytes, MSB = leftmost pixel)
  4. Match the bitmap to a character via a font lookup table

Solution

#!/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}

Step-by-Step Breakdown

  1. CSV Parsing: Read 337 rows of GPIO data with timestamps
  2. Frame Grouping: Split by time gaps >50ms → 42 frames of 8 rows each
  3. Bitmap Extraction: For each frame, determine the active column (one-hot) and read row data (active LOW)
  4. Character Mapping: Each 8×8 bitmap is matched to a character via a pre-built lookup table
  5. Flag Assembly: 42 characters → HTB{MU171P13X1N9_4ND_PC85_FUND1M3N7415_37}

Determining Pin Mapping

The critical step is determining which GPIO pins control columns vs rows. This is done in two ways:

  1. 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.

  2. 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.

Leet-speak Decoding

The flag uses leet-speak encoding:

  • MU171P13X1N9MULTIPLEXING (1=L/I, 7=T, 3=E, 9=G)
  • 4NDAND (4=A)
  • PC85PCBs (8=B, 5=S)
  • FUND1M3N7415FUNDAMENTALS (1=I/A/L, 3=E, 7=T, 4=A, 5=S)
  • 3737 (leet ending, 3=E, 7=T)

Key Indicators

Use this technique when:

  • GPIO signal capture (CSV) with 16 channels — typical sign of a multiplexed LED matrix (8 col + 8 row)
  • Gerber/PCB files included — need to trace routes to determine pin mapping
  • Data grouped in clusters of 8 rows — column scanning with 8 columns
  • One-hot pattern in some pins — column select signals
  • Description mentions "flashing message" or "too fast to read" — multiplexed display
  • 8×8 LED matrix on PCB — standard format for displaying characters one at a time
  • Active LOW row data — common scheme for LED matrices (current flows when cathode is LOW)

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups