Skip to content

Instantly share code, notes, and snippets.

@TheNotary
Created September 13, 2024 21:58
Show Gist options
  • Save TheNotary/ce7844f9333bca7fafcc7409ca6d657b to your computer and use it in GitHub Desktop.
Save TheNotary/ce7844f9333bca7fafcc7409ca6d657b to your computer and use it in GitHub Desktop.
I Accidentally Discovered the Stuxnet of Keyboards while Writing a Userspace HID Driver

I Accidentally Discovered the Stuxnet of Keyboards while Writing a Userspace HID Driver

Before we get into the secret codes I stumbled upon that are seemingly capable of mass destruction against a boutique mechanical keyboard company's product line... I want to explain my background and how I even wound up reverse engineering a keyboard and writing my own driver for it in C++. Most of the time, my professional work is done at a much higher level while engaged working on digital products. But lately I've been excited to be working on a Go server to handle the backend traffic for an upcoming multiplayer scavenger/ action title. Researching the real-time networking stuff has entailed reading and watching a lot of networking content authored by C++ gurus, with slides of C++ code cropping up left and right that only serve to raise more questions about C++ syntax than they answered about handling latency. Tired of the uncertainty, I finally decided to deep-dive into C++ x86-64 software engineering. And to keep me excited about the work, I decided to build something vaguely useful; a keyboard driver for my XVX S-K80 that would blink the lights on the keyboard and perhaps display an image on its LCD screen.


x32dbg-cpu-state

Writing it took a lot longer than I expected. I assumed this would be a three, maybe four day adventure, but the coding spanned over a week. I've done a reasonable amount of C in my life, but I've always tended to stay away from the abstractions that C++ provides as that most of my C/C++ work has been towards building firmware for microcontrollers where you really can't afford even simple niceties like arrays that can grow dynamically. You can get away with it every once in a while, but when you run out of memory, very confusing behaviors emerge from the microcontroller that are challenging to debug. With the comforting knowledge that I was coding for a machine that has over 100 gigs of ram (I really got into CPU inference in 2023...), I took every opportunity possible to get fancy with my C++ --did you know C++ began supporting destructuring assignments in 2017? Wild!

Observing the USB Messages

Before I could get into C++ abstractions, I needed to first observe the communication between my keyboard and the computer. There's a million ways for USB devices to exchange information with the OS, and so at first I wasn't sure what to look for. I at first imagined that the RGB controls might operate over serial, but it turns out it's all done over HID messages. This is likely an industry standard for many reasons but most salient to me is that we're already going through the HID protocol so there's no sense in broadening the tech stack with other communication protocols.

I was able to install the official drivers and monitor the messages sent to the device using Wireshark, a product more widely known for its ability to capture and filter network traffic. Once I saw the USB messages being sent from the official software to the device as I changed color settings, I knew it was just a matter of time before I was controlling it through my own software.


wireshark-messages

Observing the Windows API Calls

Seeing the messages wasn't enough though. I wanted to make sure I was interacting with the keyboard exactly how the official software was. Their drivers were compiled into a 32-bit windows binary. It's been a while since I've reverse engineered a windows binary, but the safety of my keyboard was on the line (foreshadowing).

I installed x32dbg.exe to use as my debugger. You use an x86-64 debugger to peek into the internal state of the CPU and also get a good look at the memory as it pertains to the process you're debugging. With a debugger attached to the running process, you can actually watch the CPU as it executes the program, instruction by instruction --and you don't need the source code at all. The only problem is that the instructions are all in machine code.


x32dbg-cpu-state

It's drastically more time-consuming getting to the bottom of what even a small snippet of a program is doing when all you have is x86 instructions to look at. And to make matters worse, programs are massive, especially these days so even finding the interesting part of the program to analyze would take ages if you were stepping through the program instruction-by-instruction. To find the interesting spots fast, the general approach is to set a breakpoint on an external function call that is of interest to you. Then when the application starts doing the interesting thing, it will pause the program and let you see that area of machine code.

After a quick dig through the Windows API, I found a handful of likely suspects to break on. The winners are listed here:

  • SetupDiEnumDeviceInterfaces
  • SetupDiGetDeviceInterfaceDetail
  • HidD_GetAttributes
  • CreateFile
  • CloseHandle

These are related to addressing the keyboard, getting a handle to it.

For sending data to the device, these were my candidates:

  • HidD_SetFeature
  • DeviceIoControl
  • WriteFile

x32dbg has a "symbols" tab that can show you what imported modules (proprietary DLLs as well as the Windows API) a program uses, but be careful, because there's no guarantee that it catches everything if dynamic imports are used (as they were in my case). To place breakpoints on those function calls, I just looked them up in their various .dll files (kernel, hid, and setupapi.dll) and sure enough the program "broke" on my breakpoint after I had the official software turn on a key. Most importantly, it was paused with a call stack that highlighted the most interesting parts of the program.


x32dbg-symbols

