The Story Behind Blackout: Abusing Gmer Driver to Terminate Protected Processes

The Story

During a ransomware incident response, I noticed a file with a strange name that was retrieved by the team. Upon inspection, it turned out to be a driver. This raised the question “what does a driver have to do with a ransomware incident response?” To understand this, I started gathering information about the binary.

Every driver contains information about itself

by inspecting it, we can see that the driver’s original name is gmer64.sys and it belongs to a company called GMER, If we visit the website mentioned in the driver description, we can see that GMER is an application used for rootkit detection and removal

Every driver should be signed with a valid certificate in order to be loaded. This driver is signed under GMEREK Systemy Komputerowe Przemysław Gmerek, which is a company located in Poland

then i remembered reading about it in the Conti ransomware group leak, which was leaked in 2022. The group used the program’s client GUI to terminate the EDRs, which is a bit messy and easy to detect. There was no proof of concept (PoC) about the driver, so I had to dig my way to it.

Technical Analysis

The driver retrieves its name at runtime from the DriverObject, allowing us to control the symbolic name. Next, the driver creates a device object using IoCreateDevice ,if all goes well, it returns a device object and to make this device object accessible to user-mode it creates a symbolic link by calling IoCreateSymbolicLink.

and since the driver name on disk is Blackout, we can see that the symbolic link is created under the same name Blackout

Now we need to open a handle to our device. The file name for CreateFile should be the symbolic link prefixed with \\.\\ , so it will be \\.\\Blackout ,

All device objects are DEVICE_OBJECT structures created by different drivers, each responsible for its own layer. These objects are essential because they enable communication. Every driver needs to create at least one device object and assign it a name so clients can communicate with it .

and since the SDDL is not configured on the device a non-admin users can interact with it

To mitigate this issue we can use IoCreateDeviceSecure with a Security Descriptor that restrict non-admin users from opening a handle to the device , for more info check Controlling-device-access

using C code

All drivers must have dispatch routines, especially IRP_MJ_CREATE without it, no device handles could be opened. In the case of the gmer64 driver all dispatch routines point to a single function ,in this function major operations are processed including:

  • IRP_MJ_CREATE : Create operation, typically invoked for CreateFile or ZwCreateFile calls.
  • IRP_MJ_SHUTDOWN : Called when the system shuts down.
  • IRP_MJ_CLOSE: Close operation, normally invoked for CloseHandle or ZwClose calls.
  • IRP_MJ_DEVICE_CONTROL: Generic call to a driver, invoked by DeviceIoControl or ZwDeviceIoControlFile calls, which is the most important one in this article.

The function accepts two arguments __int64 a1 and IRP *a2, we will focus in a2 since it contains all data passed from user-mode client this data structure used to communicate with drivers and pass information about I/O requests essentially an IRP tells the driver what action to perform and provides the necessary details to complete that action.

from windows kernel programming book

Here are some other important fields of the IRP structure:

  • IoStatus: Contains the status (NT_STATUS) of the IRP and an information field. The information field is a polymorphic one, typed as ULONG_PTR (32 or 64-bit integer), but its meaning depends on the type of IRP. For example, for Read and Write IRPs, it indicates the number of bytes transferred in the operation.
  • UserBuffer: Contains the raw buffer pointer to the user’s buffer for relevant IRPs. For example, Read and Write IRPs store the user’s buffer pointer in this field. In DeviceIoControl IRPs, this points to the output buffer provided in the request.
  • UserEvent: This is a pointer to an event object (KEVENT) provided by a client if the call is asynchronous and such an event was supplied. From user mode, this event can be provided (with a HANDLE) in the OVERLAPPED structure that is mandatory for invoking I/O operations asynchronously.
  • AssociatedIrp: This union holds three members, but only one (at most) is valid at a time:*
  • SystemBuffer: The most often used member. This points to a system-allocated non-paged pool buffer used for buffered I/O operations.
Important fields of the IRP structure from windows kernel programming book

In the Major function  we can see that it checks conditions for IRP_MJ_CLOSE and IRP_MJ_DEVICE_CONTROL. However, we are more interested in IRP_MJ_DEVICE_CONTROL, as it contains the core functionalities of the driver. IRP_MJ_CLOSE likely contains cleanup code. If other IRPs such as IRP_MJ_SHUTDOWN or IRP_MJ_CREATE are called, the operation will be completed successfully

The Dispatch function takes the following eight arguments:

    1. FileObject: Pointer to the file object for the I/O request.
    2. MasterIrp: The master I/O request packet.
    3. Options: Options for the create operation.
    4. UserBuffer: Pointer to the user buffer with data.
    5. Length: Length of the data to be read or written.
    6. LowPart: A variable used for specific conditions or flags.
    7. p_IoStatus: Pointer to the status of the I/O operation.
    8. a1: An additional parameter for the function’s context or control.

In the dispatch function, it starts by checking if the received i/o control code is 0x9876C004. If yes, it initializes a variable with the value of 1; otherwise, it returns an access denied error. Therefore, we must first send this i/o control code to enable the driver to receive further i/o control codes, additionally the buffer size should be more than 4 bytes otherwise, you will get an access denied the content of the buffer does not matter as long as the passed buffer is not null after that, it will initialize the buffer to 1 then return success and complete the IRP with the given status and buffer

Now that we have obtained a handle to the device, all we need to do is send the I/O control code with the 4-byte buffer to the driver using DeviceIoControl API to enable it. From there, we will have full access to all other I/O control codes

After sending the I/O control code, we can see that the driver has returned 1 to us, which means the driver is now enabled

We can see that this function takes an unsigned integer which is the process id as a parameter  then it attaches to it and opens a handle to it and terminates it using ZwTerminateProcess

The function is called when an I/O code 0x9876C094 is received and before calling the terminate function, a check is performed to see if the buffer is null or if the size is 4 bytes. If the conditions are met, it calls the terminate function; otherwise, it returns STATUS_INVALID_BUFFER_SIZE

Now, putting it all together, we pass the process ID as an argument and convert it to an integer. After that, we call DeviceIoControl to terminate the process. The I/O control code is 0x9876C094, and we pass the process ID as the buffer

After executing the program, we can successfully terminate the anti-malware protected process or any other sort of  PPL processes

At the time of creating the PoC it was undetected on a fully-patched Windows 11 22H2 system, even though it was covered by the Microsoft driver block list because it only get updated 1-2 times per year.

Will Dormann tweet about the PoC

you can find the source code from here !

There are numerous interesting IOCTLs that can be abused. Here is another research conducted by Jonny Johnson from Binary Defense. Instead of terminating processes, this research focuses on suspending the process threads.

Wrapping Up

Even though this technique is effective for ransomware groups to shut down EDR/AV agents before starting the encryption stage, it requires local admin privileges. However, if the driver or another driver that exposes its functionality such as terminating or suspending threads is already loaded and allows non-admin users to get a handle to its device then admin privileges are not required.