Skip to main content

Keylogger

Ссылка на задание http://f8tasks.ru/challenges#Keylogger-24

CategoryForensics
DifficultyMedium
TechniqueUSB HID Keyboard Analysis
Flagws{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 endpoint 0x81 с 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 байт:

БайтНазначение
0Modifier: бит 1 — левый Shift (0x02), бит 5 — правый Shift (0x20)
1Reserved (всегда 0x00)
2–7Key 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=060 валидных 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

  1. usbmon — это тоже трафик. pcapng содержит не только сетевые пакеты, но и USB-шину. Рефлекс «ищу HTTP/DNS» блокирует решение: сначала надо спросить «что за канал?», а не «что за протокол?»

  2. HID Keyboard Report — открытый формат. 8-байтовый стандарт HID описывает нажатия в открытом виде. Имея дамп USB-шины, восстановить весь напечатанный текст можно за 50 строк Python — без взлома и без пароля.

  3. Ловушка задачи — дублирование символов. Когда клавиша удерживается, одинаковый report приходит несколько раз. Без отслеживания pressed-состояния вместо flag{ получается fffflllaaaggg{. Первые попытки участников выдают нечитаемую строку именно из-за этой ошибки.