反 Rootkit 工具的反噬:Trend Micro RootkitBuster 漏洞导致 Rootkit 被植入
On May 18, 2020, an 18‑year‑old researcher, Bill Demirkapi, published a lengthy write-up on 2026-6-6 12:0:0 Author: hackmag.com(查看原文) 阅读量:2 收藏

On May 18, 2020, an 18‑year‑old researcher, Bill Demirkapi, published a lengthy write-up on his blog on a very curious topic. He managed to install his own rootkit in Windows using Trend Micro’s RootkitBuster utility. Today’s article tells the story of how this young researcher pulled it off.

While studying rootkit detection techniques, Bill Demirkapi came across Trend Micro’s free tool RootkitBuster. The vendor presents it as a scanner for hidden files, registry entries, and the master boot record (MBR), designed to detect and remove rootkits. According to its description, RootkitBuster can uncover several different rootkit infiltration and persistence mechanisms. This caught the researcher’s attention, and he decided to see what was really going on under the hood. A closer look at the program revealed a notable vulnerability in its code that can be abused to turn the tool not only into a scanner for deeply hidden Windows malware, but also into a vehicle for installing your own rootkits on the system.

www

This article is based on the original research by Bill Demirkapi. If you’re comfortable reading English, you can check out his write‑up on his blog.

Installation

Right after launching the RootkitBuster installer, Bill noticed a Process Hacker alert saying the tool was trying to install a file called tmcomm.sys on his system — a driver used by some Trend Micro applications.

RootkitBuster installs its driver before the user accepts the license agreement (all screenshots here and below are from Bill Demirkapi’s blog)
RootkitBuster installs its driver before the user accepts the license agreement (all screenshots here and below are from Bill Demirkapi’s blog)

Interestingly, both the driver and the scanner’s executable had already been unpacked to the %TEMP%\RootkitBuster folder on disk before the license agreement text even appeared on Bill’s screen. Among other things, the agreement stated that the RootkitBuster user agrees “not to attempt to reverse engineer, modify, disassemble, decompile, analyze the source code, or create derivative works based on this program.”

So Bill simply killed the installer via the “Close window” item in the context menu, never actually accepting the license terms. That let him, with a clear conscience, “disassemble, decompile,” and do other things to this Trend Micro product that polite society prefers not to mention out loud. A great little trick for sidestepping the legal complications!

The tmcomm.sys Driver

This driver, identified as the Trend Micro Common Module, can receive messages from privileged user‑mode applications and can perform functions that are not limited to the RootkitBuster utility. In other words, it’s used by many other Trend Micro programs as well.

One of the first things the driver does is create a device object at \Device\TmComm to receive IOCTL messages from user mode. It then creates a symbolic link for this device at \DosDevices\TmComm (accessible as \\.\Global\TmComm).

The entry point initializes a large number of classes and structures used by the driver, but for research purposes—namely, executing kernel-mode code—there’s no need to examine each of them in detail.

Device creation in the tmcomm.sys driver
Device creation in the tmcomm.sys driver

Bill pointed out that to use the virtual device created by the driver you need SYSTEM privileges—meaning at least local administrator rights. This significantly limits how any potential vulnerabilities in the driver could be exploited, but doesn’t rule them out entirely.

The TrueApi Class

One of the most important components of the driver is the TrueApi class, which is created at the entry point and holds pointers to the imported functions used by the driver. The structure of this class looks as follows:

struct TrueApi

{

BYTE Initialized;

PVOID ZwQuerySystemInformation;

PVOID ZwCreateFile;

PVOID unk1; // Initialized as NULL

PVOID ZwQueryDirectoryFile;

PVOID ZwClose;

PVOID ZwOpenDirectoryObjectWrapper;

PVOID ZwQueryDirectoryObject;

PVOID ZwDuplicateObject;

PVOID unk2; // Initialized as NULL

PVOID ZwOpenKey;

PVOID ZwEnumerateKey;

PVOID ZwEnumerateValueKey;

PVOID ZwCreateKey;

PVOID ZwQueryValueKey;

PVOID ZwQueryKey;

PVOID ZwDeleteKey;

PVOID ZwTerminateProcess;

PVOID ZwOpenProcess;

PVOID ZwSetValueKey;

PVOID ZwDeleteValueKey;

PVOID ZwCreateSection;

PVOID ZwQueryInformationFile;

PVOID ZwSetInformationFile;

PVOID ZwMapViewOfSection;

PVOID ZwUnmapViewOfSection;

PVOID ZwReadFile;

PVOID ZwWriteFile;

PVOID ZwQuerySecurityObject;

PVOID unk3; // Initialized as NULL

PVOID unk4; // Initialized as NULL

PVOID ZwSetSecurityObject;

};

If you look closely at this structure, it becomes clear that it’s being used as an alternative to direct function calls. Bill suggested that Trend Micro’s software caches these imported functions during initialization to avoid interception of the delay import table. With delayed imports, a linked DLL is loaded only when the application actually calls one of its functions. If there’s a rootkit on the system that can hook the import table at the moment the driver is loaded, you need to have a protection mechanism in place to counter that.

The XrayApi Class

The driver contains another important class called XrayApi. It’s used to access several low‑level devices and interact directly with the file system. This class includes an XrayConfig structure, which holds its main configuration:

struct XrayConfigData

{

WORD Size;

CHAR pad1[2];

DWORD SystemBuildNumber;

DWORD UnkOffset1;

DWORD UnkOffset2;

DWORD UnkOffset3;

CHAR pad2[4];

PVOID NotificationEntryIdentifier;

PVOID NtoskrnlBase;

PVOID IopRootDeviceNode;

PVOID PpDevNodeLockTree;

PVOID ExInitializeNPagedLookasideListInternal;

PVOID ExDeleteNPagedLookasideList;

CHAR unkpad3[16];

PVOID KeAcquireInStackQueuedSpinLockAtDpcLevel;

PVOID KeReleaseInStackQueuedSpinLockFromDpcLevel;

...

};

Among other things, this structure contains information about where certain internal and undocumented variables are located inside the Windows kernel, such as ExInitializeNPagedLookasideListInternal, IopRootDeviceNode, ExDeleteNPagedLookasideList, and PpDevNodeLockTree. The researcher suggested that the purpose of this class is to gain direct access to low-level devices while bypassing the documented methods (and therefore the ones that are well known to malware authors).

IOCTL Requests

Before digging into the driver’s capabilities and features, Bill Demirkapi focused on how it processes IOCTL requests. These are I/O system calls specific to certain (mostly low-level) devices that can’t be implemented using regular calls. In its main dispatch function, the Trend Micro driver converts the data from an IRP_MJ_DEVICE_CONTROL request into its own structure, which the researcher called TmIoctlRequest:

struct TmIoctlRequest

{

DWORD InputSize;

DWORD OutputSize;

PVOID UserInputBuffer;

PVOID UserOutputBuffer;

PVOID Unused;

DWORD_PTR* BytesWritten;

};

Thus, IOCTL request handling in the tmcomm.sys driver is implemented using custom “dispatch tables,” where a “base table” holds the IOCTL code and a corresponding helper function. For example, if an IOCTL request with the code 0xDEADBEEF needs to be sent, the driver compares the request against each entry in the base dispatch table and forwards it only if a match is found. Each entry in such a table has a structure similar to the one shown below:

typedef NTSTATUS (__fastcall *DispatchFunction_t)(TmIoctlRequest *IoctlRequest);

struct BaseDispatchTableEntry

{

DWORD_PTR IOCode;

DispatchFunction_t DispatchFunction;

};

After the DispatchFunction call, the data being passed is usually validated—starting with a nullptr check and continuing with verification of the input and output buffers. These “auxiliary dispatch functions” then perform another lookup based on a code supplied in the user input buffer, in order to find the corresponding entry in a helper table. Such entries use a structure of the following form:

typedef NTSTATUS (__fastcall *OperationFunction_t)(PVOID InputBuffer, PVOID OutputBuffer);

struct SubDispatchTableEntry

{

DWORD64 OperationCode;

OperationFunction_t PrimaryRoutine;

OperationFunction_t ValidatorRoutine;

};

