GMER - the art of exposing Windows rootkits in kernel mode

Chapters:
  • Introduction
  • Some basic terms
  • Howto
  • Exploring Win11 disk subsystem
  • Set up a secure environment
  • Overview of the driver
  • Patching kernel data
  • Securing disk I/O operations
  • Securing file I/O operations
  • Tracing kernel mode code
  • About PPL'ed processes
Introduction

GMER is a well-known powerful anti-rootkit tool, which has been used for years by Windows IT pros to detect the presence of rootkits in the system. A rootkit is a kind of malicious software intended to hide the components and artifacts of malware. Historically, rootkits can be divided into two types: user mode (Ring 3) and kernel mode (Ring 0). Nowadays, there are also malicious implants designed to work at the hypervisor (Ring -1) and SMM (Ring -2) privilege levels. We're gonna focus on the most common type, the kernel mode rootkit, and will simply refer to it as a rootkit.

Rootkits were popular in the Windows x86 era, when there were no restrictions on intercepting anything in privileged kernel mode. Typically, rootkits use three types of techniques: hooking, patching, DKOM. We won't delve into them in detail, but it's worth noting that the first one is used to replace a pointer to the necessary function in the function table, the second involves inline code patching and the third is used to modify members of kernel objects such as KTHREAD or EPROCESS.

The authors of other well-known anti-rootkits have dropped their support due to the growing dominance of the x64 platform and the emergence of new Windows versions. The restrictions imposed on kernel mode code on this platform have affected not only rootkits but also anti-rootkits. Rootkits have lost the ability to gain control over the system making anti-rootkit checks useless. Nevertheless, some rootkits were able to bypass the new security perimeter by rebooting the system with the test signing bootloader option. One of them was Necurs, along with several other bootkits.

Unlike other anti-rootkits, GMER has an x64 version of the driver, although doesn't support modern versions of Windows. It has an impressive arsenal of clever tricks for detecting the presence of rootkits and unhooking them, which can be useful nowadays. Since malware aims to detect the launch of GMER, it drops the driver to disk with a random name and deletes it once it's loaded. This way, the malware can't block its loading. Within the tool, the driver, which is located in the resource section, is packed twice: the tool itself is packed with UPX and its unpacked version stores the compressed driver. The tool's executable is also landed on the disk with a random name.

Some basic terms

Anti-rootkit - a standalone tool/utility or component within a security product that is designed to deeply inspect Windows environment at both user and kernel mode levels to detect system anomalies.

Direct Kernel Object Manipulation (DKOM) - a rootkit technique that means modification of Windows kernel objects through direct access to them without any API.

Disk driver - a Windows driver called disk.sys that is responsible of processing disk I/O operations usually coming from the partition manager (PartMgr.sys), volume manager (Volmgrx.sys) or any other clients via \PhysicalDriveX objects. In fact, the classpnp.sys driver dispatches all disk.sys driver requests.

Disk port driver - while disk.sys implements a high-level logic of communication with various disk types connected to different interfaces, the disk port driver is designed to communicate with a specific disk device. There are several common disk port drivers such as atapi.sys, scsiport.sys, ataport.sys, storport.sys.

Howto

To extract the driver, the dropper first should be unpacked. The driver inside the resource section of the dropper is compressed with zlib. In order to decompress it, I personally simply used the built-in VirusTotal decompressor. In the Relations tab, you can find a link to the report with the decompressed driver.

C:\test>upx -d -o C:\test\gmer\gmer_unpacked.exe C:\test\gmer\gmer.exe    

To make the analysis process faster, I ran GMER on my Win7 SP1 x64 VM and took a kernel memory dump. Since the dump contains the driver with already initialized variables and decrypted text strings, we can determine the purpose of each pointer in the disassembled version of the driver. Next, we need to rebase the driver loaded into IDA so that the offsets of both drivers are identical.
Edit->Segments->Rebase program...

Set up a secure environment

