TL;DR
- I discovered multiple bugs in OEM vendors for peripheral devices, which affected many users of these OEM vendors (Razer, EVGA, MSI, AMI).
- Many of the vulnerabilities originated in a well-known vulnerable driver that often is called WinIO / WinRing0.
- This blog post focuses on an interesting case of TOCTOU vulnerability (CVE-2022-25637), alongside trivial exploitation.
Before reading this article, you might want to read Eran Shimony’s blog about vulnerabilities in WDM drivers. It will help you understand this article better as it’s a complementary piece to this article.
Intro – Where It All Started
Our story begins in September 2021, I wanted to tune the speed of the fans on my MSI computer at home to achieve +5 points for my gaming skills.
It’s known that MSI developed a handy-dandy tool called MSI Dragon Center, whose purpose was to retrieve information about the computer stats (i.e., GPU / CPU usage) and control hardware-related settings.
Unfortunately, it didn’t play nice (Many UI issues and slow loading time – at least from my experience), so I dug in, hoping to understand how the software interacts with the fan. I guessed that MSI uses a kernel driver. My inspections confirmed the theory that MSI uses a kernel driver to perform some of the functionality Dragon Center provides – the fan controlling function is done via WMI objects or vendor-specific APIs like NvAPI_GPU_SetCoolerLevels and isn’t implemented in the Dragon Center code. In addition, Dragon Center loads a driver called WinIO, which apparently isn’t related to the logic of the fan controlling. One thing led to another, and I began to look into the WinIo driver as it might pose an interesting attack surface.
WinIO is a well-known kernel driver developed by www.internals.com (the website is no longer available online but can be reached via archive.org – link ). WinIO driver library allows 32-bit and 64-bit Windows user-mode processes to directly access I/O ports, MSR registers, and physical memory, and it has been widely used by many vendors. As it’s with great power comes great responsibility, and the driver should only allow privileged users these functionalities.
However, in WinIo, the situation is different – any user can interact with it, including a sandboxed application.
WinIo could simply set a security descriptor over the device object to avoid low-privileged users from interacting with it, as in the following code snippet
DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { UNICODE_STRING DeviceName, SymbolicLink,sddlString; PDEVICE_OBJECT deviceObject; RtlInitUnicodeString(&DeviceName, L"\\Device\\testydrv"); RtlInitUnicodeString(&SymbolicLink, L"\\DosDevices\\testydrv"); RtlInitUnicodeString(&sddlString, L"D:P(A;;GA;;;SY)(A;;GA;;;BA)"); UNREFERENCED_PARAMETER(RegistryPath); // Create a device IoCreateDeviceSecure(DriverObject, 65535, &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &sddlString, NULL, &deviceObject); // Create a symbolic link so the user can access the device IoCreateSymbolicLink(&SymbolicLink, &DeviceName); // Populating driver's object dispatch table DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = TestyDispatchIoctl; ... ... DriverObject->DriverUnload = TestyUnloadDriver; return STATUS_SUCCESS; }
Figure 1: Applying SDDL to Device Object
The WinIo version that I found on my device was an early version of the driver (we suspect it’s WinIO version 2.0) that was extremely vulnerable even to the most basic vulnerabilities – a simple DeviceIoControl request could smash the stack. By sending an I/O request using DeviceIoControl with IOCTL code 0x80102040, we reach a memmove method.
__int64 __fastcall WinIoDispatch(PDEVICE_OBJECT pDeviceObject, IRP *Irp) { ... unsigned int Status; // ebx ULONG_PTR IOPM[2]; // [rsp+30h] [rbp-38h] BYREF struct tagPhysStruct PhysStruct; // [rsp+40h] [rbp-28h] BYREF struct tagPortStruct PortStruct; // [rsp+78h] [rbp+10h] BYREF ... switch ( CurrentStackLocation->MajorFunction ) { ... case 0xEu: DbgPrint("IRP_MJ_DEVICE_CONTROL"); LowPart = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart; switch ( LowPart ) { case 0x80102040: DbgPrint("IOCTL_WINIO_MAPPHYSTOLIN"); if ( !(_DWORD)InputBufferLength ) goto LABEL_11; memmove(IOPM, SystemBuffer, InputBufferLength); ... } } }
Figure 2: WinIo Dispatch function – Vulnerable memmove/memcpy
This memmove lacks any argument checking. More precisely, we control the length argument, which originated from the
SystemBuffer. As a result, we can easily corrupt the stack by specifying a length that is larger than the IOPM local-variable length. Thus, we can override local stack data, which is a classic buffer overflow scenario that can lead to overriding the caller’s return pointer, combined with using an ROP chain that would eventually lead to privilege escalation.
Yet, another bug exists, a privilege escalation via physical memory mapping, allowing us to have a strong R/W primitive.
... case 0x80102040: DbgPrint("IOCTL_WINIO_MAPPHYSTOLIN"); if ( !(_DWORD)InputBufferLength ) goto LABEL_11; memmove(IOPM, SystemBuffer, InputBufferLength); v14 = MapPhysicalMemoryToLinearSpace( (PHYSICAL_ADDRESS)IOPM[1], IOPM[0], (PVOID *)&PhysStruct.pvPhysAddress, (void **)&PhysStruct, (PVOID *)&PhysStruct.PhysicalMemoryHandle); if ( v14 >= 0 ) { memmove(SystemBuffer, IOPM, InputBufferLength); Irp->IoStatus.Information = InputBufferLength; } Irp->IoStatus.Status = v14; break; ...
Figure 3: Arbitrary Memory R/W functionalities in WinIO
At this point, a question comes to mind, could this codebase be used in other places\drivers?
Going for the Hunt – Finding Other Vulnerable Drivers
OK, folks, let’s take all the skills set we earned from hours of playing FPS games to seek after enemy team players and apply those to hunt vulnerable drivers!
I wrote a relativity simple query in VirusTotal, we found 114 matches of potential drivers that might share the same codebase as our vulnerable driver.
By taking a quick glance into some of the drivers’ disassembled code, it seemed that many vendors use the same vulnerable codebase of the WinIo driver.
I have noticed one in particular – Razer Synapse Service.sys.
Figure 4: Razer Synapse Service.sys VirusTotal results
Three Faulty Synapses
Since I’m using some of Razer products, Razer Synapse is installed on my machine.
Razer Synapse loads a few drivers, and one of them is the Razer Synapse Service.sys – WinIo driver with a different name. Usually, when the WinIo driver is loaded, no security restrictions are set to the device object. However, in this case, it has a restrictive security descriptor.
Figure 5: SDDL applied on Razer driver
At this point, one usually should give up on this driver, even if it’s faulty, because, in order to interact with this driver, you are required to have high privileges, which means you can already perform privileged actions.
In Windows, causing drivers to do naughty things isn’t considered a security boundary bypass if you start as admin+.
Since the driver doesn’t set a security descriptor, this must have been done elsewhere.
According to MSDN: “Security for device objects can be specified by an SDDL string that is placed in an INF file or passed to IoCreateDeviceSecure.”
As of that, we should look somewhere else, which is the INF file, but surprise-surprise, there is no INF file!
I have to say this is quite an odd case. We suspected that Razer Synapse Service.exe sets the SDDL to the device object created by the driver. We monitored the system in Procmon and noticed that this program is responsible for loading the Razer Synapse Service.sys driver.
Figure 6: “Razer Synapse Service.sys” is Prepared to Be Installed
We need to reverse engineer Razer Synapse Service.exe to understand where it applies the security descriptor. Luckily, it’s written in C#, which will make our reverse engineering endeavor easier since we can use a reflector.
We iterated through the module list to find out which module is responsible for loading the kernel driver. We decompiled the different modules back to C# (we used DnSpy) and proceeded by looking for any kind of communication made with the service control manager (SC manager).
We discovered that the module that is responsible for the matter is LibreHardwareMonitorLib (which is open-source).
If we look closely into the code, we can see something peculiar.
Figure 7: Are We in a Race?
We can see that in lines 11-14, the service tries to open a handle to the device object created by the driver, followed by setting a new security descriptor to it. But WAIT A SEC, this is not how it should be done, right? I mean, they used the correct methods for doing this from user-mode, but they should never have done it in the user-mode space in the first place.
As we stated earlier, applying the SDDL should be done in the kernel and upon device creation.
The fact it didn’t happen in the kernel space caused a short period that the device object holds a default security descriptor that allows low-privilege users to interact with the device object.
This is a classic case of time of check-time of use vulnerability. If we can exploit this short time frame for getting a handle on the device object, then we can abuse the vulnerabilities of WinIo.
Exploitation
The “Razer Synapse Service” is configured with Auto Start. Hence we can’t restart it at will from a low-privilege user standpoint. Is there something else we can do? The answer is yes, we can and we will. The secret behind it is to recreate the race condition without restarting the service.
It turns out that triggering this is relativity easy using the update mechanism that synapse3 provides.
Whenever a new update or a new plugin is being installed, the Razer Synapse Service will restart.
The restart process includes unloading the WinIo driver and then reloading it. Thus, allowing us to trigger the race condition. This is done by installing a new module, an operation that does not require privileges – The Synsapse3 supports modules like Alexa, Chroma Connect, Chroma Studio, Philips HUE, and more.
Figure 8: Module List – Synapse 3
If we choose to install one of the modules, the synapse3 process sends a command via a named pipe to Razer Central Service to install the selected module.
The RazerCentralService.exe starts the module installation, including stopping and starting the Razer Synapse Service, and thus unloading and loading the driver.
We created a POC that does the entire process – between the time the POC triggers the module installation, an infinite-while loop tries to open a handle to the device object using CreateFile API. We managed to open the handle to the device before the security descriptor was changed, in other words, we won the race. At this point, the fact the service changes the security descriptor does not matter, as we have a valid handle to the device object.
Now that we can interact with the device object freely, we can exploit some of the vulnerabilities of WinIo. In our POC, we exploited the MSR R/W primitive. The write MSR primitive allows us to override IA32_LSTAR MSR. This specific MSR holds the pointer to the kernel function that handles syscalls (KiSystemCall64Shadow). By overriding the function pointer, we can achieve arbitrary kernel code execution.
Kudos to @_xeroxz, and with the inspiration in mind, we developed an exploit of the MSR write primitive with ease using a tool we developed called msrexec.
Figure 9: Demo
Conclusion
This research started with me trying to mess with my machine’s fan and ended up exploiting a cool race condition that led us to run code in the kernel. Interestingly, one can play computer games and eventually find a bug or two. We believe many vulnerable drivers are out there waiting for a security researcher to find and (hopefully) report.
If you have any questions, feel free to send me a DM over Twitter.
Thanks for reading,
@OmerTsarfati
References:
- https://eclypsium.com/2019/08/10/screwed-drivers-signed-sealed-delivered/
- https://github.com/starofrainnight/winio
- https://github.com/LibreHardwareMonitor/LibreHardwareMonitor
Disclosure Timeline
- Jan 19, 2022 – A report sent to Razer
- Feb 17, 2022 – A patch was released
- Feb 22, 2022 – CVE-2022-25637 assigned