Rogue XInput Capabilities Bug – Part 2


This is the continuation of Rogue XInput Capabilities Bug – Part 1

Right, let’s tie up loose ends and discover a proper fix!

I can’t read

Well, embarrassing things out of the way first; the major contributor to the issue was my inability to just steal copy over some simple lines from the good old ScpVBus source:

    // requested control transfer
case URB_FUNCTION_CONTROL_TRANSFER:
{
    // ignored; we are not interested in responding to control requests
    status = STATUS_UNSUCCESSFUL;
}
break;

Obviously failing any sort of requests targeted at the control endpoint will satisfy the driver stack when handled by ScpVBus and the user-mode XInput APIs return sane values. Well, perfect, just correct the status and we’re good to go. That was easy, case closed! Bye 👋

But, why tho…

Alright, of course this isn’t the full story, in reality I had to waste quite a few more hours on this topic because I was debugging the issue by dissecting the USB packets first before even looking at the old source… sometimes the easiest solution hides itself in plain sight 😑

Time to sniff some USB

So my path of discovering what’s going on and developing a reliable fix based on the findings was to go deep and have a detailed look – once again – how a real physical Xbox 360 Controller behaves when getting connected and and reaching operational state. I used two well-established tools for this task; the uncontested king of network protocol sniffers Wireshark (which will happily decode USB traffic when USBPcap is installed) and USBlyzer which spits out even more details like IRPs and involved drivers. We’ll see later on why Wireshark on its own wasn’t enough to shed light on the subject.

Wireshark session

Once Wireshark is ready to capture, plugging in the controller will result in quite a few logged packets. Thanks to the IDA disassembly we know to watch what’s happening on the control endpoint so we can go ahead and filter the output via the following expression:

usb.endpoint_address.number==0

This will still include stuff like GET DESCRIPTOR (happens on endpoint 0 as well) so we skip over that and inspect the URB_CONTROL packets (pardon the German UI, the relevant bits are in English):

Ok right from the start we get these two requests standing out. Why? Well, they’re lacking their expected answer counter-part where the device returns some data back to the host. This means that the requests failed. Unfortunately (at the time of this writing) Wireshark/USBPcap isn’t capable of reporting details about the failure. We’ll cover these later on. Let’s have a look at the next packet:

Yeah, some data! The controller reported a byte sequence too small to actually feed XINPUT_CAPABILITIES with meaningful properties:

0x31, 0x3F, 0xCF, 0xDC

So what do these stand for? Honestly, I got no idea. Rather anti-climactic, I know. I simply took this exact sequence and report it back in ViGEm when this request flies in. Bit of a redneck thing to do I admit but as long as it works 😅 This is the snippet of how that’s implemented:

switch (urb->UrbControlTransfer.SetupPacket[6])
{
case 0x04:
    //
    // Xenon magic
    // 
    COPY_BYTE_ARRAY(urb->UrbControlTransfer.TransferBuffer, P99_PROTECT({
        0x31, 0x3F, 0xCF, 0xDC
        }));
    status = STATUS_SUCCESS;
    break;

Now that this case is handled properly, what about the failing requests? Time for another tool!

USBlyzer session

The USBlyzer program is specifically crafted for in-depth USB analysis to a level that software-only solutions can provide. Likewise to Wireshark lets inspect the packets in question:

Ha! There it is, the URB Status: USBD_STATUS_STALL_PID. Da heck is that:

The device returned a stall packet identifier (defined for backward compatibility with the USB 1.0)

Uh, alright. STALL simply means error. So it looks like the XUSB driver expects these requests to fail and can then happily continue initializing the internal representation of XINPUT_CAPABILITIES. Fair enough. I won’t scrutinize this any further and just implement it in ViGEm and we should be good to go:

case 0x14:
    //
    // This is some weird USB 1.0 condition and _must fail_
    // 
    urb->UrbControlTransfer.Hdr.Status = USBD_STATUS_STALL_PID;
    status = STATUS_UNSUCCESSFUL;
    break;
case 0x08:
    //
    // This is some weird USB 1.0 condition and _must fail_
    // 
    urb->UrbControlTransfer.Hdr.Status = USBD_STATUS_STALL_PID;
    status = STATUS_UNSUCCESSFUL;
    break;

Which indeed fixed the problem! 😀

Conclusion

Got some lessons learned:

  • When porting over legacy code, take the time and double check
  • I can’t think of more 😁

Plus I think XUSB.SYS might actually leak kernel memory if control requests aren’t handled as expected. Might as well just be a missing RtlZermoMemory call in the XInput user-mode libraries, who knows. Some food for further investigations.

Liked it? Take a second to support nefarius on Patreon!