Title | Date | Author | |
---|---|---|---|
APC Queue Code Injection |
05 May 2021 |
Soufiane Fariss |
Injection techniques that rely on creating a remote thread in the target process to execute the shellcode might cause a huge increase in the malware confidence score which will raise a lot of suspicion among security products. Nevertheless, these techniques create a new thread, which causes a lot of overhead because of allocating new resources to get the thread up and running.
So, is there a way we can run shellcode without resorting to creating a new thread? The answer is yes!
Threads are able to run asynchronous tasks (Asynchronous Procedure Calls), though they can only do it one at time. If and only if, a thread enters an alterable
state, it checks its APC queue for queued tasks to run. We can take advantage of this and queue a malicious task (APC) for it to get executed by the thread.
Though, there's no guarantee that the thread will run the shellcode. Because, as we previously stated, a thread can't switch between APCs unless it enters an alterable
state. This happens when one of the following APIs is called SleepEx
, WaitForSignalObject
, WaitForMultipleObjects
.. etc.
Picture courtesy of Dwight Hohnstein
To carry out this code injection techniques, we first need to find the target (remote) process PID, allocate and copy our shellcode to the process memory, enumerate all the remote threads, finally queue our code to their respective APC queues and wait for one of them to enter an alterable
state.
This flow is presented as sequence diagram:
We suppose that only one thread is running in svchost.exe
and that is the the main thread.
sequenceDiagram
participant malware.exe
participant svchost.exe
activate svchost.exe
malware.exe ->> malware.exe: Enumerate running processes
Note left of malware.exe: Malware.exe retrieves<br/> the PID of svchost.exe
activate svchost.exe
malware.exe ->> svchost.exe: OpenProcess()
malware.exe ->> svchost.exe: VirtualAlloc()
malware.exe ->> svchost.exe: WriteProcessMemory()
Note over malware.exe,svchost.exe: Malware.exe opens a handle<br/> to the remote process,<br/> allocates virtual memory <br/>and, copies shellcode to memory
malware.exe ->>svchost.exe: Enumerate remote threads<br>and OpenThread()
malware.exe ->>svchost.exe: QueueUserAPC()
deactivate svchost.exe
Note over malware.exe,svchost.exe: After queueing, wait<br> for a thread to enter an alterable state
rect rgb(191, 223, 255)
svchost.exe-->> svchost.exe: Thread enters alterable state
Note right of svchost.exe: Thread fetches a queued APC<br> from its APC Queue
end
activate svchost.exe
svchost.exe->>svchost.exe: Resume execution
Note right of svchost.exe: Thread resumes execution <br>and runs shellcode
deactivate svchost.exe
Blue rectangle means, main thread has entered an alterable state.
Note: In most case, allocating a RWX using VirtualAllocEx()
with PAGE_EXECUTE_READWRITE
could trigger an EDR/AV. A quick workaround is to first allocate a RW region with PAGE_READWRITE
(Read, Write) permissions then using VirtualProtectEx
to make the allocation executable. However, the payload has to be non-self-changing, meaning it doesn't write back to memory since it doesn't have the required permission
Demo:
The simple (or classic) APC queue injection techniques that we introduced the previous section, involves queuing an already-running remote process' threads. This is however, is not optimal when it comes to avoiding EDR hooks. When a process is already running and hooks are present, they can simply intercept the malicious code.
So we might ask ourselves, is there a way we can avoid EDR/AV hooks when deploying our shellcode?
The idea revolves around creating a legitimate process in a SUSPENDED
state, queuing an APC to the main thread, then resuming the thread. In order to trigger execution the malware uses an asynchronous procedure call and enforces execution of the APC call using the NtTestAlert
function in the thread initialization phase, avoiding hooks placed by EDR.
Note: NtTestAlert
is an undocumented API. Though, it is called after the main thread initialization and before the the thread starts to check if APC jobs are queued. If yes, it notifies the kernel (By calling KiUserApcDispatcher
), otherwise it has no effect.
So, if we queued an APC, then start main thread, NtTestAlert
is called, which will immediately process the malicious APC before the process main routine!
This variant of APC Queue injections is dubed "Early Bird" referring to the early stage of queuing the APC, and is famously used by "TURNEDUP" malware attributed to APT33.
Here is a simplified flowchart:
graph TB
id[Create SUSPENDED process] --> id2[Allocate virtual memory]
id2[Allocate virtual memory] --> id3[Copy shellcode to memory]
id3[Copy shellcode to memory] --> id4[Queue APC] --> id5[Resume Thread]
Demo: