|Informative Information for the Uninformed|
In order to support thread suspension, the Windows kernel includes a KAPC field named SuspendApc in the KTHREAD structure that is associated with each thread running on a system. When thread suspension is requested, the kernel takes steps to queue the SuspendApc structure to the thread's APC queue. When the APC queue is processed, the kernel invokes the APC's NormalRoutine, which is typically initialized to nt!KiSuspendThread, from the SuspendApc structure in the context of the thread that is being suspended. Once nt!KiSuspendThread completes, the thread is suspended. The following shows what values the SuspendApc is typically initialized to:
kd> dt -r1 _KTHREAD 80558c20 ... +0x16c SuspendApc : _KAPC +0x000 Type : 18 +0x002 Size : 48 +0x004 Spare0 : 0 +0x008 Thread : 0x80558c20 _KTHREAD +0x00c ApcListEntry : _LIST_ENTRY [ 0x0 - 0x0 ] +0x014 KernelRoutine : 0x804fa8a1 nt!KiSuspendNop +0x018 RundownRoutine : 0x805139ed nt!PopAttribNop +0x01c NormalRoutine : 0x804fa881 nt!KiSuspendThread +0x020 NormalContext : (null) +0x024 SystemArgument1: (null) +0x028 SystemArgument2: (null) +0x02c ApcStateIndex : 0 '' +0x02d ApcMode : 0 '' +0x02e Inserted : 0 ''
Since the SuspendApc structure is specific to a given KTHREAD, any modification made to a thread's SuspendApc.NormalRoutine will affect only that specific thread. By modifying the NormalRoutine of the SuspendApc associated with a given thread, a backdoor can gain arbitrary code execution in kernel-mode by simply attempting to suspend the thread. It is trivial for a user-mode application to trigger the backdoor. The following sample code illustrates how a thread might execute arbitrary code in kernel-mode if its SuspendApc has been modified:
The following code gives an example of assembly that implements the technique described above taking into account the InitialStack insight described in the considerations below:
public _RkSetSuspendApcNormalRoutine@4 _RkSetSuspendApcNormalRoutine@4 proc assume fs:nothing push edi push esi ; Grab the current thread pointer xor ecx, ecx inc ch mov esi, fs:[ecx+24h] ; Grab KTHREAD.InitialStack lea esi, [esi+18h] lodsd xchg esi, edi ; Find StackBase repne scasd ; Set KTHREAD->SuspendApc.NormalRoutine mov eax, [esp+0ch] xchg eax, [edi+1ch] pop esi pop edi ret _RkSetSuspendApcNormalRoutine@4 endp
Category: Type IIa
Origin: The authors believe this to be the first public description of this technique. Skywing is credited with the idea. Greg Hoglund mentions abusing APC queues to execute code, but he does not explicitly call out SuspendApc.
Capabilities: Kernel-mode code execution.
Considerations: This technique is extremely effective. It provides a simple way of executing arbitrary code in kernel-mode by simply hijacking the mechanism used to suspend a specific thread. There are also some interesting side effects that are worth mentioning. Overwriting the SuspendApc's NormalRoutine makes it so that the thread can no longer be suspended. Even better, if the hook function that replaces the NormalRoutine never returns, it becomes impossible for the thread, and thus the owning process, to be killed because of the fact that the NormalRoutine is invoked at APC level. Both of these side effects are valuable in the context of a rootkit.
One consideration that must be made from the perspective of a backdoor is that it will be necessary to devise a technique that can be used to locate the SuspendApc field in the KTHREAD structure across multiple versions of Windows. Fortunately, there are heuristics that can be used to accomplish this. In all versions of Windows analyzed thus far, the SuspendApc field is preceded by the StackBase field. It has been confirmed on multiple operating systems that the StackBase field is equal to the InitialStack field. The InitialStack field is located at a reliable offset (0x18) on all versions of Windows checked by the authors. Using this knowledge, it is trivial to write some code that scans the KTHREAD structure on pointer aligned offsets until it encounters a value that is equal to the InitialStack. Once a match is found, it is possible to assume that the SuspendApc immediately follows it.
Covertness: This technique involves overwriting a function pointer in a dynamically allocated region of memory that is associated with a specific thread. This makes the technique fairly covert, but not impossible to detect. One method of detecting this technique would be to enumerate the threads in each process to see if the NormalRoutine of the SuspendApc is set to the expected value of nt!KiSuspendThread. It would be challenging for someone other than Microsoft to implement this safely. The authors are not aware of any tool that currently does this.