ThreadSleeper: Suspending Threads via GMER64 Driver

Jonathan Johnson
8 min readJul 21, 2023

--

Originally posted: https://www.binarydefense.com/resources/blog/threadsleeper-suspending-threads-via-gmer64-driver/

Recently a friend of mine, Nick Powers, sent me the gmer.sys driver that was involved with the Blackout activity which exposed functionality to terminate any process you wanted from a medium integrity level context. This was being used against many EDR vendors, including Microsoft Defender for Endpoint, to kill their service process (MsSense.exe in MDE’s case) which was running as an anti-malware-light protected process (PPL). ZeroMemoryEx tweeted about this here:

This was obviously not ideal and vendors like Microsoft worked quickly to prevent this driver from being dropped to disk. However, once loaded there isn’t much that could be done.

While looking at this driver, I found a lot of suspicious capability. One of which was suspending any thread you chose, assuming you have its thread identifier. This blog will cover my process for finding the alternative capability for interfering with the operation of a PPL process associated with an EDR driver that is, in my opinion, a little stealthier.

Methodology

Step 1: Check the device object security descriptor

Every device driver must have at least one device object. The purpose of this device object is to handle I/O requests. Device objects are created via IoCreateDevice or IoCreateDeviceSecure. The difference between these 2 APIs is that IoCreateDeviceSecure’s 7th parameter allows the author to specify a DACL for the device object, whereas with IoCreateDevice the developer would have to do so differently.

Note: Some drivers specify the security descriptor in the INF file but that doesn’t apply to us today.

After opening up IDA and finding the DriverEntry, we can see that there is a call to IoCreateDevice:

One thing we can see here is that the DeviceObject name comes from whatever the service name the driver was installed under. Also, there isn’t a security descriptor set on the device object, so we know if we find this driver installed somewhere we could interact with it from a non-privileged standpoint (e.g., medium integrity level). You can also see this easily with WinObjEx64:

We can see that “Everyone” has plenty of access for us to get a handle to the device object via CreateFile, which I only use GenericRead/GenericWrite. More on this in the POC below.

Note: If you plan on dropping this driver you will need to be admin on the target host to install the service.

We then see a symbolic link created which allows user-mode applications to get a handle to the device object. This is needed so we can send the IOCTL via DeviceIOControl later.

Step 2: Find the DispatchDeviceControl routine

Typically when a vulnerability in a driver is discovered, it is because the driver exposes some functionality that can be accessed through its DispatchDeviceControl routine which can be accessed when a user-mode client uses the DeviceIoControl Win32 API (there are other functions that can be leveraged) to pass in an input/output control code or IOCTL.

When a user-mode application uses DeviceIoControl to send an IOCTL to a device driver the information gets packaged into an I/O request packet or IRP. Once the driver has the IRP it will access the I/O stack location to figure out what type of major function the operation is requesting (in this case, the IRP_MJ_DEVICE_CONTROL major function), along with its parameters. The IRP is passed to the DispatchDeviceControl that is registered up within the driver. The DispatchDeviceControl routine will unpackage the IRP to find the IOCTL and the included buffer (typically containing parameters to go with the functionality within the driver). Control codes are defined by the CTL_CODE macro which can be found in the ntfis.h:

  1. Device Type
  2. Access
  3. Function
  4. Method

When reversing a driver, the IOCTLs are easy to point out after finding the function behind DispatchDeviceControlbecause they are typically in a switch statement. The switch statement is executed to find the internal function that is set up to handle requests with the matching IOCTL and passes along the parameters which were passed in by the user. I went a bit more in-depth on this in my blogpost Exploring Impersonation through the Named Pipe Filesystem Driver.

To find the driver-specific DispatchDeviceControl routine we need to find if the driver handles the IRP_MJ_DEVICE_CONTROL major function. Most drivers will do something like this if they do:

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = TestDeviceControl;

If we look in the gmer64 driver we see:

Within sub_12448, we can see that there is redundant check to see if the major function code is IRP_MJ_DEVICE_CONTROL and to pass it into another internal function, which I renamed to DispatchDeviceControl:

Now that we have found the internal function that handles IRP_MJ_DEVICE_CONTROL requests, we need to see if there are any calls of interest to us.

Step 3: Map out IOCTLs

If you look closely at the exposed IOCTLs and their functionality, you will quickly see that there are some sketchy things you can do with this driver. However, the one that I found interesting was 0x9876C098:

0x9876C098 makes a call to an internal function (which I renamed as SuspendThread), which takes in 2 parameters: A pointer to a structure and the 2nd member in that structure. Once in that internal function a call to ZwOpenThread is made and then a function pointer is used to call ZwSuspendThread:

