APC注入成功的核心前提是目标线程必须处于可唤醒的alertable wait状态;若线程忙于执行用户代码(如busy-loop或GUI消息循环未进入WaitMessage),QueueUserAPC虽返回成功但APC永不执行。

APC 注入的核心限制:目标线程必须处于可唤醒的等待状态
Windows APC 不能强行中断正在执行用户代码的线程;它只会在目标线程进入 WaitForSingleObject、SleepEx、MsgWaitForMultipleObjectsEx 等「alertable wait」状态时被投递。如果你直接对一个 busy-loop 或正常运行的 GUI 线程调用 QueueUserAPC,调用会成功返回,但 APC 永远不会执行。
常见误判场景:
- 试图向
explorer.exe的主线程注入 —— 它几乎从不进入 alertable wait(除非你主动触发其消息循环进入WaitMessage) - 目标线程刚创建完就立刻
QueueUserAPC—— 此时线程可能还在初始化,或已进入非 alertable 状态 - 用
CreateRemoteThread启动一个线程后立即投递 APC —— 新线程执行的是你指定的起始地址,不是 alertable 函数
如何让目标线程“配合”执行 APC:用 SleepEx(0, TRUE) 强制进入 alertable 状态
最可靠的做法不是等线程自己睡,而是用 SuspendThread + 修改上下文 + ResumeThread,把它的下一条指令改成 SleepEx(0, TRUE),让它短暂进入 alertable 等待,此时你的 APC 就能被调度执行。
关键步骤(需在目标进程内执行 shellcode):
- 用
GetThreadContext获取目标线程的CONTEXT,定位Rip(x64)或Eip(x86) - 将原指令备份(至少 12–14 字节,足够覆盖
mov rax, 0x7fff; call SleepEx类似逻辑) - 写入一段最小 shellcode:调用
SleepEx(0, TRUE),然后跳回原地址(需修复栈和寄存器) - 用
SetThreadContext更新上下文,再ResumeThread - 立刻在当前线程调用
QueueUserAPC,参数为你的远程函数地址(如LoadLibraryA地址 + 参数指针)
注意:SleepEx 在 kernel32.dll 中,不同系统版本基址不同,需先用 GetProcAddress 解析;若目标进程未加载 kernel32(极少见),需先手动映射。
QueueUserAPC 的参数陷阱:第三个参数必须是目标进程内的有效地址
QueueUserAPC 的原型是:QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData)。其中 dwData 会作为唯一参数传给 pfnAPC。很多人误以为可以传一个本地变量地址过去 —— 这会导致目标线程访问非法内存而崩溃。
正确做法:
- 用
VirtualAllocEx在目标进程中申请内存(PAGE_EXECUTE_READWRITE) - 用
WriteProcessMemory写入你的 payload(例如一段跳转到LoadLibraryA的 stub,或完整 shellcode) -
dwData填这个远程内存地址(即 payload 起始地址) -
pfnAPC填LoadLibraryA地址(需GetModuleHandleA("kernel32.dll") + offset)—— 注意:不能填你自己进程里的LoadLibraryA地址
典型错误:QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)"C:\m.dll") —— 这里 "C:\m.dll" 是你进程的地址,目标线程读它就是访问自己的无效地址。
64 位下特别注意:APC 函数签名与栈对齐
x64 调用约定要求调用者保证栈 16 字节对齐,且前 4 个参数通过寄存器(RCX、RDX、R8、R9)传递。QueueUserAPC 只传一个参数(dwData → RCX),但目标线程在 alertable wait 返回后,会以 RCX 为唯一参数调用你的 APC 函数。这意味着:
- 如果你的 APC 入口是 C++ 函数(如
void __stdcall apc_main(ULONG_PTR)),它会被正确调用 - 但如果你直接把
LoadLibraryA当作 APC 函数,它期望参数是LPCSTR,而QueueUserAPC传的是ULONG_PTR—— 类型一致,能跑,但必须确保该字符串地址在目标进程有效 - 若写自定义 shellcode,必须在开头手动
sub rsp, 28h(为影子空间腾出 32 字节),否则调用任何 Windows API 都可能崩溃
绕过栈对齐风险的稳妥方式:用一小段 stub(5–10 字节)跳转到你分配的远程代码,由那段代码完成真正的逻辑(比如调用 LoadLibraryA),而不是直接把 API 地址当 APC 函数用。
APC 注入不是“发个消息就完事”,它依赖线程状态、地址空间隔离、调用约定三重对齐。漏掉任意一环,都会静默失败 —— 没报错,也没执行。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