Immediately before calling the PrimaryRoutine module, which performs the requested action, the SubDispatchTableEntry function invokes ValidatorRoutine. This subroutine validates the input buffer—that is, it checks the data that will later be used by PrimaryRoutine. The main routine runs only if ValidatorRoutine completes the validation successfully.

While studying how IOCTL requests are processed, Bill Demirkapi examined each entry in the main dispatch table and the functions stored there. This, in turn, allowed him to determine the purpose of the auxiliary dispatch tables.

IoControlCode == 9000402Bh

The first table Bill examined is responsible for interacting with the file system. The code for this auxiliary dispatch table is obtained by dereferencing a DWORD located at the beginning of the input buffer. In other words, to determine which entry in the auxiliary table should be executed, you place a DWORD corresponding to a specific opcode at the start of the input buffer.

To make life easier for researchers, the Trend Micro developers left a lot of debug strings in the driver code. Using these, Bill Demirkapi reconstructed the PrimaryRoutine function table stored in the auxiliary dispatch table and described what each function does.

Opcode PrimaryRoutine Function Description
—— ———————- ———–
2713h IoControlCreateFile Calls NtCreateFile; all parameters are taken from the request
2711h IoControlFindNextFile Returns STATUS_NOT_SUPPORTED
2710h IoControlFindFirstFile Does nothing; always returns STATUS_SUCCESS
2712h IoControlFindCloseFile Calls ZwClose; all parameters are taken from the request
2714h IoControlCreateFileIRP Creates a new FileObject and associates a DeviceObject for the requested disk with it
2715h IoControlReadFileIRPNoCache References the FileObject using the HANDLE from the request, calls IofCallDriver, and reads the result
2716h IoControlDeleteFileIRP Deletes a file by sending an IRP_MJ_SET_INFORMATION request
2717h IoControlGetFileSizeIRP Retrieves the file size by sending an IRP_MJ_QUERY_INFORMATION request
2718h IoControlSetFilePosIRP Sets file metadata by sending an IRP_MJ_SET_INFORMATION request
2719h IoControlFindFirstFileIRP Returns STATUS_NOT_SUPPORTED
271Ah IoControlFindNextFileIRP Returns STATUS_NOT_SUPPORTED
2720h IoControlQueryFile Calls NtQueryInformationFile; all parameters are taken from the request
2721h IoControlSetInformationFile Calls NtSetInformationFile; all parameters are taken from the request
2722h IoControlCreateFileOplock Creates an oplock using IoCreateFileEx and other filesystem APIs
2723h IoControlGetFileSecurity Calls NtCreateFile, then ZwQuerySecurityObject; all parameters are taken from the request
2724h IoControlSetFileSecurity Calls NtCreateFile, then ZwSetSecurityObject; all parameters are taken from the request
2725h IoControlQueryExclusiveHandle Checks the file handle and how it is being used
2726h IoControlCloseExclusiveHandle Forcibly closes the file handle

IoControlCode == 90004027h

This dispatch table is primarily used to control the process scanner. Many of its functions use a dedicated scanning thread to synchronously enumerate processes using various techniques, both documented and undocumented. Bill Demirkapi has also compiled descriptions of the main functions in this table.

Opcode PrimaryRoutine Function Description
—— ———————- ———–
C350h GetProcessesAllMethods Scans for processes using ZwQuerySystemInformation and WorkingSetExpansionLinks
C351h DeleteTaskResults* Deletes results produced by other routines such as GetProcessesAllMethods
C358h GetTaskBasicResults* Retrieves basic analysis results produced by other routines such as GetProcessesAllMethods
C35Dh GetTaskFullResults* Retrieves full analysis results produced by other routines such as GetProcessesAllMethods
C360h IsSupportedSystem Returns TRUE if the system is “supported” (regardless of whether hardcoded offsets exist for the current build)
C361h TryToStopTmComm Attempts to stop the driver
C362h GetProcessesViaMethod Scans for processes using the specified method
C371h CheckDeviceStackIntegrity Checks for device tampering on devices associated with physical disks
C375h ShouldRequireOplock Returns TRUE if an opportunistic lock is required for certain scanning operations

All these functions use several structures that the researcher named MicroTask and MicroScan. Here’s how they look in the disassembled code.

struct MicroTaskVtable

