Monitor handle acquisition

Acquiring a handle to a target process is a critical step in many code injection techniques. The Windows operating system exposes a mechanism that allows kernel mode drivers to supply handle operation callbacks. These callbacks can be registered by calling ObRegisterCallbacks. When a handle is created or duplicated the pre-operation callback is invoked before the operation is performed and the post-operation callback after the operation occurred. This mechanism is utilized by many antivirus and Anti-Cheat solutions to protect processes from code injection. For more information about how this mechanism can be employed to protect processes and how to overcome these protections take a look at this excellent article by Daax Rynd.

Since I do not own a valid driver certificate our first task is to load our unsigned driver. This can be accomplished by enabling TESTSIGNING. Enabled test signing can be detected from user mode by querying the SystemCodeIntegrityInformation through NtQuerySystemInformation. Therefore, it is reasonable to assume that whatever we want to monitor may check whether test signing is enabled and alter its behavior accordingly. Therefore, we choose the other option which is loading the driver using an exploit.

Kdmapper

kdmapper exploits a vulnerable Intel driver to manually map unsigned drivers. After we define a custom entry point for our driver, it can be successfully mapped by kdmapper. The downside of loading our driver this way is that we do not have a DRIVER_OBJECT.

ObRegisterCallbacks

As discussed previously ObRegisterCallbacks requires the callbacks to reside in signed kernel images. This is an issue because our manually mapped kernel mode driver does not have a valid DRIVER_OBJECT. The following call chain is invoked when we attempt to register a handle operation callback from our driver:

ObRegisterCallbacks 
	| // 0x20 = LDRP_VALID_SECTION flag
	|--	MmVerifyCallbackFunctionCheckFlags(ourCallbackFunction, 0x20)	
		| // looks up in which module ourCallbackFunction resides
		|--	MiLookupDataTableEntry
			| // checks if our driver has the required flag 										
			|--	DriverObject->DriverSection->flags & 0x20   			

Since our DRIVER_OBJECT is not valid because we mapped it with kdmapper, we do not have the required flag. However, if you are running Windows 7 you can use DSEFix to map your driver which comes with the added benefit of having a valid DRIVER_OBJECT. Therefore, on Windows 7 the trick we describe in the next section is not required to register handle operation callbacks.

One weird trick

AdrianVPL suggested a trick to bypass this limitation in a forum post. To understand it we take a look at the signature of the pre-operation callback:

OB_PREOP_CALLBACK_STATUS PobPreOperationCallback(
	PVOID RegistrationContext
	POB_PRE_OPERATION_INFORMATION OperationInformation
)
{...}

The calling convention of PobPreOperationCallback is fastcall. This means that the first parameter (RegistrationContext) is passed in the RCX register and the second parameter (OperationInformation) is passed in RDX. The interpretation of the registration context is driver-defined which in turn means that the driver itself is the only code accessing it.

The idea is to set the registration context to the address of our pre-operation callback and set the pre-operation field which would usually hold the callback to an address containing a JMP RCX instruction inside a valid driver. This results in ObRegisterCallbacks validating whether the driver containing the JMP RCX instruction is valid instead of checking our unsigned driver. When the callback is executed, the JMP RCX instruction jumps to the value in the registration context, which is the real callback inside our unsigned driver.

In his post, AdrianVPL suggested abusing ntoskrnl.exe which is signed and contains multiple JMP RCX instructions. However, abusing ntoskrnl did not work on several Windows 10 versions we tested. We conducted kernel mode debugging and noticed that ntoskrnl is missing a required flag that is checked when attempting to register a handle operation callback. We solved this issue by iterating currently running drivers and searching a signed driver with the required flag and a JMP RCX instruction. On our system one such driver is the DirectX Graphics Kernel which our current implementation abuses.

Our prototype implementation is available here: https://github.com/Fahersto/kernel_handle_monitoring