What the Shard Displayed
What the Shard Displayed
Platform: CACTF2026 | Category: Hardware | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-27 | Status: Solved Techniques: i2c_decoding, scl_sda_identification, ssd1306_init_recognition, gddram_framebuffer_reconstruction, page_addressing_render
Summary
Task: a sigrok/PulseView logic-analyzer capture (capture.sr) of an embedded device. Solution: identify the two active channels as I2C SCL/SDA, decode with sigrok-cli, recognize the SSD1306 OLED init at address 0x3C, split control bytes (0x00 command / 0x40 data), collect 3 GDDRAM frames and render the 128x64 page-addressed framebuffers to recover the flag drawn on the OLED.
What the Shard Displayed — CACTF2026
Description
The Signet shattered, the great houses fell to arguing with steel, and Alyss, Queen of Quiet Marches, began sending her dead to watch the living. One of her crows fell over our winter line, a maker's device bound beneath its wing, an eye she threaded through dead flesh to count our banners. Fed a current, it still wakes: it checks its roost, marks the hour it last saw us, and paints what it saw onto its pane. Reconstruct it, and learn what her eye found of us before the Hollow Host moves.
English summary: We are given a logic-analyzer capture of a small embedded device. The device drives an OLED display over a serial bus. The goal is to reconstruct the bus traffic and recover the images the device painted onto its screen — one of which contains the flag.
Semantic clue mapping
The flavor text maps directly to the technique:
- "maker's device / eye threaded through dead flesh" → a hobbyist hardware device driving an OLED "eye" (a logic capture of the display bus)
- "checks its roost" → the I2C device address / chip select (0x3C)
- "marks the hour it last saw us" → a clock/time frame ("05:17")
- "paints what it saw onto its pane" → the OLED framebuffer (GDDRAM) where the flag is drawn
Analysis
The capture container
The challenge download is a ZIP; inside is capture.sr, a sigrok / PulseView
logic-analyzer capture. A .sr file is itself a ZIP container. Its metadata
file describes the session:
- sigrok 0.5.2
total probes=8samplerate=2 MHzunitsize=1— each stored sample is one byte, packing all 8 channels as bits (channel Dn = bit n)
The logic samples are stored in concatenated logic-1-* chunks
(4,000,000 samples total).
Identifying the protocol
Rather than guessing, the active channels are found by measuring per-channel duty cycle and transition counts over the sample stream (numpy):
| Channel | High % | Transitions | Role |
|---|---|---|---|
| D0 | ~90% | 64642 | fast, idle-high → SCL (clock) |
| D1 | ~84% | 4256 | slower, idle-high → SDA (data) |
| D2–D7 | — | idle | unused |
Two wires, both idle-high, one fast clock plus one slower data line — the signature of I2C.
Solution
1. Decode I2C with sigrok-cli
bash read-only • copy allowedsigrok-cli -i extracted/capture.sr -P i2c:scl=D0:sda=D1 -A i2c
Key observations from the decode:
- Write address 0x3C — the standard SSD1306 OLED I2C address.
- The command stream begins with the classic SSD1306 initialization sequence:
AE D5 80 A8 3F D3 00 40 8D 14 20 00 A1 C8 DA 12 81 CF D9 F1 DB 40 A4 A6 AF(display off, clock div, multiplex 0x3F=64, charge pump, memory mode, segment remap, COM scan, contrast, display on…). - Column addressing
21 00 7F(columns 0–127) and page addressing22 00 07(pages 0–7) → a 128×64 display.
2. Split the SSD1306 stream (control bytes)
After the 0x3C write, each I2C payload is prefixed by an SSD1306 control byte:
0x00→ the following bytes are commands0x40→ the following bytes are GDDRAM (display RAM) pixel data
Parsing the transactions (parse_i2c.py) and collecting only the 0x40 data
yields 3072 GDDRAM bytes = 3 frames of 1024 bytes each
(128×64 = 8 pages × 128 columns; in page addressing each byte is 8 vertical
pixels).
python read-only • copy allowed#!/usr/bin/env python3 """Split SSD1306 I2C stream into command vs GDDRAM data (0x00 vs 0x40).""" import re # Each I2C write to 0x3C carries: <control byte> <payload...> # 0x00 -> command bytes, 0x40 -> display RAM (GDDRAM) bytes. gddram = bytearray() for control, payload in iter_i2c_writes("i2c_decode.txt"): # (control, [bytes]) if control == 0x40: gddram.extend(payload) assert len(gddram) == 3072, len(gddram) # 3 frames * 1024 bytes open("gddram.bin", "wb").write(gddram)
3. Render the framebuffers
SSD1306 uses vertical byte packing: within a page, bit 0 is the top pixel and
bit 7 the bottom pixel of that 8-pixel-tall band; pages stack top to bottom,
columns run left to right (render.py).
python read-only • copy allowed#!/usr/bin/env python3 """Render SSD1306 128x64 page-addressed GDDRAM frames to PNG.""" from PIL import Image W, H = 128, 64 PAGES = H // 8 # 8 pages FRAME = W * PAGES # 1024 bytes per frame data = open("gddram.bin", "rb").read() for f in range(len(data) // FRAME): frame = data[f * FRAME:(f + 1) * FRAME] img = Image.new("1", (W, H), 0) px = img.load() for page in range(PAGES): for col in range(W): byte = frame[page * W + col] for bit in range(8): # bit 0 = top pixel of the page if byte & (1 << bit): px[col, page * 8 + bit] = 1 img.save(f"frame_{f}.png")
Result:
frame_0.png— an eye graphic ("her eye")frame_1.png— the text "05:17" ("the hour it last saw us")frame_2.png— the flag text painted onto the display
Reading frame 2 gives the flag.