Any anti-rootkit shouldn't trust the environment in which it works. Members of kernel mode objects, pointers in dispatch tables, the integrity of the WinNT kernel executable, and drivers - all this stuff may already be compromised before an anti-rootkit is launched.

A secure environment means a set of artifacts such as kernel object pointers, their values, kernel function pointers, that have been retrieved or restored in a secure way and are suitable for further use.

GMER takes the following actions to initialize its own secure environment in DriverEntry.

  • To construct names of driver objects of interest on the stack, such as \Filesystem\Ntfs, \Driver\Disk, \Driver\atapi.
  • To select kernel object offsets depending on the OS version, including, EPROCESS.Peb, EPROCESS.UniqueProcessId, KPROCESS.ThreadListHead, ETHREAD.StartAddress, etc.
  • In case of an unknown Windows version, it obtains those offsets through manual analysis of the appropriate ntoskrnl functions, such as PsGetProcessPeb, PsGetProcessSectionBaseAddress, etc.
  • Dynamically resolves some important imports such as IofCompleteRequest and IofCallDriver using MmGetSystemRoutineAddress.
  • To map the NT layer DLL ntdll.dll, which is used further to get additional information.
  • To obtain the start address of loaded Ntfs.sys and Fastfat.sys, locate the entry point and scan it for a specific signature to retrieve the real address of their IRP_MJ_CREATE handlers.
  • To get information about loaded ataport.sys and scsiport.sys. These drivers are responsible for low-level disk communication.
  • To use its own PE export parser and get the addresses of the sensitive functions listed below.
For most of the kernel objects offsets, GMER obtains them by analyzing the following functions.
The following driver function looks up for most offsets due to their simple structure at the beginning.
The situation varies when it comes to finding the corresponding EPROCESS and ETHREAD fields used to collect information about threads, as different Windows versions use a different number of lists.

GMER obtains the addresses of nt!Zw exports (services) in a tricky way. First, it obtains the KeServiceDescriptorTable address by finding a 8-byte signature "8B F8 C1 EF 07 83 E7 20" and two other values in ntoskrnl, which represent the following instructions inside the KiSystemServiceStart function. As a starting point, it takes the address of the nt!strnicmp function and scans it to the KdDebuggerNotPresent variable.
The disposition of the target ntoskrnl functions.
To obtain the address of a specific ntoskrnl!Zw function, it maps Ntdll and retrieves from it the address of the required export function. Next, it grabs the System Service Number (SSI) from the second instruction of the export, which is identical for all of them: mov eax, SSN (B8 3F 00 00 00).

With these pieces together, the process of obtaining ntoskrnl exports for Zw services looks as follows:
  1. To get an export function address from the mapped Ntdll.
  2. To take the SSN from the second instruction of the function.
  3. To use this SSN as an index in the KeServiceDescriptorTable.KiServiceTable array and calculate the appropriate Zw function address. Note that the pointers in this array are protected by PatchGuard.
An interesting fact is that when scanning ntoskrnl data between strnicmp and KdDebuggerNotPresent to find the address of KeServiceDescriptorTable, the driver doesn't validate the current pointer with MmIsAddressValid. Since the space between these symbols belongs to multiple ntoskrnl sections, one of them may be INIT, which may be already discarded from memory.

Overview of the driver

The driver provides its user mode counterpart with various valuable interfaces aimed at obtaining trustworthy data about the Windows environment. These features are available via the appropriate IOCTL codes listed below. After obtaining the necessary data, GMER compares it with the data obtained through regular Windows API and report to the user about the detected anomalies.

Basically, to supply the requested data, the driver leverages Windows kernel API, low-level disk and file system access skipping the intermediate filters, DKOM and custom implementation of some Windows kernel functions.
During initialization, the driver creates a separate thread to execute the following operations in the System process context: shutdown system, read process memory, suspend thread, query information about thread, process, system, and system registry. When the GMER's DeviceIoControl handler, which is executed in the current process context, recognizes one of those IOCTLs, it builds the context structure with a pointer to a specific handler and sets the appropriate event that triggers another thread to execute it.

