Đề bài

Phân tích
Đầu tiên mình sẽ xem xem lưu lượng mạng từ file pcap này

Nhận thấy đây là các kết nối USB, vậy thì nó sẽ liên quan đến tương tác người dùng nhiều hơn
Đầu tiên sẽ là các gói tin ban đầu đại diện cho việc kết nối giữa USB và có thể là máy tính hoặc một thiết bị nào đó với GET/SET DESCRIPTOR hoặc SET CONFIGURATION

Protocol = USB, Info mô tả request/response chuẩn:
- GET DESCRIPTOR Request DEVICE → host hỏi Device Descriptor (18 byte: bcdUSB, idVendor, idProduct, bMaxPacketSize0…)
- GET DESCRIPTOR Response DEVICE → thiết bị trả descriptor DEVICE
- GET DESCRIPTOR Request CONFIGURATION → host xin Configuration Descriptor (chuỗi: Configuration + Interface + (HID) + Endpoint…)
- GET DESCRIPTOR Response CONFIGURATION → thiết bị trả block descriptor dài (wTotalLength, nên bạn thấy frame length lớn 46/87/228…)
- SET CONFIGURATION Request/Response → host chọn cấu hình (thường là cấu hình 1) và thiết bị ACK (status stage)
Tiếp theo bên dưới sẽ là các gói tin thể hiện sự tương tác giữa người dùng và thiết bị

Những gói URB INTERRUPT là USB Request Block của hệ điều hành
- Với HID (bàn phím/presenter), dữ liệu được truyền bằng Interrupt transfer trên endpoint IN (thiết bị → host) theo chu kỳ bInterval (host chủ động “polling”)
- Vì là IN, mỗi “chu kỳ” thường tạo 2 bản ghi trong capture
- URB_SUBMIT (source = host, dest = bus.dev.ep): host gửi yêu cầu đọc
- URB_COMPLETE (source = bus.dev.ep, dest = host): thiết bị trả kết quả (có hoặc không có dữ liệu)
Còn với HID, HID là Keyboard/Presenter phổ biến, Input Report luôn 8 byte