The previous example demonstrated by ZeroMemoryEx used the IOCTL 0x9876C094 which made a call to an internal function that took in a process ID that was passed into ZwTerminateProcess, terminating the process. This had me thinking — what if I could suspend any process I wanted and render a process (say, a PPL process) useless without showing that it was terminated. Let’s create the POC.

Step 4: Create POC

Note: In order for the us to get the IOCTL to work correctly, initialization has to be done within the driver which is sent via the IOCTL 0x9876C004 which can be seen in the Blackout POC.

The first thing I needed to do was create a custom structure that could be passed in because the SuspendThread internal function took in 2 parameters: a pointer to some structure and the 1st member in that structure. After looking into that internal function I realized that the structure in question was likely a CLIENT_ID. as the members of the user-supplied structure were passed into a new CLIENT_ID

Driver Code:

POC code:

struct TargetProcess {
DWORD ProcessId;
DWORD ThreadId;
};

I do want to point out that the ProcessId member isn’t actually used, so from the user-mode side you only need to get the thread ID which can be grabbed as medium IL. I was confused by this at first and was worried I overlooked something so thank you to Matt Hand for sanity checking this — it was weird to both of us.

Next, we need to get a handle to the device object via the symlink which can be done using CreateFile:

hDevice = CreateFileW(L"\\\\.\\gmer64", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

If you are not familiar, CreateFile can be used to not only create files but also obtain handles to existing objects. You will also notice the device object name is gmer64. That is because I created the service as gmer64 on my machine.

After that I created an instance of my structure and passed in the parameters appropriately, where argument 1 is the process ID (again not used) and argument 2 is the thread ID:

TargetProcess data;
data.ProcessId = atoi(argv[1]);
data.ThreadId = atoi(argv[2]);

We then need to send the IOCTL via DeviceIoControl. Before we can send the IOCTL to suspend threads, a prerequisite is that we must first do some initialization with the driver. The initialization is simple as we just need to send the IOCTL 0x9876C004. This will set a global variable to 1, if we do not do this and the global variable is not set to 1 the DeviceIoControl call later will fail. Once this is set you can send as many DeviceIoControl requests as you please.

BOOL deviceControl = DeviceIoControl(hDevice, INITIALIZE_IOCTL_CODE, &data, sizeof(data), output, outputSize, &bytes, NULL);

After initializing using the previous request, we can call DeviceIoControl for the IOCTL we want along with the thread ID we want to suspend.

deviceControl = DeviceIoControl(hDevice, SUSPEND_THREAD_IOCTL_CODE, &data, sizeof(data), &output, outputSize, &bytes, NULL);

Here is the output showing all threads being suspended in a process:

Code can be found here: https://github.com/jsecurity101/RandomPOCs/tree/main/SuspendThreadDriver

Impact

For those that aren’t aware, a lot of endpoint protection products will restart their main processes if they have been killed. This is relatively common practice now; however, they aren’t checking if the threads in that main process are suspended. They usually just check to see if the process is alive, which technically still is using this technique. If this driver is already running on a system, you could obtain all the thread IDs associated with a sensor process from medium IL and suspend them with this driver. This is what I was alluding to in this tweet a while back:

Just to prove that no callbacks happen afterwards here a check I did on a device that had MDE installed 3 days after executing the above:

Recommendations

From a defensive perspective, my recommendation is mostly to sensor vendors:

  • Collect SuspendThread events from the Threat-Intelligence ETW provider. There are events for calls coming from user-mode and kernel-mode.
  • Collect Driver/DeviceLoad events from the Threat-Intelligence ETW provider, you will be able to see when this driver is loaded.
  • Add the gmer64 driver to your list of prohibited drivers

Conclusion

Although this was already tagged as a vulnerable driver, this was an interesting project as it highlights that if a driver is found to be vulnerable, oftentimes there are other vulnerabilities or functionality useful to an adversary embedded if you look at it a bit closer.

Lastly, vulnerable drivers have been really amping up this year and I wanted to point out that there are defensive capabilities outside of WDAC. Microsoft’s team has worked hard to expose events through the Threat-Intelligence ETW provider that can be used in situations like this.

Resources

Again, thank you to ZeroMemoryEx for their awesome with BlackOut, finding the terminate process vulnerability, and their POC which made my life a bit easier.

General References:

Other good blogs on reversing drivers/vulnerable drivers:

--

--

Jonathan Johnson

Principal Security Engineer @Prelude | Windows Internals