The purpose of this lab is to develop arbitrary Ring 0 physical memory read and write primitives by leveraging a signed and vulnerable third-party software device driver that allows us to map physical memory to our User-Mode process.
A Short Prelude to Signed and Vulnerable Drivers
You might have already heard a ton of chatter about how some driver devs wrote copy-pasta'd a lot of crappy code back in ye olden days and how the same drivers came back to bite the very foundations of OS security boundaries in its arse so I will not waste your time with that.
In fact, there are so many of these bad drivers in existence that
I heard you like memes so...
But jokes apart, these vendors used driver samples available freely primarily from the following two sources with little to no modification:
1. WinIo project by Yariv Kaplan
2. WinRing0 project by hiyohiyo
The problem arises from the fact that these drivers themselves were not written with the potential security implications in mind and the authors of these drivers can hardly be blamed for that. However, the same cannot be said for the hardware vendors who decided to blindly copy-paste from these very same sources for their drivers.
These software device drivers(non-PnP drivers) are not tied to a specific hardware device and are used to manage/control system resources that are not available or exposed from User-Mode.
As a result of unfettered access, these bad drivers expose quite a variety of "interesting functionalities" to us including(but not limited to):
1. Ability to map/unmap physical memory either via MmMapIoSpace or ZwOpenSection + ZwMapViewOfSection or MmMapMemoryDumpMdl
2. Ability to read and write arbitrary Model Specific Registers(MSRs) via exposed __rdmsr(rdmsr) and __wrmsr(wrmsr)
3. Ability to read and write Control Registers
4. Ability to read and write I/O Ports
There are also "actual bugs" in device drivers including Buffer Overflow, Arbitrary Overwrite, Null Pointer Dereference etc. which leads to Ring 0 code execution. However, they usually come with some constraints ergo will be the topic of another post.
In this lab, we are going to be dealing with the first case only i.e. mapping physical memory which leads to us gaining an extremely powerful primitive - arbitrary Ring-0 physical memory R/W.
With this primitive built, there are two categories of attacks we can opt for:
1. Ring-0 code execution
2. Data/Data Corruption attacks
Note that it's not uncommon for these bad drivers to have more than one type of the above bugs features.
Selecting a Proxy Driver
Now that we know what we're looking for, the question that arises is where do we actually look for it?
I can tell you the kind of software that tends to be shipped with these buggy drivers:
1. CPU/GPU overclocking software
2. System monitoring/Diagnostic/Benchmarking software
Keep in mind that this is not an exhaustive list and there are more varieties that bundle these drivers but this is primarily what you should keep an eye out for.
Totally original meme #2
For the purposes of this lab, I have chosen to go with a publicly disclosed driver - MsIo64.sys(MICSYS Technology Co. Ltd.) that is shipped with Viper RGB v1.0 and MSI Ambient Link v18.104.22.168 among other products. This driver is associated with the following CVEs(albeit different versions):
I have already taken the time to analyze it and confirmed that it is a clone of the WinIo library. This is significant in terms of cutting productization costs because it implies that we do not have to reverse the driver sample to figure out how to interact with it or even the User-Mode helper DLL that comes with it since we already have the source code of both.
It should also be worth noting that this driver is Attestation Signed or in other words, it has a WHQL countersignature. This implies that it will load even on the latest Windows 10 builds with SecureBoot enabled.
However, it should be noted that this driver will not load when HVCI is enabled due to it being present in the WDAC blacklist of known bad drivers.
Remember that if a driver name contains Io there's a 90% chance that it is based on the WinIo project.
I do have some previously undiscovered(publicly at least) WHQL Signed vulnerable drivers in my arsenal. However, I do not wish to burn them right now as they are being actively used in operations and dropping 0-days was never the goal of this lab. I can tell you though that it is still possible to find these drivers if you know where to look and have a little bit of luck for it is a lot like finding a four-leaf clover.
Deriving the Primitive
Now that we have selected a target driver, let's get down to deriving the physical memory read/write primitives.
The steps for doing so are:
1. The first step is obtaining a handle to the driver's nt!_DEVICE_OBJECT structure via the symbolic link that the driver creates. This is established with the help of NtCreateFileNTAPI function(or CreateFileA/WWin32API function).
Getting the driver handle
Note here that we do not care if the driver allows low/medium-privileged users to interact with it since we are already in High-IL in a BYOVKD scenario. Also, beware that there are some drivers that implement some kind of caller checks meant to control who can talk to it. However, these are a feeble stopgap solution at best and can be bypassed with a little bit of analysis.
2. Next step is to figure out how to interact with the driver. This would have required us to reverse the driver(or the User-Mode DLL) but as I said before, we can skip this step since we already have the source code of both available.
Interacting with the driver
Here is the structure that we use for mapping/unmapping physical memory:
Now that we have this knowledge at hand, we can map/unmap physical memory by sending an I/O Request Packet(IRP) to the device driver with the appropriate IOCTL and buffer filled with our desired parameters using NtDeviceIoControlFileNTAPI function(or DeviceIoControlWin32API function).
This IRP will be handled by the target device's IRP_MJ_DEVICE_CONTROL dispatch routines responsible for mapping/unmapping physical memory.
In case you haven't noticed yet, WinIo(and by extension its derivatives) use ZwOpenSection + ZwMapViewOfSection/ZwUnmapViewOfSection on \\Device\\PhysicalMemory section object to map/unmap physical memory into local process as it is not directly accessible from User-Mode.
3. Now that we can map/unmap physical memory at our desired base PA and size at will, let's look at how we can derive an arbitrary physical memory read/write.
To read at desired PA, we map the corresponding physical memory into our process and then use RtlMoveMemory/memmove(or RtlCopyMemory/memcpy) to copy the memory contents at this PA to our output buffer before unmapping it again.
To write at desired PA, we map the corresponding physical memory into our process and then use RtlMoveMemory/memmove(or RtlCopyMemory/memcpy) to copy our input buffer into the memory contents at mapped PA before unmapping it again. In other words, we just change the direction of the copy operation.
Now, we can successfully read or write X bytes at YPA.
Enough with all the theory, let's take a look at some code now.