
We get keyboard.pcap, a capture of USB traffic with one keyboard on the bus. The goal is to recover what was typed.
Open it in Wireshark, or list it with tshark:
$ tshark -r keyboard.pcap -c 4
1 0.000000 host → 1.7.1 USB 64 URB_INTERRUPT in
2 0.000428 1.7.1 → host USB 72 URB_INTERRUPT in
3 0.009939 host → 1.7.1 USB 64 URB_INTERRUPT in
4 0.010727 1.7.1 → host USB 72 URB_INTERRUPT in
A USB keyboard reports keypresses over an interrupt-IN endpoint. The host submits an empty IN request (the 64-byte frames, host → device) and the keyboard answers with the data (the 72-byte frames, device → host). The 8 extra bytes on the device-to-host frames are the HID keyboard report.
The report layout is fixed:
byte 0 modifier bitmap (bit1 = left shift, bit5 = right shift)
byte 1 reserved (0)
bytes 2-7 up to six keycodes currently held down
For normal typing only one key is down at a time, so byte 2 is the key and the rest are zero. A frame of all zeros means every key was released.
The keyboard data lands in the usb.capdata field. Grab every report:
$ tshark -r keyboard.pcap -Y usb.capdata -T fields -e usb.capdata
0000000000000000
0000000000000000
0000000000000000
0200160000000000 <- modifier 0x02 (shift), key 0x16
0000000000000000
00000e0000000000 <- key 0x0e
00000c0000000000 <- key 0x0c
...
Now translate the keycodes with the USB HID usage table (Keyboard/Keypad page, 0x07). 0x04–0x1d are a–z, 0x1e–0x27 are 1–0, and so on:
02 00 16 = shift + 0x16 = S00 00 0e = 0x0e = k00 00 0c = 0x0c = iSo the first three keys are Ski, and it keeps going as SkillBit{....
There's an all-zero report after every keypress (the key coming back up). Ignore those or your output doubles up. There's also a single 0x2a (Backspace) partway through, where the typist hit a wrong key and deleted it before retyping. Skip the backspace and you end up with a stray character in the middle of 3v3ry and a flag that won't validate, so handle it the way a keyboard does and drop the previous character.
import subprocess, sys
PCAP = sys.argv[1] if len(sys.argv) > 1 else "keyboard.pcap"
KEYS = { # keycode -> (unshifted, shifted)
0x04:('a','A'),0x05:('b','B'),0x06:('c','C'),0x07:('d','D'),0x08:('e','E'),
0x09:('f','F'),0x0a:('g','G'),0x0b:('h','H'),0x0c:('i','I'),0x0d:('j','J'),
0x0e:('k','K'),0x0f:('l','L'),0x10:('m','M'),0x11:('n','N'),0x12:('o','O'),
0x13:('p','P'),0x14:('q','Q'),0x15:('r','R'),0x16:('s','S'),0x17:('t','T'),
0x18:('u','U'),0x19:('v','V'),0x1a:('w','W'),0x1b:('x','X'),0x1c:('y','Y'),
0x1d:('z','Z'),0x1e:('1','!'),0x1f:('2','@'),0x20:('3','#'),0x21:('4','$'),
0x22:('5','%'),0x23:('6','^'),0x24:('7','&'),0x25:('8','*'),0x26:('9','('),
0x27:('0',')'),0x2c:(' ',' '),0x2d:('-','_'),0x2e:('=','+'),0x2f:('[','{'),
0x30:(']','}'),0x31:('\\','|'),0x33:(';',':'),0x34:('\'','"'),0x35:('`','~'),
0x36:(',','<'),0x37:('.','>'),0x38:('/','?'),
}
rows = subprocess.run(["tshark","-r",PCAP,"-Y","usb.capdata",
"-T","fields","-e","usb.capdata"], capture_output=True, text=True).stdout.split()
out, prev = [], None
for h in rows:
d = bytes.fromhex(h.replace(":",""))
mod, key = d[0], d[2]
if key == 0: prev = None; continue
if key == prev: continue
prev = key
if key == 0x2a:
if out: out.pop()
elif key in KEYS:
out.append(KEYS[key][1 if mod & 0x22 else 0])
print("".join(out))
This will get us the flag.