Dumping the State of All Keys on a USB Keyboard: A Low-Level Linux Adventure

Dumping the State of All Keys on a USB Keyboard: A Low-Level Linux Adventure
Have you ever wondered what’s happening under the hood when you press a key on your USB keyboard? What if you could peek into the raw data being sent from the keyboard to your computer? In this blog post, we’ll dive deep into the world of USB HID (Human Interface Device) protocols and write a low-level Linux program to dump the current state of all keys on a USB keyboard. No high-level abstractions—just raw, unfiltered access to the hardware.
The Problem: Reading Keyboard State Without Events
When you press a key on your keyboard, the operating system processes it as an event. These events are convenient for most applications, but what if you want to know the current state of all keys—not just the ones that triggered events? For example:
- Which keys are currently pressed?
- What’s the state of modifier keys (Shift, Ctrl, Alt)?
- What about the LED states (Caps Lock, Num Lock, Scroll Lock)?
This is a common challenge for:
- Security researchers analyzing keyboard input.
- Embedded developers debugging USB devices.
- Curious hackers who want to understand how USB keyboards work.
But there’s a catch: If a key was pressed while the system was off, the Linux kernel won’t generate any event unless another key is pressed. This means you can’t rely on the kernel’s input subsystem to detect keys that were pressed before the system booted. To solve this, we need to bypass the high-level input subsystem and interact directly with the USB keyboard at the lowest level possible.
The Solution: Using `libusb` to Query the Keyboard
We’ll use the libusb
library to interact with the USB keyboard directly. Here’s what we’ll do:
- Enumerate all USB devices to find keyboards.
- Identify HID interfaces on those devices.
- Send a
HID GET_REPORT
request to retrieve the current input report (key states). - Decode the input report to determine which keys are pressed.
The Code: Dumping Key States
Below is the C program that does all the heavy lifting. It uses libusb
to interact with USB devices and retrieves the input report for all connected keyboards.
#include <stdio.h>
#include <stdlib.h>
#include <libusb-1.0/libusb.h>
// HID GET_REPORT request
#define HID_GET_REPORT 0x01
#define HID_REPORT_TYPE_INPUT 0x01
// Function to check if a device is a HID keyboard
int is_hid_keyboard(libusb_device *device) {
struct libusb_device_descriptor desc;
int ret = libusb_get_device_descriptor(device, &desc);
if (ret < 0) {
fprintf(stderr, "Failed to get device descriptor\n");
return 0;
}
// Check if the device is a HID device
if (desc.bDeviceClass == LIBUSB_CLASS_PER_INTERFACE) {
struct libusb_config_descriptor *config;
ret = libusb_get_config_descriptor(device, 0, &config);
if (ret < 0) {
fprintf(stderr, "Failed to get config descriptor\n");
return 0;
}
for (int i = 0; i < config->bNumInterfaces; i++) {
const struct libusb_interface *interface = &config->interface[i];
for (int j = 0; j < interface->num_altsetting; j++) {
const struct libusb_interface_descriptor *iface_desc = &interface->altsetting[j];
if (iface_desc->bInterfaceClass == LIBUSB_CLASS_HID) {
libusb_free_config_descriptor(config);
return 1; // This is a HID device
}
}
}
libusb_free_config_descriptor(config);
}
return 0; // Not a HID device
}
// Function to get the input report from a HID keyboard
void get_input_report(libusb_device_handle *handle) {
unsigned char input_report[8]; // Most keyboards use 8-byte input reports
int ret = libusb_control_transfer(
handle,
LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE,
HID_GET_REPORT,
(HID_REPORT_TYPE_INPUT << 8) | 0x00, // Report Type (Input) and Report ID (0)
0, // Interface
input_report,
sizeof(input_report),
1000 // Timeout in milliseconds
);
if (ret < 0) {
fprintf(stderr, "Failed to get input report: %s\n", libusb_error_name(ret));
} else {
printf("Input Report:\n");
for (int i = 0; i < ret; i++) {
printf("%02x ", input_report[i]);
}
printf("\n");
}
}
int main() {
libusb_device **devices;
ssize_t count;
int ret;
// Initialize libusb
ret = libusb_init(NULL);
if (ret < 0) {
fprintf(stderr, "Failed to initialize libusb: %s\n", libusb_error_name(ret));
return 1;
}
// Get the list of USB devices
count = libusb_get_device_list(NULL, &devices);
if (count < 0) {
fprintf(stderr, "Failed to get device list: %s\n", libusb_error_name((int)count));
libusb_exit(NULL);
return 1;
}
// Iterate through all devices
for (ssize_t i = 0; i < count; i++) {
libusb_device *device = devices[i];
// Check if the device is a HID keyboard
if (is_hid_keyboard(device)) {
struct libusb_device_descriptor desc;
ret = libusb_get_device_descriptor(device, &desc);
if (ret < 0) {
fprintf(stderr, "Failed to get device descriptor\n");
continue;
}
printf("Found HID keyboard: %04x:%04x\n", desc.idVendor, desc.idProduct);
// Open the device
libusb_device_handle *handle;
ret = libusb_open(device, &handle);
if (ret < 0) {
fprintf(stderr, "Failed to open device: %s\n", libusb_error_name(ret));
continue;
}
// Detach the kernel driver (if attached)
if (libusb_kernel_driver_active(handle, 0) == 1) {
ret = libusb_detach_kernel_driver(handle, 0);
if (ret < 0) {
fprintf(stderr, "Failed to detach kernel driver: %s\n", libusb_error_name(ret));
libusb_close(handle);
continue;
}
}
// Claim the interface
ret = libusb_claim_interface(handle, 0);
if (ret < 0) {
fprintf(stderr, "Failed to claim interface: %s\n", libusb_error_name(ret));
libusb_close(handle);
continue;
}
// Get the input report
get_input_report(handle);
// Release the interface
libusb_release_interface(handle, 0);
// Reattach the kernel driver (if detached)
libusb_attach_kernel_driver(handle, 0);
// Close the device
libusb_close(handle);
}
}
// Free the device list
libusb_free_device_list(devices, 1);
// Clean up libusb
libusb_exit(NULL);
return 0;
}
How It Works
-
Device Enumeration:
- The program lists all USB devices and identifies HID keyboards by checking their interface class.
-
Opening the Device:
- For each HID keyboard, the program opens the device and detaches the kernel driver (if necessary).
-
Claiming the Interface:
- The program claims the HID interface to communicate directly with the device.
-
Sending `HID GET_REPORT`:
- The program sends a `GET_REPORT` request to retrieve the input report, which contains the current state of all keys.
-
Decoding the Input Report:
- The input report is printed in hexadecimal format. Each byte corresponds to a specific key or modifier.
Running the Program
- Install
libusb
:
sudo apt install libusb-1.0-0-dev
- Compile the program:
gcc -o dump_keys dump_keys.c -lusb-1.0
- Run the program with root privileges:
sudo ./dump_keys
Example Output
For a keyboard with the F9 key pressed, the output might look like this:
Found HID keyboard: 046d:c31c
Input Report:
00 00 42 00 00 00 00 00
This means:
- No modifier keys are pressed (`00`).
- The F9 key is pressed (`42`).
- No other keys are pressed (`00 00 00 00 00`).
Why This Matters
This low-level approach gives you complete control over the USB keyboard, allowing you to:
- Debug USB devices.
- Analyze keyboard input for security research.
- Build custom keyboard firmware or drivers.
Next Steps
- Experiment with different keyboards and observe their input reports.
- Extend the program to decode LED states or handle multiple keyboards simultaneously.
- Dive deeper into the USB HID specification to understand more complex devices.
Happy hacking! Let us know if you have any questions or need further assistance. 🚀