Exploring Win11 disk subsystem

Before we start discussing the topic, let's take a look at some basic aspects of Windows disk subsystem.

The Disk.sys driver is responsible for dispatching storage devices I/O. The client must specify the offset from the beginning of the disk and data length. The driver in turn redirects this request to one of the corresponding disk port drivers.

We can start by exploring disk device stack and go deeper.
As we can see there are four devices on that device stack.
  • The first one belongs to the partition manager and presents the device of the raw disk partition. Partmgr forwards the disk I/O request further to the disk driver, providing it an LBA instead of a partition offset.
  • The second device belongs to the disk driver itself, described above.
  • The next device was created by the common driver acpi.sys which, essentially simply forwards disk I/O requests to the port driver. The responsibilities of Acpi. sys include support for power management and Plug and Play (PnP) device enumeration.
  • The latter belongs to the iaStorVD disk port driver (Intel Rapid Storage Technology) and is used in many computers with Intel chipsets. Being a disk port driver, it's responsible for low-level communication with a specific storage device type, including, initializing the storage controller and identifying and attaching storage devices.
But the whole picture of processing disk requests is a bit more complicated, since there are more drivers that are indirectly involved in it. Let's inspect those driver objects on the disk device stack.
As we can see both drivers redirect their driver dispatch routines to another drivers. In the case of disk.sys its table of driver dispatch routines points to Classpnp functions. The driver name stands for "Class Plug and Play Driver" and is responsible for managing Plug and Play (PnP) devices. Classpnp handles the device requests addressed to disk.sys. classpnp!ClassGlobalDispatch, in turn, simply redirects the execution flow to the appropriate classpnp dispatch function. Therefor, these items of the disk driver dispatch table are perfect targets for any rootkit (if it can defeat PatchGuard first).
If we know the pointer to the disk device object, we can get information about the real dispatch table of the classpnp driver.
The second driver iaStorVD.sys relies on its counterpart storport.sys. The latter is a general-purpose storage driver responsible for managing communication between storage devices and the operating system itself. While iaStorVD.sys provides functionality for managing RAID arrays and handling I/O requests for devices connected to the Intel RAID controller, storport.sys is directly responsible for communication with storage devices. Thus, Storport.sys operates at a lower level than iaStorVD.sys, providing basic communication and data transfer functionality with the storage hardware.

iaStorVD.sys also creates one more device incorporated in another device stack that ends up in Pci.sys. It's used to handle requests for the PCI bus driver Pci.sys.
️ Patching kernel data

GMER aims to patch kernel mode code and disk port driver data in two cases mentioned below. In both of them, it is interested in intercepting control of the disk I/O operation before the disk port driver returns control to the client. The driver parses the SCSI_REQUEST_BLOCK structure and, in particular, its Cdb structure to obtain information about the LBA and the size of the requested data.

  • The anti-rootkit driver overwrites the IAT entry of the disk port driver that matches the IofCompleteRequest function with a GMER's one.
  • It also implements run-time code patching. First 0xF bytes of ataport!IdePortDispatch->ataport!IdePortPdoDispatch are subject to this modification in the case of a sector write operation.
GMER puts the following instruction at the beginning of ataport!IdePortPdoDispatch. A pointer to its implementation of IofCompleteRequest follows the instruction.
While the address of ataport!IdePortDispatch can simply be obtained from the driver's dispatch function table, to find the address of ataport!IdePortPdoDispatch GMER needs to check the body of the first function for a specific signature chain. This signature chain is presented below.
Below you can see part of the code that implements the interception of the ataport!IdePortPdoDispatch function. The first call is used to modify that function and copy the old 0xF bytes. The second one saves the copied old bytes to the global driver data.
It's worth noting that the GMER function for patching kernel mode code isn't safe for use in multiprocessor systems (SMP). Instead of raising IRQL on all CPUs, the driver does this only on the current one. GMER implements a typical method for patching kernel mode data as follows.

