Challenge File: https://github.com/mlesterdampios/tcon8/tree/main/the_typing_ghost
In this challenge we’re given a PCAP that looks like normal UDP noise at first, but it has a very “human” rhythm: a long burst of small packets all going to UDP/55555, while the source port starts at 4000 and increments by 1. That combination (tiny payloads + steady cadence + monotonic “counter” behavior) is a classic sign of keystroke-style exfiltration rather than bulk file transfer.
1) PCAP triage: spotting the exfil stream
In Wireshark, filtering down to the suspicious traffic makes the pattern obvious:
- Display filter (example):
udp.dstport == 55555 - What you’ll observe:
- Many packets with very small UDP payloads (consistent size)
- Source ports like
4000, 4001, 4002, ...(likely a per-event/sequence counter) - Destination port fixed at
55555(receiver “collector” service)
At this stage the working hypothesis becomes:
Each packet represents one keyboard event (press/release), exfiltrated to a listener on UDP/55555.
2) Recognizing the payload format (“hid:NNN|….”)
When extracting the UDP payload bytes, the data decodes cleanly into ASCII and follows this structure:
hid:<sequence>|<hex report bytes>
Example line (after hex→ASCII):
hid:008|02002f0000000000
Key observations:
- The
hid:NNNportion is metadata (sequence/index). - The part after
|is always 16 hex characters, i.e. 8 bytes.
That “8-byte report” is a huge tell: it matches the standard USB HID keyboard report layout.
3) USB HID keyboard report (why 8 bytes matters)
A standard HID keyboard input report is typically:
| Byte | Meaning |
|---|---|
| 0 | Modifier bitmap (Shift/Ctrl/Alt/GUI, left/right) |
| 1 | Reserved (often 0x00) |
| 2–7 | Up to 6 simultaneous keycodes |
So a report like:
02 00 2f 00 00 00 00 00
means:
0x02in the modifier byte → Left Shift is pressed- keycode
0x2fis present → a specific key (in the map:'[') - with Shift held,
'['becomes'{'
Also, you’ll frequently see:
00 00 00 00 00 00 00 00
which indicates no keys pressed — effectively a key release event. That’s why skipping those records is correct: they don’t add characters.
4) Extract → decode: turning HID keycodes into text
The script does the right pipeline:
- Hex → bytes → ASCII to get
hid:NNN|... - Split on
|to isolate the 8-byte report - Ignore
0000000000000000(release frames) - Read:
mod = b[0](modifier)- keycodes from
b[2:](the 6 key slots)
- Map HID codes to characters using a lookup table
- If Shift is active (
0x02left shift or0x20right shift), apply:- uppercase for letters
- symbol transform for
1..0,-,=,[,],\, etc.
Conceptually, the “decoder” is reconstructing exactly what a keylogger would record from raw HID events.
5) Result
Running the decoder over the extracted stream reconstructs the full typed message:
tcon{USB_H1D3N_3NTRY_TC0N_EXF1L_2025}
tshark -r usb.pcap -Y "ip.src==10.20.0.5 && ip.dst==198.51.100.200 && udp.dstport==55555" -T fields -e udp.payload

Solution
data = """6869643a3030307c3030303031373030303030303030
6869643a3030317c30303030303030303030303030303030
6869643a3030327c3030303030363030303030303030
6869643a3030337c30303030303030303030303030303030
6869643a3030347c3030303031323030303030303030
6869643a3030357c30303030303030303030303030303030
6869643a3030367c3030303031313030303030303030
6869643a3030377c30303030303030303030303030303030
6869643a3030387c3032303032663030303030303030
6869643a3030397c30303030303030303030303030303030
6869643a3031307c3032303031383030303030303030
6869643a3031317c30303030303030303030303030303030
6869643a3031327c3032303031363030303030303030
6869643a3031337c30303030303030303030303030303030
6869643a3031347c3032303030353030303030303030
6869643a3031357c30303030303030303030303030303030
6869643a3031367c3032303032643030303030303030
6869643a3031377c30303030303030303030303030303030
6869643a3031387c3032303030623030303030303030
6869643a3031397c30303030303030303030303030303030
6869643a3032307c3030303031653030303030303030
6869643a3032317c30303030303030303030303030303030
6869643a3032327c3032303030373030303030303030
6869643a3032337c30303030303030303030303030303030
6869643a3032347c3030303032303030303030303030
6869643a3032357c30303030303030303030303030303030
6869643a3032367c3032303031313030303030303030
6869643a3032377c30303030303030303030303030303030
6869643a3032387c3032303032643030303030303030
6869643a3032397c30303030303030303030303030303030
6869643a3033307c3030303032303030303030303030
6869643a3033317c30303030303030303030303030303030
6869643a3033327c3032303031313030303030303030
6869643a3033337c30303030303030303030303030303030
6869643a3033347c3032303031373030303030303030
6869643a3033357c30303030303030303030303030303030
6869643a3033367c3032303031353030303030303030
6869643a3033377c30303030303030303030303030303030
6869643a3033387c3032303031633030303030303030
6869643a3033397c30303030303030303030303030303030
6869643a3034307c3032303032643030303030303030
6869643a3034317c30303030303030303030303030303030
6869643a3034327c3032303031373030303030303030
6869643a3034337c30303030303030303030303030303030
6869643a3034347c3032303030363030303030303030
6869643a3034357c30303030303030303030303030303030
6869643a3034367c3030303032373030303030303030
6869643a3034377c30303030303030303030303030303030
6869643a3034387c3032303031313030303030303030
6869643a3034397c30303030303030303030303030303030
6869643a3035307c3032303032643030303030303030
6869643a3035317c30303030303030303030303030303030
6869643a3035327c3032303030383030303030303030
6869643a3035337c30303030303030303030303030303030
6869643a3035347c3032303031623030303030303030
6869643a3035357c30303030303030303030303030303030
6869643a3035367c3032303030393030303030303030
6869643a3035377c30303030303030303030303030303030
6869643a3035387c3030303031653030303030303030
6869643a3035397c30303030303030303030303030303030
6869643a3036307c3032303030663030303030303030
6869643a3036317c30303030303030303030303030303030
6869643a3036327c3032303032643030303030303030
6869643a3036337c30303030303030303030303030303030
6869643a3036347c3030303031663030303030303030
6869643a3036357c30303030303030303030303030303030
6869643a3036367c3030303032373030303030303030
6869643a3036377c30303030303030303030303030303030
6869643a3036387c3030303031663030303030303030
6869643a3036397c30303030303030303030303030303030
6869643a3037307c3030303032323030303030303030
6869643a3037317c30303030303030303030303030303030
6869643a3037327c3032303033303030303030303030
6869643a3037337c30303030303030303030303030303030""".strip().splitlines()
hid = {
0x04:'a',0x05:'b',0x06:'c',0x07:'d',0x08:'e',0x09:'f',0x0A:'g',0x0B:'h',0x0C:'i',0x0D:'j',0x0E:'k',0x0F:'l',
0x10:'m',0x11:'n',0x12:'o',0x13:'p',0x14:'q',0x15:'r',0x16:'s',0x17:'t',0x18:'u',0x19:'v',0x1A:'w',0x1B:'x',0x1C:'y',0x1D:'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:'\\'
}
shift = {'1':'!','2':'@','3':'#','4':'$','5':'%','6':'^','7':'&','8':'*','9':'(','0':')',
'-':'_','=':'+','[':'{',']':'}','\\':'|'}
out = []
for hx in data:
s = bytes.fromhex(hx).decode()
rep = s.split('|',1)[1]
if rep == "0000000000000000": # release
continue
b = bytes.fromhex(rep)
mod = b[0]
keys = b[2:]
for k in keys:
if k == 0: continue
ch = hid.get(k, '')
if mod & (0x02|0x20): # L/R shift
ch = ch.upper() if ch.isalpha() else shift.get(ch, ch)
out.append(ch)
print(''.join(out))