{

PVOID Constructor;

PVOID NewNode;

PVOID DeleteNode;

PVOID Insert;

PVOID InsertAfter;

PVOID InsertBefore;

PVOID First;

PVOID Next;

PVOID Remove;

PVOID RemoveHead;

PVOID RemoveTail;

PVOID unk2;

PVOID IsEmpty;

};

struct MicroTask

{

MicroTaskVtable* vtable;

PVOID self1; // ptr to itself

PVOID self2; // ptr to itself

DWORD_PTR unk1;

PVOID MemoryAllocator;

PVOID CurrentListItem;

PVOID PreviousListItem;

DWORD ListSize;

DWORD unk4; // Initialized as NULL

char ListName[50];

};

struct MicroScanVtable

{

PVOID Constructor;

PVOID GetTask;

};

struct MicroScan

{

MicroScanVtable* vtable;

DWORD Tag; // Always 'PANS'

char pad1[4];

DWORD64 TasksSize;

MicroTask Tasks[4];

};

For most IOCTL requests in this helper table, the MicroScan structure is populated on the client side that calls the driver. Demirkapi decided to exploit this exact behavior as a potential vulnerability.

The Exploit

Bill Demirkapi admits that while reversing the functions stored in this auxiliary dispatch table, he was completely puzzled. It turned out that the kernel pointer MicroScan, returned by functions like GetProcessesAllMethods, was being passed directly to other client-side functions, such as DeleteTaskResults. These functions accept this untrusted kernel pointer and, with almost no validation, invoke functions from the virtual function table defined in the class.

Invoking functions through the obtained pointer
Invoking functions through the obtained pointer

If you look closely at the “verification subroutine” of the DeleteTaskResults helper dispatch table function, you can see that for the MicroScan instance specified in the input buffer at offset +0x10, there is effectively only one check: it just verifies that the instance holds a valid kernel memory address.

Checking the MicroScan instance
Checking the MicroScan instance

There is one more simple check used in DeleteTaskResults.

Checking the Tag field in a MicroScan instance
Checking the Tag field in a MicroScan instance

Next, the DeleteTaskResults function calls the constructor specified in the MicroScan instance’s virtual function table. To invoke an arbitrary kernel function this way, the following conditions must be met.

  • Allocate at least 10 bytes of kernel memory (for the vtable and tag).
  • Manage that kernel memory to set the virtual function table pointer and the tag parameter.
  • Learn how to determine the address of that kernel memory from user mode.

The researcher writes that the right approach to finding a way to allocate and manage kernel memory from user mode was suggested to him by his mentor, Alex Ionescu. In the Hack In The Box magazine back in 2010, an article by Mateusz “j00ru” Jurczyk titled “Windows 7 Reserved Objects” was published. The core idea is that you can craft an APC queue for an APC Reserve Object in a special way, and then use NtQuerySystemInformation to locate the desired APC Reserve Object in kernel memory. This lets a user‑mode application allocate and control up to 32 bytes in kernel memory (in a 64‑bit environment) and determine exactly where that memory is located.

This technique still works in Windows 10, which means we can meet all the requirements. By using a spare Apc object, we can allocate at least 10 bytes for the MicroScan structure and completely bypass the checks mentioned above. As a result, we gain the ability to invoke arbitrary kernel pointers.

Invoking arbitrary kernel pointers using spare APC objects
Invoking arbitrary kernel pointers using spare APC objects

Bill Demirkapi concluded that this type of vulnerability is not limited to the DeleteTaskResults function, but affects all dispatcher functions marked with an asterisk in the table. They all trust the kernel pointer supplied by the client and invoke the constructor from the virtual function table of the MicroScan instance without any additional validation.

IoControlCode == 90004033h

This dispatch table controls the TrueApi class and includes the following functions.

Opcode PrimaryRoutine Function Description
EA60h IoControlGetTrueAPIPointer Retrieves function pointers from the TrueApi class
EA61h IoControlGetUtilityAPIPointer Retrieves pointers to the driver’s utility functions
EA62h IoControlRegisterUnloadNotify* Registers a callback function to be called on driver unload
EA63h IoControlUnRegisterUnloadNotify Unregisters a previously registered unload callback function

The IoControlRegisterUnloadNotify function first caught Bill Demirkapi’s attention because it appeared in debugging strings. By using this function from the auxiliary dispatch table, an “untrusted client” can register up to 16 arbitrary routines that will be invoked when the driver is unloaded.

The validator for this function checks whether the pointer from the client-side buffer is valid. If the caller is running in user mode, the validator calls ProbeForRead on the pointer. If the caller is running in kernel mode, the validator checks that the pointer refers to a valid kernel memory address.

This function can’t be used directly in a user‑mode exploit. The problem is that if we invoke it from user mode, we have to pass it a user‑mode pointer, because the validator uses ProbeForRead. When the driver is unloaded, this user‑mode pointer is called, but it doesn’t do much because of restrictions imposed by SMEP (Supervisor Mode Execution Prevention), a memory page protection mechanism. We’ll come back to this quirk later.

IoControlCode == 900040DFh

This auxiliary dispatch table is used to interact with the Xray API. While the Xray API is normally used by the core scanning engine, the functions in this table provide limited access for the client to interact with physical disks.

Opcode PrimaryRoutine Function Description
15F90h IoControlReadFile Reads a file directly from disk
15F91h IoControlUpdateCoreList Updates kernel pointers used by the Xray API
15F92h IoControlGetDRxMapTable Retrieves the table mapping disks to their corresponding devices

IoControlCode == 900040E7h

The last dispatch table examined by the researcher is used to search various system structures for hooks that might be used by different rootkits. It lists the checks performed by Trend Micro software to detect hooked function calls and other types of hooks.

Opcode PrimaryRoutine Function Description
186A0h TMXMSCheckSystemRoutine Checks certain system routines for hooks
186A1h TMXMSCheckSystemFileIO Checks core file I/O functions for hooks
186A2h TMXMSCheckSpecialSystemHooking Checks the file object type and the Itoskrnl Io function for hooks
186A3h TMXMSCheckGeneralSystemHooking Checks the Io Manager for hooks
186A4h TMXMSCheckSystemObjectByName Recursively traces system objects (directory or symbolic link)
186A5h TMXMSCheckSystemObjectByName2* Copies a system object into user‑mode memory

Before diving into a detailed analysis of the TMXMSCheckSystemObjectByName2 function, Bill Demirkapi presents the disassembled listing of several structures used by this function:

struct CheckSystemObjectParams

{

PVOID Src;

PVOID Dst;

DWORD Size;

DWORD* OutSize;

};

struct TXMSParams

{

DWORD OutStatus;

DWORD HandlerID;

CHAR unk[0x38];

CheckSystemObjectParams* CheckParams;

};

The TMXMSCheckSystemObjectByName2 function takes a source pointer, a destination pointer, and a size in bytes.
The validator invoked for TMXMSCheckSystemObjectByName2 checks the following:

  • ProbeForRead in the CheckParams object of the TXMSParams structure
  • ProbeForRead and ProbeForWrite in the Dst object of the CheckSystemObjectParams structure

In essence, this means we need to pass a valid CheckParams structure, while the Dst pointer we pass in resides in user‑mode memory. Now let’s look at the function itself.

TMXMSCheckSystemObjectByName2 function
TMXMSCheckSystemObjectByName2 function

The for loop may look intimidating, but all it really does is provide an optimized way to scan a range of kernel memory. For each memory page in the range from Src to Src + Size, the function calls MmIsAddressValid. The truly scary parts are the operations that follow.

Copying `Size` into the `Dst` pointer
Copying `Size` into the `Dst` pointer

These lines take an untrusted Src pointer and copy Size bytes into an untrusted Dst pointer. We can use memmove operations to read from an arbitrary kernel pointer, but what about writing to an arbitrary kernel pointer? The problem is that the TMXMSCheckSystemObjectByName2 checker requires the destination to be user‑mode memory. Fortunately, Bill Demirkapi found another bug in the code.

The line *params->OutSize = Size; takes the Size field from our structure and writes it into the pointer specified by OutSize. There’s no check on what OutSize actually points to, so we can write an arbitrary DWORD in any IOCTL call. One caveat: the Src pointer must refer to valid kernel memory whose length in bytes matches the value in Size. To satisfy this requirement, Demirkapi simply used the base address of the ntoskrnl module as the source.