As an aside, x32dbg.exe was indispensable in showing me open handles of my process, which was how I narrowed down which "USB device path" was the correct one to be sending messages. For brevity, I'll simply link off to this log breakpoints reference and leave it as an exercise to the curious to imagine how I logged out all File handles that were closed, and all files handles that were opened to pinpoint the USB device handle that the official drivers were using (it was the only file handle opened that was not closed throughout the program's bootup).


x32dbg-handles

"Improving" Upon the Existing Design

After a bit of digging, I had enough information to begin implementing my drivers in C++. And here's where things got a little unexpected. I was analyzing the messages my computer was sending, and after a little C++ coding, I was able to reproduce what the official drivers were doing to turn on the keyboard's LED. But I didn't like the protocol it was using.


wireshark-annotated

While it was very easy to interpret the data as KeyIds and RGB values, it was also tremendously wasteful. To turn on 1 LED, the program was sending 13 messages to the device each bearing 65 bytes. And if that wasn't bad enough, the official drivers demonstrated that a sleep statement was required between messages, which meant toggling a key's state wasn't at all an instantaneous process.

From my analysis of the keyboard's message traffic, I could see that the LED values payload consisted of 9 packets. And each packet included 15 keys. If I only wanted to change one key, couldn't I just send one of these packets? What could go wrong with testing this out?

So I set up my C++ program to only send the first of the 9 official packets. And... it actually worked fine. I was excited, and began to doubt the fragility of the device. But I didn't feel completely comfortable about this. I had never captured this sequence of packets before from the official drivers. What if my keyboard was silently screaming in agony every time I toggled an LED in this way? I continued researching, pouring over the packets and concluded that it must work in this way...


diagram-hl

To instruct a key's LED to change its color:

  1. Send 2 header packets that indicate we're going to bulk set the keyboard's LED values
  2. Send X payload packets that consist of the KeyIDs and their RGB values
  3. Send 2 footer packets, concluding the LED change operation

That all makes sense as a protocol, but I looked closely at the header packet and found a mysterious 'X' value encoded in one of its bytes. I thought, "Oh my, I'm telling you, the keyboard, how many payload packets to get ready for, aren't I? And when I only send you the one to save time, you really find that uncomfortable, don't you? You want me to change you to an 0x01, don't you?"


wireshark-xbyte

And so I specially crafted a header packet with an 0x01 byte in it. Here's where I was a little worried, but hey, it was the same number as the number of payload packets being sent. Surely this isn't a coincidence. If it were, it would be bonkers, the byte could have been any value between 0 and 255; there's only a 0.4% chance this is a coincidence. After mulling it over for quite a while, I realized that I needed to send my specially crafted packets to my keyboard or I wouldn't sleep very well ever again.

I compiled and ran my program... and once again, I was rewarded with blinking keyboard lights. But something odd was happening. Sometimes the keys would turn off correctly, but in some cases, the keys would stay on. As development continued, I eventually reverted to sending all of the payload packets again, and all was working fine-ish... except I forgot to set that special byte back to its original X value. When I realized this, I was dumbfounded. Why was it working still and changing all of the LED values after I went through all that trouble to tell it to only care about the first packet?

Now I was deeply perplexed. Was that byte meant to be an X all along? Was this 0.4% probability really just a coincidence? "It can't be, that's just not how math works!" And so I did the only thing I could do, I conducted an experiment where I set that byte to the maximum value possible, 0xFF. What was my hypothesis? Well if it was set to 0xff, and that byte was involved with setting the type of operation, then it's unlikely that 255 operations would have been defined, and so the keyboard would just do nothing despite being sent the payload data. And boy was I right about the keyboard doing nothing...

I set the mysterious 'X byte' (as I've come to call it) to FF and moments after sending the message to the keyboard, the LCD screen on the keyboard went out. Cool, an off button to my keyboard that I can trigger over software? Well yes and no: Yes because the keyboard was no longer on, and never would come online again; and NO because "Off button" implies there's also an "On button" counterpart which to the best of my understanding the keyboard does not have. After a few minutes of troubleshooting, I relented. The pointless built-in LCD screen and I stared back at one another, blankly. My beautiful keyboard was now a brick.

Luckily (after a lot of back and forth) the vendor was willing to help me out with a replacement --and shouldn't they, I mean they've built a beautiful looking product, but... it has a self-destruct button that can be triggered remotely by a non-root user. That's not exactly an ideal form to ship a product in. Or am I wrong?

Where should the line be between power users and misusers when it comes to tech gadgets like this? When I first told this story to a friend, he made the clever summary, "so you built a Stuxnet for keyboards?" which felt like a fitting description. When Siemens AG shipped their infamous centrifuge machines (which fell into embargoed hands) they shipped with a similar, albeit more technically complex to activate and perhaps more difficult to avoid self-destruct button. Was that a finished product or a WIP that should have had some better protections from loading code that would eventually destroy the product?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment