Bypassing the Microsoft-Windows-Threat-Intelligence Kernel APC Injection Sensor

Philip Tsukerman
Sep 23 · 5 min read

Using APCs (Asynchronous Procedure Calls) as a method to inject user-mode code into processes from the Windows kernel is hardly a new technique, but it is still extremely relevant both as a method for various security products to execute code in a target process, and as a core technique of rootkits such as DOUBLEPULSAR. For a good (and well documented) example of an APC injector driver you might want to look at injdrv by Petr Beneš. Windows 10, version 1809 has introduced a kernel sensor instrumenting APC queuing to try and detect malicious usage of this mechanism, as described in a blogpost by Microsoft. Driven by a combination of curiosity and a random remark by Bruce Dang during one of his fantastic training sessions, I have decided to break it.

The sensor itself is exposed as the THREATINT_QUEUEUSERAPC_REMOTE_KERNEL_CALLER event of the Microsoft-Windows-Threat-Intelligence ETW provider, available to processes under AntiMalware protection and higher (this means it’s officially only available to security vendors). To see how and when this event fires, let’s take a look at the KeInsertQueueApc function, which is responsible for attaching an initialized APC to the APC queue of its target thread:

Somewhat annotated Hex-Rays output for KeInsertQueueApc

As can be seen from the decompiled code, the function first checks if the relevant ETW provider is enabled. A flag (I have elegantly named DoEtwTiEvent) is then raised if the following conditions are true: The current process is not the same as the process of the thread to which the APC is queued, the Threat-Intelligence provider is enabled, and the APC getting queued is a user-mode APC (meaning that the NormalRoutine argument is a pointer to a function to be called in user mode) or that this is a kernel APC, with the KernelRoutine being a specific function (KeSpecialUserApcKernelRoutine).

Disregarding the special kernel APC routine, which we are not going to use anyway, we have a few sub-conditions we might want to control to circumvent this check:

— We could try and disable the Threat-Intelligence provider or corrupt the provider handle, but this is a type of tampering which could be detected, and we prefer to have the provider running as usual, while being blind only to our actions.

— Another option is to only inject from callbacks set by PsSetCreateThreadNotifyRoutine (already described here by Souhail Hammou). This is great for AV vendors who would like to inject at process start, but will make us dependent on thread creation, which will not always happen when we want in our target process.

— There is also the possibility do some weird, hardcoded-offset/heuristic stuff with the current ETHREAD, but we’re trying to be respectable(?) rootkit authors(??) here, so let’s just avoid the endless blue-screen party which attempting this approach will inevitably become.

— My choice for approaching this issue was to use an unmodified user-mode APC to inject, and to simply find a way to queue it from the context of the target thread. But how could we execute the code to queue our APC in the context of a thread of our choosing? The answer to this lies in a mechanism you might recognize, called Asynchronous Procedure Calls.

1) This meme is way too old. 2) No. Just no.

We will initialize the injecting APC without any modifications, but will not queue it immediately. Instead, we will first queue a different APC to the thread:

// Initialize injecting APC
KeInitializeApc(TargetApc, TargetThread, OriginalApcEnvironment, TargetApcKernelCleanup, NULL, (PKNORMAL_ROUTINE)LoadLibraryAddress, UserMode, sectionAddress);
// Initialize Proxy APC
KeInitializeApc(ProxyApc, TargetThread, OriginalApcEnvironment, ProxyApcRoutine, NULL, NULL, KernelMode, NULL);
//Queue proxy APC to target thread, with injecting APC as argument
KeInsertQueueApc(ProxyApc, TargetApc, LibraryName, 0);

This kernel-mode “proxy” APC will have very little functionality:

VOID NTAPI ProxyApcRoutine(
_In_ PKAPC Apc,
_Inout_ PKNORMAL_ROUTINE* NormalRoutine,
_Inout_ PVOID* NormalContext,
_Inout_ PVOID* SystemArgument1,
_Inout_ PVOID* SystemArgument2
)
{
KeInsertQueueApc(*(PKAPC*)SystemArgument1, SystemArgument2, NULL, 0);
KeTestAlertThread(UserMode);
ExFreePoolWithTag(Apc, POOL_TAG);
return;
}

Pretty much all it does is take the first SystemArgument, and treat is as an APC to queue to its thread. Seeing as we have provided our injecting APC as the parameter, this is what’s getting queued. The routine then calls KeTestAlertThread to force the execution of our user routine (LoadLibrary, for example) upon return to user mode.

Now, let’s dissect the two calls to KeInsertQueueApc:

— The first call queues our proxy APC. This is a kernel-mode APC, which invalidates the “ApcMode==UserMode” clause of the event condition. No event is fired. This APC will allow us to execute the ProxyApcRoutine from kernel mode in the target thread. This is represented by the lower blue arrow in the diagram.

— The second call is our target injecting APC. While this is a user-mode APC, this one is queued from inside of our proxy routine. This routine, as you might remember, executes in the context of the target thread inside of our proxy APC, meaning that the target process and current process are the same, again invalidating the condition and preventing the publishing of the event. Here we triggering the execution of the payload itself, which will be run in user-mode inside of our target process. The action is represented by the blue arrow on the right.

This simple two-step approach breaks apart the main clauses of the condition to trigger the event, and avoids queuing an APC in a manner that traverses the red arrow in the diagram. There’s no need to modify the injected code and the target APC itself to make it work with this method.

So that’s it. Unless I made some really silly mistake and missed something while debugging this (which is always an option for me), you can now resume injecting libraries into arbitrary processes from the kernel, without getting detected by that pesky ETW event :)

P. S. If you queue your APC (without using this trick) from the context of an IOCTL handler, the PreviousMode will be UserMode. This will not circumvent the sensor, but the EtwTiLogInsertQueueUserApc function will write an event that describes an APC being queued from user mode by the SYSTEM process.