Keylogger
Ссылка на задание http://f8tasks.ru/challenges#Keylogger-24
| Category | Forensics |
| Difficulty | Medium |
| Technique | USB HID Keyboard Analysis |
| Flag | ws{Keyb0ard_is_@_ch3at} |
Recon
На зараженном устройстве был обнаружен кейлоггер. Узнайте что он записал.
Артефакт задачи: task2_final.pcapng.
Первичный осмотр файла:
file task2_final.pcapng
task2_final.pcapng: pcapng capture file - version 1.0
Файл — сетевой дамп. Рефлекс большинства участников: открыть в Wireshark и искать HTTP/DNS/FTP. Это ложный путь.
Анализ трафика по устройствам и endpoint-ам:
# статистика активности endpoint-ов
bus=1 dev=8 ep=0x81 xfer=1 count=120 # ← явный лидер
bus=1 dev=1 ep=0x80 xfer=2 count=8
bus=1 dev=1 ep=0x00 xfer=2 count=6
bus=1 dev=1 ep=0x81 xfer=1 count=6
bus=1 dev=5 ep=0x80 xfer=2 count=4
Ключевые наблюдения:
- Трафик не сетевой, а usbmon — захват USB-шины ядром Linux.
- Устройство
1.8(bus=1, dev=8) имеет активный interrupt IN endpoint0x81с 120 пакетами. - Почти все остальные — единичные control-транзакции (xfer=2) к хаб-устройству.
xfer_type=1— это interrupt transfer — именно так клавиатуры передают нажатия.
Первые пять payload-ов с активного endpoint-а:
00 00 1a 00 00 00 00 00 ← key 0x1a, без Shift
00 00 16 00 00 00 00 00 ← key 0x16, без Shift
02 00 2f 00 00 00 00 00 ← key 0x2f, Shift=0x02
02 00 0e 00 00 00 00 00 ← key 0x0e, Shift=0x02
00 00 0e 00 00 00 00 00 ← key 0x0e, без Shift (удержание)
8-байтовый payload — стандартный размер USB HID клавиатурного отчёта. Вывод: перед нами захват ввода с USB-клавиатуры.
USB HID Keyboard Analysis
USB HID (Human Interface Device) — стандартный протокол, по которому клавиатуры, мыши и геймпады общаются с компьютером. Каждое нажатие клавиши передаётся как HID report — пакет фиксированного размера.
Для клавиатуры report всегда 8 байт:
| Байт | Назначение |
|---|---|
| 0 | Modifier: бит 1 — левый Shift (0x02), бит 5 — правый Shift (0x20) |
| 1 | Reserved (всегда 0x00) |
| 2–7 | Key codes: коды нажатых клавиш (0 = не нажата) |
Ручной разбор первого report-а:
Bytes: 00 00 1a 00 00 00 00 00
│ │ └─ key code 0x1a = 26
│ └─ reserved
└─ modifier = 0x00 (Shift не нажат)
KEYMAP[0x1a] = ('w', 'W') → modifier=0, берём индекс [0] → 'w'
Второй report — Shift-символ:
Bytes: 02 00 2f 00 00 00 00 00
│ │ └─ key code 0x2f = 47
│ └─ reserved
└─ modifier = 0x02 (левый Shift)
KEYMAP[0x2f] = ('[', '{') → modifier с Shift → берём индекс [1] → '{'
Важная деталь: когда клавиша удерживается, одинаковый report приходит несколько раз. Нельзя добавлять символ повторно — нужно отслеживать, какие клавиши уже были приняты.
Report 4: 02 00 0e → 'K' (0x0e новая, добавляем)
Report 5: 00 00 0e → '-' (0x0e уже в pressed, пропускаем)
Decryption
Полная цепочка декодирования:
| Шаг | Действие | Результат |
|---|---|---|
| 1 | Отфильтровать пакеты типа C (completion) с xfer=1 (interrupt) от dev=8, ep=0x81, status=0 | 60 валидных HID reports |
| 2 | Из каждого report взять byte[0] (modifier) и bytes[2:] (key codes) | Послед-ность пар (modifier, keycode) |
| 3 | Сопоставить keycode с таблицей KEYMAP, учесть Shift | Символы строки |
| 4 | Пропустить коды, уже находящиеся в pressed (удержание) | Дедуплицированная строка |
| 5 | Применить [BKSP] — удалять предыдущий символ | Финальный текст |
Результат: ws{Keyb0ard_is_@_ch3at}
Как работает @: key code 0x1f = 51 при shift=0x02 → KEYMAP[0x1f][1] = @ (аналог Shift+2 на US-раскладке).
Root cause задачи. usbmon захватывает USB-трафик до Any шифрования на уровне ОС. Клавиатура передаёт физические нажатия в открытом виде — поэтому любой, кто получил дамп устройства, может восстановить весь напечатанный текст.
Правильный подход для защиты:
- Использовать зашифрованный ввод (USB-клавиатуры с аппаратным шифрованием).
- Ограничивать доступ к
/dev/usbmon*— только root или специальная группа. - Применять full-disk encryption, чтобы даже физический доступ к устройству не давал дамп в открытом виде.
- На чувствительных машинах — использовать экранные клавиатуры или одноразовые пароли.
Automation
import struct
from scapy.all import PcapNgReader
USBMON_FMT = "<QBBBBHccqiiII8siiII"
USBMON_SIZE = struct.calcsize(USBMON_FMT)
# Таблица соответствия USB HID key code → (символ, Shift+символ)
KEYMAP = {
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", ")"),
0x28: ("\n", "\n"),
0x2A: ("[BKSP]", "[BKSP]"),
0x2B: ("\t", "\t"),
0x2C: (" ", " "),
0x2D: ("-", "_"), 0x2E: ("=", "+"),
0x2F: ("[", "{"), 0x30: ("]", "}"), 0x31: ("\\", "|"),
0x33: (";", ":"), 0x34: ("'", '"'), 0x35: ("`", "~"),
0x36: (",", "<"), 0x37: (".", ">"), 0x38: ("/", "?"),
}
def iter_usbmon_packets(path):
"""Читает pcapng и возвращает usbmon-пакеты в виде словарей."""
with PcapNgReader(path) as packets:
for packet in packets:
raw = bytes(packet)
if len(raw) < USBMON_SIZE:
continue
fields = struct.unpack(USBMON_FMT, raw[:USBMON_SIZE])
yield {
"type": chr(fields[1]), # 'S' submit / 'C' completion
"xfer_type": fields[2], # 1 = interrupt
"epnum": fields[3],
"devnum": fields[4],
"busnum": fields[5],
"status": fields[10],
"len_cap": fields[12],
"data": raw[USBMON_SIZE:USBMON_SIZE + fields[12]],
}
def decode_hid_keyboard(reports):
"""Конвертирует список 8-байтовых HID reports в строку."""
text = []
pressed = set() # отслеживаем удержанные клавиши, чтобы не дублировать символ
for report in reports:
if len(report) != 8:
continue
modifier = report[0]
shift = bool(modifier & 0x22) # биты левого и правого Shift
keys = [code for code in report[2:] if code] # активные key codes
for key in keys:
if key in pressed: # клавиша уже была нажата — пропуск
continue
mapping = KEYMAP.get(key)
if not mapping:
text.append(f"[0x{key:02x}]")
continue
char = mapping[1] if shift else mapping[0]
if char == "[BKSP]":
if text:
text.pop() # Backspace убирает последний символ
else:
text.append(char)
pressed = set(keys) # обновляем состояние удержания
return "".join(text)
def main():
reports = []
for packet in iter_usbmon_packets("task2_final.pcapng"):
if packet["type"] != "C": # только completion (ответ устройства)
continue
if packet["xfer_type"] != 1: # только interrupt transfer
continue
# фильтр по конкретному устройству и endpoint-у
if packet["busnum"] != 1 or packet["devnum"] != 8 or packet["epnum"] != 0x81:
continue
if packet["status"] != 0: # пропускаем ошибочные транзакции
continue
reports.append(packet["data"])
text = decode_hid_keyboard(reports)
print(text)
if __name__ == "__main__":
main()
python3 solve.py
# ws{Keyb0ard_is_@_ch3at}
Key Takeaways
usbmon — это тоже трафик. pcapng содержит не только сетевые пакеты, но и USB-шину. Рефлекс «ищу HTTP/DNS» блокирует решение: сначала надо спросить «что за канал?», а не «что за протокол?»
HID Keyboard Report — открытый формат. 8-байтовый стандарт HID описывает нажатия в открытом виде. Имея дамп USB-шины, восстановить весь напечатанный текст можно за 50 строк Python — без взлома и без пароля.
Ловушка задачи — дублирование символов. Когда клавиша удерживается, одинаковый report приходит несколько раз. Без отслеживания
pressed-состояния вместоflag{получаетсяfffflllaaaggg{. Первые попытки участников выдают нечитаемую строку именно из-за этой ошибки.
