Category: Forensics
Flag: dalctf{h3h3_i_s2_p41nt}
Overview
We're given a packet capture. The capture turns out to be a Linux USB HID (mouse) trace — the user held left-click and physically drew the flag on screen. The drawn text is ROT13-encoded, giving one extra layer of obfuscation.
Reconnaissance
file capture.pcap
# pcap capture file, microsecond ts (little-endian) - version 2.4
# (Memory-mapped Linux USB, capture length 134217728)
The link type is 220 (LINKTYPE_USB_LINUX_MMAPPED) — Linux USB captured via usbmon. Standard tools like Wireshark will decode this, but scapy treats it as raw since it doesn't recognise link type 220 natively.
Packet Structure
The capture contains 8618 packets. Parsing with scapy using the raw usbmon header layout:
from scapy.all import rdpcap
import struct
packets = rdpcap("capture.pcap")
Each raw packet is a usbmon_packet struct (48 bytes) followed by the HID data payload. Key fields in the header:
| Offset | Field | Value |
|---|---|---|
| 8 | type |
S=submit, C=complete |
| 9 | transfer type |
1 = interrupt |
| 10 | endpoint |
0x81 = EP1 IN |
| 36 | data length | 4 bytes LE |
Filtering for interrupt IN packets with data gives 4254 HID reports — all mouse events.
HID Payload Layout
Each report is 20 bytes. The first 16 bytes are usbmon internal metadata (constant across packets). The actual HID data occupies bytes 16–19:
| Byte | Field |
|---|---|
| 16 | buttons (0 = none, 1 = left click) |
| 17 | X delta (signed 8-bit) |
| 18 | Y delta (signed 8-bit) |
| 19 | scroll wheel |
Extracting the Mouse Path
events = []
for pkt in packets:
raw = pkt.load
if len(raw) < 48:
continue
if raw[9] == 1 and (raw[10] & 0x80): # interrupt IN
data_len = struct.unpack_from("<I", raw, 36)[0]
if data_len > 0 and len(raw) > 48:
hid = raw[48:]
if len(hid) >= 20:
btn = hid[16]
dx = struct.unpack_from("b", hid, 17)[0]
dy = struct.unpack_from("b", hid, 18)[0]
events.append((dx, dy, btn))
Reconstructing absolute position by accumulating deltas:
x, y = 0, 0
path = []
for dx, dy, btn in events:
x += dx
y += dy
path.append((x, y, btn))
- Total points: 4254
- Clicked points (btn=1): 2783
- X range: −1021 to 340
- Y range: −197 to 99
Rendering the Drawing
Only points where btn == 1 (left button held) are drawn. Connecting consecutive clicked points as line segments produces clean cursive-style strokes:
from PIL import Image, ImageDraw
scale = 4
margin = 40
xmin, ymin = min(xs), min(ys)
w = (max(xs) - xmin + 1) * scale + 2*margin
h = (max(ys) - ymin + 1) * scale + 2*margin
img = Image.new("RGB", (w, h), "white")
draw = ImageDraw.Draw(img)
prev = None
for px, py, btn in path:
rx = (px - xmin) * scale + margin
ry = (py - ymin) * scale + margin
if btn == 1:
if prev:
draw.line([prev, (rx, ry)], fill="black", width=4)
prev = (rx, ry)
else:
prev = None
The rendered image reveals handwritten text: QNYPGS{U3U3_V_F2_C41AG}
ROT13 Decode
The drawn text is ROT13-encoded:
QNYPGS{U3U3_V_F2_C41AG}
↓ ROT13
dalctf{h3h3_i_s2_p41nt}
Only alphabetic characters are shifted; digits and symbols pass through unchanged.
Flag
dalctf{h3h3_i_s2_p41nt}
Key Takeaways
- USB pcap with link type 220 is Linux usbmon mmapped format —
fileidentifies it correctly but scapy needs manual parsing. - The usbmon header is 48 bytes; HID data follows immediately. For this mouse, the actual report is at bytes 16–19 of the data section (not byte 0), due to usbmon internal padding.
- Mouse drawing challenges require reconstructing absolute position from cumulative relative deltas, then rendering clicked-only segments as strokes.
- The extra ROT13 layer is a lightweight encode-on-top that's easy to miss if you're only looking for standard flag format wrappers.
Tools Used
| Tool | Purpose |
|---|---|
file |
Identify pcap link type (USB Linux mmapped) |
scapy |
Parse raw usbmon packets |
struct |
Unpack signed 8-bit deltas and little-endian fields |
Pillow |
Render mouse path as PNG |
| ROT13 | Decode drawn ciphertext to flag |
Top comments (0)