Our love for gaming alongside finding bugs led us back to the good ol’ question: Is it true that the more RGB colors you have (except for your gaming chair, of course), the more skill and power you’ll have over your opponents? Or could it put you at a disadvantage?
Since we are always interested in finding vulnerabilities, we have recently started to research third-party Linux kernel drivers, one of which was OpenRazer [0] — an open-source driver for gaming devices produced by Razer. Finding any vulnerabilities in such a product has a significant impact due to the vast number of users.
TL;DR
We have found a buffer overflow in the OpenRazer open-source kernel drivers, which caused a Denial of Service and possibly Elevation of Privileges (CVE-2022-29021, CVE-2022-29022, CVE-2022-29023).
During the research, we encountered a newly added feature to the Linux Kernel, which is a part of Fortify Source, that caused some weird behavior during the exploit development.
Hunting for Bugs
When you’re using Razer products in a Linux environment, you’ll probably end up using OpenRazer — “An entirely open source driver and user-space daemon that allows you to manage your Razer peripherals on GNU/Linux” or, in human words, an application that lets you customize/manage your Razer products.
One of the best places to look for vulnerabilities in Linux is in kernel modules. They are always privileged, and therefore, every bug in them is essentially a bug in the kernel. In addition, kernel modules are far less examined and tested, as they are not considered a part of the core kernel. The most important thing we need to figure out is how to interact with them and then how we can affect their behavior from an unprivileged user standpoint.
OpenRazer has multiple drivers — in other words, kernel modules (razreaccessory, razerkbd, razermouse and razerkraken). Since the drivers are open-sourced, it’s relatively easy to discover how we can interact with them, if at all.
There are multiple ways to conduct vulnerability research, including reversing of the binary, fuzzing or even looking at the plain code. Good thing we’re using Linux, where most of the drivers are open sourced (even Nvidia as of 5/2022!! [1]). Therefore, we’ve decided to statically check the source code to better understand how the drivers are implemented.
static struct hid_driver razer_kbd_driver = { .name = "razerkbd", .id_table = razer_devices, .input_mapping = razer_kbd_input_mapping, .probe = razer_kbd_probe, .remove = razer_kbd_disconnect, .event = razer_event, .raw_event = razer_raw_event, }; module_hid_driver(razer_kbd_driver);
From a quick glimpse, we can see that the drivers are implemented as USB-HID devices, which is a method of implementing USB devices that humans usually interact with, like keyboards, mice, etc.
static DEVICE_ATTR(game_led_state, 0660, razer_attr_read_mode_game, razer_attr_write_mode_game); static DEVICE_ATTR(macro_led_state, 0660, razer_attr_read_mode_macro, razer_attr_write_mode_macro); ... static DEVICE_ATTR(version, 0440, razer_attr_read_version, NULL); static DEVICE_ATTR(kbd_layout, 0440, razer_attr_read_kbd_layout, NULL); static DEVICE_ATTR(firmware_version, 0440, razer_attr_read_get_firmware_version, NULL); ... static DEVICE_ATTR(device_type, 0440, razer_attr_read_device_type, NULL); static DEVICE_ATTR(device_mode, 0660, razer_attr_read_device_mode, razer_attr_write_device_mode); static DEVICE_ATTR(device_serial, 0440, razer_attr_read_get_serial, NULL); ... static DEVICE_ATTR(matrix_effect_none, 0220, NULL, razer_attr_write_mode_none); ... static DEVICE_ATTR(matrix_custom_frame, 0220, NULL, razer_attr_write_matrix_custom_frame); ...
Each driver exposes its functionalities via multiple attribute files [2]. The attribute files reside in sysfs, and each one of them matches a functionality implemented in the driver. In our case, the attribute files are created by calling device_create_file with attributes declared by the DEVICE_ATTR macro, which are:
- name – Represents the attribute file’s name
- mode – The permissions for attribute file
- show – Function for reading from the attribute file
- store – Function for writing to the attribute file
In the razerkbd driver, for example, there are three types of attribute files: files with only read permissions (0440), only write permissions (0220), and both read and write permissions (0660).
We first should check the attribute files with write permissions. The reason is that the buffer that we write to the attribute file is normally used in the implemented function in the driver (the store function). As a result, the potential for bugs is higher than when we don’t have write permissions to the attribute file.
The Vulnerability
One of the attribute files with write permissions of razerkbd is matrix_custom_frame. From the Github wiki page of OpenRazer, matrix_custom_frame lets us set the colors of keyboard rows by passing a row index, column start, column stop and RGB values to the corresponding keys. For example, this will set the left “Ctrl,” “WinKey” and “alt” keys to blue:
BLUE = '\x00\x00\xff' with open('matrix_custom_frame', 'wb') as f: f.write(b'\x05\x00\x03' + BLUE * 4) with open('matrix_effect_custom', 'wb') as f: f.write('\x01')
The general flow of writing to matrix_custom_frame is the following:
static DEVICE_ATTR(matrix_custom_frame, 0220, NULL, razer_attr_write_matrix_custom_frame); // 1 ... static ssize_t razer_attr_write_matrix_custom_frame(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct usb_interface *intf = to_usb_interface(dev->parent); struct usb_device *usb_dev = interface_to_usbdev(intf); struct razer_report report = {0}; size_t offset = 0; unsigned char row_id; unsigned char start_col; unsigned char stop_col; unsigned char row_length; while(offset < count) { ... row_id = buf[offset++]; // 2 start_col = buf[offset++]; stop_col = buf[offset++]; row_length = ((stop_col+1) - start_col) * 3; ... switch(usb_dev->descriptor.idProduct) { case USB_DEVICE_ID_RAZER_ORNATA: ... case USB_DEVICE_ID_RAZER_CYNOSA_CHROMA_PRO: report = razer_chroma_extended_matrix_set_custom_frame(row_id, start_col, stop_col, (unsigned char*)&buf[offset]); // 3 break; ... razer_send_payload(usb_dev, &report); // 8 // *3 as its 3 bytes per col (RGB) offset += row_length; } return count; }
1. razer_attr_write_matrix_custom_frame receives the buffer that we’re writing to matrix_custom_frame as the buf argument.
2. Assigns row_id start_col and stop_col as the first, second and third bytes from the buf buffer, which we have complete control
over, as they are user input.
struct razer_report razer_chroma_extended_matrix_set_custom_frame(unsigned char row_index, unsigned char start_col, unsigned char stop_col, unsigned char *rgb_data) { return razer_chroma_extended_matrix_set_custom_frame2(row_index, start_col, stop_col, rgb_data, 0x47); }
3. Passes the row_id, start_col, stop_col and the buf buffer, starting from the offset of the RGB data to razer_chroma_extended_matrix_set_custom_frame that calls
razer_chroma_extended_matrix_set_custom_frame2.
struct razer_report { unsigned char status; union transaction_id_union transaction_id; unsigned short remaining_packets; unsigned char protocol_type; unsigned char data_size; unsigned char command_class; union command_id_union command_id; unsigned char arguments[80]; unsigned char crc; unsigned char reserved; }; struct razer_report razer_chroma_extended_matrix_set_custom_frame2(unsigned char row_index, unsigned char start_col, unsigned char stop_col, unsigned char *rgb_data, size_t packetLength) { const size_t row_length = (size_t) (((stop_col + 1) - start_col) * 3); // 4 const size_t data_length = (packetLength != 0) ? packetLength : row_length + 5; struct razer_report report = get_razer_report(0x0F, 0x03, data_length); // 5 report.transaction_id.id = 0x3F; report.arguments[2] = row_index; // 6 report.arguments[3] = start_col; report.arguments[4] = stop_col; memcpy(&report.arguments[5], rgb_data, row_length); // 7 return report; }
4. Calculates the row_length for the RGB values (each RGB value is three bytes long) based on start_col and stop_col.
5. Creates a new razer_report via get_razer_report that one of its members is a char array named arguments with the length of 80.
6. Puts the row_id (row_index), start_col and stop_col to the arguments buffer.
7. Copies row_length bytes from the buf buffer (rgb_data) to the arguments buffer.
8. Sends the new razer_report to the USB device.
Can you spot the problem here?
Since we have complete control over row_length, which is then being used as the copy size to the arguments buffer in the memcpy, by providing a size bigger than 80 we can overflow the arguments buffer, as there is no validation whatsoever, thus gaining a classic case of a buffer overflow :).
If we check the other drivers in OpenRazer, we can see that both razermouse and razeraccessory implemented matrix_custom_frame the same way. Hence, they suffer from the same bug.
Reversing
To understand which code actually runs, we need to reverse engineer the binary, as the compiler could add/change stuff from the original source code.
Our first step is to review how the kernel module was compiled and make sure we’ve compiled it with debug symbols for easier reversing (Check Environment Setup).
After reversing the relevant parts of the driver, the decompiled code looked quite similar compared to the source code, but an odd test was added that compares row_length with 0x4D and calls fortify_panic if row_length is bigger than 0x4D:
What is this test, and what even is 0x4D?
It’s apparent that 0x4D aka 77 is the space left in the struct razer_report starting from the offset 5 in the arguments buffer (which is the destination of the memcpy), so potentially, we could override both crc and reserved fields.
However, we know that the copy size, row_length, in the memcpy can’t be 76 or 77 bytes long because they are not a multiplication of 3 (code snippet 5-4).
Therefore, we can’t overwrite the crc and the reserved fields of the structure.
Usually, when we compile the drivers without FORTIFY_SOURCE enabled on the machine, the odd test wasn’t there. So, what even is fortify_panic?
Fortify Source
The added compile-time function fortify_panic made us think that it is a part of some mitigation (well, the name quite says it), and indeed, after a quick deep into the fountain of knowledge — Google — that mitigation was none other than Fortify Source.
Essentially, Fortify Source is a feature originally from glibc that provides compile-time and run-time buffer overflow checks for potentially dangerous functions like memcpy and strcpy. Unlike glibc, the Linux kernel implementation covers buffer reads in addition to writes.
At its core, FORTIFY_SOURCE uses the compiler’s __builtin_object_size() to determine the available size at a target address based on the compile-time known structure layout.
__builtin_object_size() has multiple modes, and two of them are relevant to us:
Mode 0 – used for checking the outer bounds of the structure.
Mode 1 – used for checking the inner bounds of members in the structure.
For example [3]:
struct object { u16 scalar1; /* 2 bytes */ char array[6]; /* 6 bytes */ u64 scalar2; /* 8 bytes */ u32 scalar3; /* 4 bytes */ u32 scalar4; /* 4 bytes */ } instance;
__builtin_object_size(instance.array, 0) == 22, since the remaining size of the enclosing structure starting from the field array is 22 bytes (6 + 8 + 4 + 4). __builtin_object_size(instance.array, 1) == 6, since the remaining size of the specific field array is 6 bytes.
The initial implementation of FORTIFY_SOURCE in kernel v4.13 used __builtin_object_size() with mode 0 because there were many cases of both strcpy and memcpy functions being used to write (or read) across multiple fields in a structure. Indeed, this prevented overflows from reaching beyond the end of the structure; however, since structures are a continuous area in the memory, it didn’t protect against overwriting of neighbor structure fields.
In kernel v5.11, __builtin_object_size() with mode 1 was turned on for the strcpy family of functions, and in kernel v5.18, __builtin_object_size() with mode 1 was turned on for the memcpy family of functions [4].
Therefore, compiling a kernel module on a machine with FORTIFY_SOURCE enabled (CONFIG_FORTIFY_SOURCE) will have this mitigation enabled by default.
__FORTIFY_INLINE void *memcpy(void *p, const void *q, __kernel_size_t size) { size_t p_size = __builtin_object_size(p, 0); size_t q_size = __builtin_object_size(q, 0); if (__builtin_constant_p(size)) { // The size is known at compile-time if (p_size < size) __write_overflow(); if (q_size < size) __read_overflow2(); } if (p_size < size || q_size < size) fortify_panic(__func__); return __underlying_memcpy(p, q, size); }
Fortify Source checks the code both in compile and run time:
For instance, in the memcpy check, if the size is known at compile-time, there will be an error right away, which, of course, causes compilation failure.
Otherwise, if the size is determined at run time, a call to fortify_panic will be triggered before out-of-bounds copy operation.
void fortify_panic(const char *name) { pr_emerg("detected buffer overflow in %s\n", name); BUG(); }
So, what does fortify_panic do exactly? If we look at the source code in the kernel, we can see that it calls the BUG macro, which calls unreachable and crashes the driver, which is way better than letting the attacker arbitrarily write code in the kernel.
Exploitation
To exploit the vulnerability explained above, we must send a specially constructed buffer, which will cause row_length to be bigger than 80. To do so, we can set start_col to be 0x00 and stop_col to have a larger value than 0x18, so row_length will exceed the size of the arguments buffer and overflow the buffer.
file_path = '/sys/bus/hid/devices/0003:1532:021E.0004/matrix_custom_frame' payload = b'\x00\x00\x19' payload += b'\x41' * 80 def crash(): with open(file_path, 'wb') as f: f.write(payload) if __name__ == '__main__': crash()
Running the code above crashes the driver, causing a DoS. The impact may be more severe if the driver was not compiled with FORTIFY_SOURCE enabled. In this case, the attacker can chain this vulnerability with an information leak vulnerability, bypassing KASLR and stacking canary mitigations (if present). This leads to kernel control-flow hijack, which results in code execution in kernel space. Even without leveraging an information leak vulnerability, exploiting the bug in the absence of FORTIFY_SOURCE would cause a system crash.
Environment Setup
All the work we’ve done was on an Ubuntu 20.04 kernel 5.13.
To install OpenRazer, you could either follow the steps in the Download section on the OpenRazer page or compile and install the drivers directly from the Github page. After installing, ensure the specific kernel module is loaded (dmesg).
To check whether the VM recognizes the Razer USB device, run “lsusb,” for example:
If you can’t see the Razer USB device, make sure the device is connected to the VM (USB and Bluetooth). If you can’t see the USB device, you might need to add the following to the “.vmx” file of the VM and restart the machine:
usb.generic.allowHID = “TRUE” usb.generic.allowLastHID = “TRUE
After connecting the USB device successfully, you should have a new hid device under /sys/bus/hid/devices for the connected device containing all the driver’s device files.
# Driver compilation driver: @echo -e "\n::\033[32m Compiling OpenRazer kernel modules\033[0m" @echo "========================================" $(MAKE) -C $(KERNELDIR) M=$(DRIVERDIR) modules EXTRA_CFLAGS="-g -DDEBUG"
To simplify debugging, add debug symbols to the Makefile by adding EXTRA_CFLAGS=”-g -DDEBUG”.
Summary
As part of this third-party Linux kernel drivers research, we’ve analyzed an open-source Linux driver for razer devices — OpenRazer. We’ve found a buffer overflow vulnerability that could be exploited to a Denial of Service and possibly Elevation of Privileges during our examination.
While developing the exploit, we encountered a newly added feature to the Linux Kernel that is a part of the Fortify Source mitigation, which added strict bounds checking for memcpy.
This mitigation made exploiting vulnerabilities based on an out-of-bound memcpy operation beyond a denial of service unlikely, thus, making our life much safer :).
Patch
As part of the disclosure, we’ve submitted a patch for the described vulnerabilities, adding checks before memcpy to prevent any overflow, which was merged soon after and was a part of the new release.
Disclosure
- April 3, 2022 – Bug found
- April 4, 2022 – Reported to the OSS maintainer
- April 8, 2022 – PR for fixing the vulnerable code was open
- April 9, 2022 – Merged and a new release of OpenRazer was published (3.3.0)
- May 20, 2022 – CVEs granted [5]:
- razerkbd – CVE-2022-29021
- razeraccessory – CVE-2022-29022
- razermouse – CVE-2022-29023
References
[0] OpenRazer – https://openrazer.github.io/
[1] Nvidia’s open source – https://github.com/NVIDIA/open-gpu-kernel-modules
[2] sysfs attributes – https://www.kernel.org/doc/html/latest/filesystems/sysfs.html#attributes
[3] __builtin_object_size mode example – https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f68f2ff91512c199ec24883001245912afc17873
[4] Patch set for inner bounds memcpy checks – https://lwn.net/Articles/864521/
[5] Granted CVEs – CVE-2022-29021 CVE-2022-29022 CVE-2022-29023