Securing disk I/O operations

The driver provides GMER with the ability to work with disk at a low level by addressing directly to the Atapi/Ataport or Scsiport disk drivers. It builds a request packet and sends it to one of them through the IRP_MJ_INTERNAL_DEVICE_CONTROL request depending on which one is active in the system.

The driver receives the name of the disk device object from its user-mode counterpart. Before sending a request to the disk device, the driver obtains the pointer to the lowest device on the stack using IoGetBaseFileSystemDeviceObject, thus bypassing all potentially non trusted devices.

struct _CDB10 {

UCHAR OperationCode;
...

UCHAR LogicalUnitNumber : 3;

UCHAR LogicalBlockByte0;

UCHAR LogicalBlockByte1;

UCHAR LogicalBlockByte2;

UCHAR LogicalBlockByte3;
...

UCHAR Control;

} CDB10, *PCDB10;


Before calling the disk driver, GMER patches its IAT entry matching the IofCompleteRequest function to GMER's one.
GMER supports IOCTL code 0x7201C008 to perform secure disk I/O operation for its user-mode application. Below you can see its pseudo code, which omits minor operations.
GMER has several I/O completion routines that are designed to be used in multiple anti-rootkit scenarios when processing disk I/O operations.
  • The first is used to secure (intercept) the disk read operation (IRP_MJ_INTERNAL_DEVICE_CONTROL, SCSIOP_READ) globally (IOCTL 0x7201C020). Its pointer replaces IofCompleteRequest in the disk port driver IAT entry and is used to copy read data from the system buffer to the GMER's one.
  • Another one is involved in securing the disk write op (IRP_MJ_INTERNAL_DEVICE_CONTROL, SCSIOP_WRITE) globally (IOCTL 0x7201C02C). The driver uses a pointer to this function in the patching code for ataport!IdePortPdoDispatch. This routine is used to prohibit write access to disk sectors (LBA) supplied from user mode .
  • The latter is used to dispatch the initiated disk I/O request coming from the GMER app (IOCTL 0x7201C008).
In addition to those IOCTLs, GMER has a feature to scan the classpnp handlers for potential run-time hooks (0x9876C058). The driver obtains the handler offset inside the classpnp driver file, its SYSTEM_MODULE_ENTRY.ImageBase, and checks its prologue for two signature sequences: 0x55 0x8B 0xEC, 0x8B 0xFF 0x55.

But it's unclear why the 64-bit GMER driver checks the classpnp dispatch functions for x86 instructions...

️ Securing file I/O operations

GMER secures file operations as follows.
  • To open a file, the driver calls the IoCreateFileSpecifyDeviceObjectHint API, sending a request directly to the FSD, skipping possible intermediate filters. To obtain a pointer to the FSD device object that is the lowest on the stack, it uses IoGetBaseFileSystemDeviceObject (IoGetDeviceAttachmentBaseRef).
  • If the function fails, GMER tries to check IofCallDriver for hooks, but only its the old version, which has a jump instruction to the IopfCallDriver pointer at the beginning.
  • In addition to checking IofCallDriver for hooks, the driver also checks and restores the original IRP_MJ_CREATE handlers for Ntfs and Fastfat driver objects. As was mentioned above, GMER obtains these handlers at the driver initialization phase.
  • After obtaining a handle to the requested file, the driver uses the ordinary APIs ZwReadFile, ZwWriteFile, ZwDeleteFile, ZwQueryInformationFile, ZwSetInformationFile, ZwClose.
These operations are performed in the System process context.
The following pseudocode demonstrates how GMER secures file I/O operations.

⛓️Tracing kernel mode code