[0] modifier | [1] reserved=0x00 | [2..7] là các keycode (tối đa 6 phím đang giữ)
- modifier (byte 0): bitmask (0x01 LCtrl, 0x02 LShift, 0x04 LAlt, 0x08 LGUI, 0x10 RCtrl, 0x20 RShift, 0x40 RAlt, 0x80 RGUI)
- reserved (byte 1): luôn 0x00
- keycode (byte 2..7): mã HID theo Keyboard/Keypad Usage Page (0x07). 0x00 = không có phím tại vị trí đó
Trong bài này mình phát hiện HID chỉ có 3 trường hợp
- 0200520000000000: 0x02 = Left Shift đang giữ; 0x52 = Up Arrow; còn lại 0 → không có phím khác
- 0200510000000000: 0x02 = Left Shift đang giữ; 0x51 = Down Arrow; còn lại 0 → không có phím khác
- 0200000000000000: 0x02 = Left Shift đang giữ; còn lại 0 → không có phím khác
Vậy nó sẽ rất giống với mã morse khi có 1 dài 1 ngắn, dựa trên ý tưởng này, mình tiếp tục phân tích tiếp
Trích xuất
Đầu tiên để phân tích thì mình sẽ trích xuất các HID từ gói tin pcap này ra
Sử dụng tshark để lấy ra hid.hex
$ tshark -r My_PPT.pcapng \
-Y 'usbhid.data && usb.endpoint_address.direction==1 && usb.data_len==8' \
-T fields -e usbhid.data > hid.hex
Kiểm tra xem đã lấy ra thành công chưa
$ cat hid.hex
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200510000000000
0200000000000000
0200510000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
0200520000000000
0200000000000000
0200510000000000
0200000000000000
0200520000000000
0200000000000000
Vậy là đã trích xuất xong, bây giờ với suy đoán của mình thì khả năng 0x52 sẽ là dài, 0x51 sẽ là ngắn, tại vì 0x52 > 0x51 nên mình nghĩ vậy thôi
Script
#!/usr/bin/env python3
import sys, struct, re
EPB_TYPE = 0x00000006 # Enhanced Packet Block
MORSE = {
".-":"A","-...":"B","-.-.":"C","-..":"D",".":"E","..-.":"F","--.":"G","....":"H","..":"I",".---":"J",
"-.-":"K",".-..":"L","--":"M","-.":"N","---":"O",".--.":"P","--.-":"Q",".-.":"R","...":"S","-":"T",
"..-":"U","...-":"V",".--":"W","-..-":"X","-.--":"Y","--..":"Z",
"-----":"0",".----":"1","..---":"2","...--":"3","....-":"4",".....":"5","-....":"6","--...":"7","---..":"8","----.":"9",
".-.-.-":".","--..--":",","..--..":"?","-.-.--":"!","-....-":"-","-..-.":"/",".--.-.":"@","-.--.":"(","-.--.-":")","..--.-":"_"
}
def read_pcapng_epbs(path):
epbs = []
data = open(path, "rb").read()
pos, n = 0, len(data)
while pos + 8 <= n:
bt, bl = struct.unpack_from("<II", data, pos)
if bl < 12 or pos + bl > n: break
if bt == EPB_TYPE:
# EPB fixed fields after type/len
iface, ts_hi, ts_lo, cap_len, orig_len = struct.unpack_from("<IIIII", data, pos+8)
pkt_start = pos + 8 + 20
pkt_end = pkt_start + cap_len
if pkt_end <= pos + bl:
epbs.append((ts_hi, ts_lo, data[pkt_start:pkt_end]))
pos += bl
return epbs
def decode_hid_candidates(pkt_bytes):
"""Scan for HID 8-byte report windows: [mod][res=0][k1..k6], >=1 key non-zero, small number of simultaneous keys."""
cands = []
L = len(pkt_bytes)
for i in range(0, L-7):
chunk = pkt_bytes[i:i+8]
if chunk[1] != 0: # reserved must be 0
continue
keys = chunk[2:]
if not any(keys):
continue
# keep reports with up to 2 non-zero keys (reduce false positives)
if sum(1 for k in keys if k != 0) > 2:
continue
cands.append(chunk)
return cands
def keydown_events(epbs):
"""Emit key-down events (newly pressed HID codes) with timestamps."""
prev_pressed = set()
events = []
for ts_hi, ts_lo, pkt in epbs:
# Just scan raw payload for HID-like 8-byte windows
for rep in decode_hid_candidates(pkt):
mod = rep[0]
keys = [k for k in rep[2:] if k != 0]
cur = set(keys)
new_keys = [k for k in keys if k not in prev_pressed]
t = ts_hi + ts_lo/1e9 # relative time ok
for k in new_keys:
events.append((t, k, mod))
prev_pressed = cur
return events
def decode_morse_from_arrows(events):
"""Use only UP/DOWN (0x52/0x51) with timestamps, infer unit timing, segment Morse, try two mappings."""
# filter to arrows and sort by time
arr = [(t, 'U' if k==0x52 else 'D') for (t,k,_) in events if k in (0x51,0x52)]
arr.sort()
if not arr:
return "", None, 0.05
# build dt sequence
seq = []
for i,(t,sym) in enumerate(arr):
dt = 0.0 if i==0 else max(0.0, t - arr[i-1][0])
seq.append((sym, t, dt))
# estimate unit from small non-zero gaps (10–40 percentile avg)
nonzero = sorted([dt for _,_,dt in seq if dt>0])
if nonzero:
p10 = nonzero[int(0.10*len(nonzero))]
p40 = nonzero[int(0.40*len(nonzero))]
unit = (p10 + p40)/2 if p40>0 else (p10 or 0.05)
else:
unit = 0.05
def decode_with(mapping_u_dot=True, intra=1.5, word=4.5):
# mapping: True -> U='.', D='-'; False -> U='-', D='.'
def gap(dt):
if dt <= intra*unit: return 'intra'
if dt <= word*unit: return 'letter'
return 'word'
blocks, cur = [], []
for i,(sym, _, dt) in enumerate(seq):
ch = ('.' if mapping_u_dot else '-') if sym=='U' else ('-' if mapping_u_dot else '.')
cur.append(ch)
if i+1 < len(seq):
g = gap(seq[i+1][2])
if g=='letter': blocks.append("".join(cur)); cur=[]
elif g=='word': blocks.append("".join(cur)); blocks.append(" "); cur=[]
if cur: blocks.append("".join(cur))
text = "".join(" " if b==" " else MORSE.get(b, "?") for b in blocks)
return text, blocks
# try both mappings; pick fewer '?' (tie-breaker: prefers 'KHK','LOOK','PPT','AT','MY')
t1,_ = decode_with(mapping_u_dot=True)
t2,_ = decode_with(mapping_u_dot=False)
def score(tx): return -5*tx.count("?") + 3*tx.count("KHK") + 2*tx.count("LOOK") + 2*tx.count("PPT") + tx.count("AT") + tx.count("MY")
best = (t1, True) if score(t1) >= score(t2) else (t2, False)
return best[0], best[1], unit
def normalize_flag(text):
# collapse spaces, map () -> {} then look for KHK{...}
norm = "".join(text.split()).replace("(", "{").replace(")", "}")
m = re.search(r"KHK\{[A-Z0-9_]+\}", norm.upper())
return m.group(0) if m else None
def main():
if len(sys.argv) < 2:
print("Usage: python3 usb_presenter_morse_solver.py <file.pcapng>")
sys.exit(1)
epbs = read_pcapng_epbs(sys.argv[1])
events = keydown_events(epbs)
text, u_is_dot, unit = decode_morse_from_arrows(events)
print(f"[*] arrow-events: {sum(1 for e in events if e[1] in (0x51,0x52))}, unit≈{unit:.4f}s, mapping: {'U=.' if u_is_dot else 'U=-'}")
print("[*] decode:", text)
flag = normalize_flag(text)
if flag:
print("[*] flag:", flag)
if __name__ == "__main__":
main()
Khi chạy ra thì kết quả khá xấu, có vẻ là đề cố tình làm nhiễu
$ python3 solve.py My_PPT.pcapng
[*] arrow-events: 70, unit≈0.0006s, mapping: U=-
[*] decode: K H K ( L O O K IQ A T IQ M Y _ EG P T T?
Flag
Flag: KHK{LOOK_AT_MY_PPT}
'WriteUp > Forensics' 카테고리의 다른 글
| [Forensics] flask-forensics - Dreamhack (0) | 2025.10.12 |
|---|---|
| [Forensics] abcdefg-who - Dreamhack (0) | 2025.10.12 |
| [Forensics] Dream Zoo - Dreamhack (0) | 2025.10.11 |
| [Forensics] Silent Visitor - Securinets CTF Quals 2025 (0) | 2025.10.06 |
| [Forensics] Remotely Interesting (SunshineCTF 2025) (0) | 2025.09.30 |
