DEV Community

Cover image for Weird Movements - dalCTF
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

Weird Movements - dalCTF

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)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

Only alphabetic characters are shifted; digits and symbols pass through unchanged.


Flag

dalctf{h3h3_i_s2_p41nt}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • USB pcap with link type 220 is Linux usbmon mmapped format — file identifies 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)