Along with information about system anomalies, GMER is also capable of providing details about possible code execution flow violations involved in processing file system operations. This feature is based on code tracing or single-step CPU mode, when the driver code intercepts control after each executed CPU instruction and saves information about each system module to which this instruction belongs. This mode is activated by setting the trap flag in the RFLAGS register {pushfq; pop rax; or eax 100h; push rax; popfq}.
Below you can see the driver code responsible for intercepting the int 1 handler and the corresponding x64 structures.
The driver interrupt handler is simply a wrapper, it prepares the necessary data on the stack and calls the real handler.
When tracing code, the driver maintains a context structure with several arrays that store information about system modules and their call stack frames. Then this data will be copied to the provided user buffer and analyzed by the application for the presence of unknown system modules.
The driver single step mode dispatch function looks as follows.
The above function is involved in code tracing in two scenarios - calling the FSD driver directly via IofCallDriver and ZwQueryDirectoryFile.
The second scenario.
About PPL'ed processes

PPL is a widely known built-in Windows security feature designed to provide high-end protection for trusted Windows services such as anti-malware processes. It's recognized as a robust security measure aimed at protecting running processes from any form of modification or other destructive impact. Access checks for PPL-protected processes are implemented at the opening process handle level, without any exceptions for kernel mode code. As a result, these processes cannot be terminated using ZwOpenProcess and ZwTerminateProcess calls made by the kernel mode driver.

The required access checks can only be successfully passed by code that works on another PPL protection level with an equal or higher one. GMER isn't an exception; like other renowned security tools, including, Process Explorer, Process Hacker, Process Informer, WinArk, it can't terminate PPL'ed processes. This limitation arises not only because it employs a simple trick involving a pair of aforementioned functions after attaching to the System process, but also due to its constraints when working on modern Windows versions. To terminate a PPL-protected process, kernel mode code requires a pointer to the kernel object of the process and a pointer to an internal ntoskrnl function, PspTerminateProcess, capable of process termination by a pointer rather than its handle.

GMER terminates the process as shown below (IOCTL 0x9876C094).
To make the process of finding PspTerminateProcess more reliable across Windows versions, a signature chain candidate should consist of unique byte sequences. GMER can easily find many Windows kernel undocs, so locating PspTerminateProcess shouldn't be difficult for it.
If we delve deeper, we find that the key function in the process of checking PPL protection is an open procedure for the process object type (PsProcessType) called PspProcessOpen. This is the only purpose of this function, which is responsible for comparing PSPROTECTION values of both processes. Before implementing PPL, the process kernel object didn't have the open procedure.

Below, you can see the process of obtaining a handle to the PPL'ed process, starting with the call of NtOpenProcess and ending with the actual validation of the protection.
The process of removing PPL protection could be simplified with just zeroing EPROCESS_Protection value that is used by the Windows kernel to set the corresponding level of protection. It's related to DKOM and there are several projects on GitHub demonstrating this method. It can also be used by attackers or defenders for the opposite purpose to enable the protection for specific processes, making them inaccessible for any kind of modification. Below you can see the corresponding structures describing PPL.
To disable PPL, the protection byte or all three fields should be set to zero (see Kernel Driver Utility, KDU).
To enable protection for the process (EDRSandblast).
The following projects on GitHub demonstrate this trick with disabling the PPL protection using DKOM, i e manually changing EPROCESS_Protection. In addition, from a blog post by Denis Skvortcov, we can learn that Avast security products set protection for their anti-malware services by manually PPL'ing the corresponding process.


The latter tool was reportedly utilized by the Midnight Blizzard TA to disable the protection of an installed anti-malware product. It's also capable of enabling protection for the current process. To RW kernel memory, this tool requires a driver that is vulnerable to BYOVD. As opposed to EDRSandblast, KDU comes with numerous vulnerable drivers so you don't need to find it yourself.

References

https://www.crowdstrike.com/blog/evolution-protected-processes-part-2-exploitjailbreak-mitigations-unkillable-processes-and/

https://www.alex-ionescu.com/146/

https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/srb/ns-srb-_scsi_request_block

https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-347a

Article Link: A blog about rootkits research and the Windows kernel: GMER - the art of exposing Windows rootkits in kernel mode