Earlier in this chapter, you saw that by creating a thread using CreateRemoteThread
, you can invoke functionality in a remote process. However, thread
creation requires overhead, so it would be more efficient to invoke a function on an existing thread. This capability exists in Windows as the asynchronous
procedure call (APC).
APCs can direct a thread to execute some other code prior to executing its regular execution
path. Every thread has a queue of APCs attached to it, and these are processed when the thread is in
an alertable state, such as when they call functions like WaitForSingleObjectEx
, WaitForMultipleObjectsEx
, and
SleepEx
. These functions essentially give the thread a chance to
process the waiting APCs.
If an application queues an APC while the thread is alertable but before the thread begins running, the thread begins by calling the APC function. A thread calls the APC functions one by one for all APCs in its APC queue. When the APC queue is complete, the thread continues running along its regular execution path. Malware authors use APCs to preempt threads in an alertable state in order to get immediate execution for their code.
APCs come in two forms:
An APC generated for the system or a driver is called a kernel-mode APC.
An APC generated for an application is called a user-mode APC.
Malware generates user-mode APCs from both kernel and user space using APC injection. Let’s take a closer look at each of these methods.
From user space, another thread can queue a function to be invoked in a remote thread, using
the API function QueueUserAPC
. Because a thread must be in an
alertable state in order to run a user-mode APC, malware will look to target threads in processes
that are likely to go into that state. Luckily for the malware analyst, WaitForSingleObjectEx
is the most common call in the Windows API, and there are usually
many threads in the alertable state.
Let’s examine the QueueUserAPC
’s parameters:
pfnAPC
, hThread
, and dwData
. A call to QueueUserAPC
is a
request for the thread whose handle is hThread
to run the
function defined by pfnAPC
with the parameter dwData
. Example 12-5 shows
how malware can use QueueUserAPC
to force a DLL to be loaded in
the context of another process, although before we arrive at this code, the malware has already
picked a target thread.
During analysis, you can find thread-targeting code by looking for API calls such as
CreateToolhelp32Snapshot
, Process32First
, and Process32Next
for the malware to find the target process. These API calls will
often be followed by calls to Thread32First
and Thread32Next
, which will be in a loop
looking to target a thread contained in the target process. Alternatively, malware can also use
Nt/ZwQuerySystemInformation
with the
SYSTEM_PROCESS_INFORMATION
information class
to find the target process.
Example 12-5. APC injection from a user-mode application
00401DA9 push [esp+4+dwThreadId] ; dwThreadId
00401DAD push 0 ; bInheritHandle
00401DAF push 10h ; dwDesiredAccess
00401DB1 call ds:OpenThread ❶
00401DB7 mov esi, eax
00401DB9 test esi, esi
00401DBB jz short loc_401DCE
00401DBD push [esp+4+dwData] ; dwData = dbnet.dll
00401DC1 push esi ; hThread
00401DC2 push ds:LoadLibraryA ❷ ; pfnAPC
00401DC8 call ds:QueueUserAPC
Once a target-thread identifier is obtained, the malware uses it to open a handle to the
thread, as seen at ❶. In this example, the malware is
looking to force the thread to load a DLL in the remote process, so you see a call to QueueUserAPC
with the pfnAPC
set to
LoadLibraryA
at ❷.
The parameter to be sent to LoadLibraryA
will be contained in
dwData
(in this example, that was set to the DLL
dbnet.dll earlier in the code). Once this APC is queued and the thread goes
into an alertable state, LoadLibraryA
will be called by the
remote thread, causing the target process to load dbnet.dll.
In this example, the malware targeted svchost.exe, which is a popular target for APC injection because its threads are often in an alertable state. Malware may APC-inject into every thread of svchost.exe just to ensure that execution occurs quickly.
Malware drivers and rootkits often wish to execute code in user space, but there is no easy way for them to do it. One method they use is to perform APC injection from kernel space to get their code execution in user space. A malicious driver can build an APC and dispatch a thread to execute it in a user-mode process (most often svchost.exe). APCs of this type often consist of shellcode.
Device drivers leverage two major functions in order to utilize APCs: KeInitializeApc
and KeInsertQueueApc
. Example 12-6 shows an example of these functions in use in a
rootkit.
Example 12-6. User-mode APC injection from kernel space
000119BD push ebx 000119BE push 1 ❶ 000119C0 push [ebp+arg_4] ❷ 000119C3 push ebx 000119C4 push offset sub_11964 000119C9 push 2 000119CB push [ebp+arg_0] ❸ 000119CE push esi 000119CF call ds:KeInitializeApc
000119D5 cmp edi, ebx 000119D7 jz short loc_119EA 000119D9 push ebx 000119DA push [ebp+arg_C] 000119DD push [ebp+arg_8] 000119E0 push esi 000119E1 call edi ;KeInsertQueueApc
The APC first must be initialized with a call to KeInitializeApc
. If the sixth parameter (NormalRoutine
) ❷ is non-zero in combination
with the seventh parameter (ApcMode
) ❶ being set to 1, then we are looking at a user-mode type.
Therefore, focusing on these two parameters can tell you if the rootkit is using APC injection to
run code in user space.
KeInitializeAPC
initializes a KAPC structure, which must be
passed to KeInsertQueueApc
to place the APC object in the target
thread’s corresponding APC queue. In Example 12-6, ESI will contain the KAPC structure. Once KeInsertQueueApc
is
successful, the APC will be queued to run.
In this example, the malware targeted svchost.exe, but to make that
determination, we would need to trace back the second-to-last parameter pushed on the stack to
KeInitializeApc
. This parameter contains the thread that will be
injected. In this case, it is contained in arg_0
, as seen at
❸. Therefore, we would need to look back in the code to
check how arg_0
was set in order to see that
svchost.exe’s threads were targeted.