Using this arbitrary write primitive, we can reuse the previously discovered unload-procedure trick to execute arbitrary code. Although the validation routine doesn’t let us pass a kernel pointer when the call originates from user mode, there’s actually no need to go through the validator at all. Instead, with our write primitive we can place any pointer we want into the unload-procedure array located in the driver’s .data section.

Bypassing Microsoft’s WHQL Certification

So far we’ve covered methods for reading and writing arbitrary kernel memory, but we’re still missing one step to deploy our own rootkit. While it’s possible to run kernel shellcode using only a read/write primitive, Bill Demirkapi suggested taking the path of least resistance. Since we’re dealing with a third‑party driver, it’s very likely using NonPagedPool allocations, which we can repurpose to place and execute our malicious shellcode.

Let’s take a look at how Trend Micro handles memory allocation. Right at the driver’s entry point, the program checks whether the operating system is supported by determining its version and build number. Trend Micro does this because the program has several hard‑coded offsets that differ between various editions of Windows.

Fortunately, the global variable PoolType, which is used when allocating nonpaged memory (NonPagedPool), is set to 0 by default. Bill Demirkapi noticed that although this value starts out as 0, the variable still resides in the .data section, meaning it can be modified. When he looked at what was being written into the variable, he found that the function responsible for checking the operating system version was, in some cases, also setting the value of PoolType.

Setting the value of the PoolType variable
Setting the value of the PoolType variable

If the computer is running Windows 10 or a more recent version of Windows, the driver prefers to use NonPagedPoolNx. Good for security, but bad for our purposes. To exploit the vulnerability, we need to find an alternate ExAllocatePoolWithTag call with a hard‑coded NonPagedPool argument; otherwise we won’t be able to use the driver’s allocated memory on Windows 10. But that’s not so easy. What about the MysteriousCheck() function?

The `MysteriousCheck()` function
The `MysteriousCheck()` function

The MysteriousCheck() function checks whether Microsoft Driver Verifier is enabled. Instead of simply using the NonPagedPoolNx flag, which is supported starting with Windows 8, Trend Micro performs an explicit check so it can use only safe memory allocation.

Trend Micro’s driver is WHQL-certified, and obtaining that certificate requires driver verification. In Windows 10, drivers are not allowed to allocate executable memory, and Driver Verifier enforces this rule. Trend Micro chose to ignore these security requirements and implemented its driver so that it would work in any testing or debugging environment that detects such violations.

Trend Micro’s driver is WHQL‑certified
Trend Micro’s driver is WHQL‑certified

Trend Micro could have just relied on the standard Windows 10 check, so why explicitly add a separate check for Driver Verifier at all? The only working theory, originally suggested by Bill Demirkapi, is that for some reason most of their drivers are incompatible with NonPagedPoolNx (only their entry point is compatible); otherwise, tricks like this make no sense.

Installing Your Own Rootkit with RootkitBuster

So how can you actually use RootkitBuster to plant your own rootkit in a system? It only takes three simple steps.

  • Locate any NonPagedPool allocation that the driver is currently using. As long as Driver Verifier is not enabled, you can rely on any non-paged allocations whose pointers are stored in the .data section. Ideally, pick an allocation that is not used very often.

  • Write your kernel shellcode somewhere inside that allocated memory, using the arbitrary kernel write primitive TMXMSCheckSystemObjectByName2.

  • Execute your shellcode by registering an unload routine (directly in the .data section), or by using one of the other methods exposed via the dispatch table at 0x90004027.

That’s basically it. In his research, Demirkapi notes that the tmcomm.sys driver developed by Trend Micro looks like pieces of code roughly duct-taped together. While the developers did implement some security measures, a large portion of the code inside the IOCTL handlers uses rather unsafe techniques that attackers could easily abuse for malicious purposes. Moreover, the tmcomm.sys driver is used not only in the RootkitBuster utility, but also in other Trend Micro products, which means those potential vulnerabilities affect them as well.


文章来源: https://hackmag.com/security/trendmicro-rootkit
如有侵权请联系:admin#unsafe.sh