注入技术(下):APC注入、线程劫持、反射式DLL注入、Shellcode注入
好,咱们接着聊注入技术。上一章我们把远程线程注入、SetWindowsHookEx、注册表这些经典手法过了一遍。这一章要讲的,是更进阶、更隐蔽的几种玩法——APC注入、线程劫持、反射式DLL注入,还有Shellcode注入。
说实话,这些技术我在实际对抗中见得越来越多。尤其是现在EDR(端点检测与响应)越来越聪明,传统的CreateRemoteThread一调用,基本就暴露了。所以攻击者开始玩更花哨的——不创建新线程,不调用LoadLibrary,甚至不落地DLL文件。
嗯,咱们一个一个拆解。
APC注入:异步过程调用的妙用
APC(Asynchronous Procedure Call)是Windows提供的一种异步执行机制。每个线程都有自己的APC队列。当一个线程进入可告警等待状态(alertable wait)时,系统会依次执行队列中的APC函数。
说白了,就是你可以让一个线程在它“闲着”的时候,帮你干点私活。
APC注入的流程大致如下:
- 找到目标进程中的一个线程(通常是主线程)
- 申请一段内存,写入shellcode或DLL路径
- 调用QueueUserAPC,把我们的代码地址排进目标线程的APC队列
- 等待目标线程进入可告警等待状态(比如SleepEx、WaitForSingleObjectEx)
- APC被触发,我们的代码被执行
代码示例:
// 找到目标线程
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, dwThreadId);
// 申请内存并写入shellcode
LPVOID pShellcode = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, pShellcode, shellcode, dwSize, NULL);
// 排队APC
QueueUserAPC((PAPCFUNC)pShellcode, hThread, (ULONG_PTR)NULL);
这里有个坑——你不能保证目标线程什么时候进入可告警等待。如果它一直在跑计算密集型任务,你的APC可能永远排不上队。我在一次渗透测试中就遇到过这种情况,目标进程是个游戏客户端,主线程忙得飞起,APC排了一整天都没触发。后来我换了个线程,找了个专门处理网络I/O的线程,才搞定。
线程劫持:比APC更直接
线程劫持(Thread Hijacking),也叫SetThreadContext注入。它的思路更暴力——直接修改目标线程的上下文,让它的执行流跳到我们的代码。
具体步骤:
- 挂起目标线程(SuspendThread)
- 获取线程上下文(GetThreadContext)
- 修改指令指针(EIP/RIP)指向我们的shellcode
- 设置线程上下文(SetThreadContext)
- 恢复线程(ResumeThread)
你想想看,这相当于你正在跑步,我直接把你脑子里的导航路线改了,让你跑向我家。线程自己都不知道发生了什么。
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId);
// 挂起线程
SuspendThread(hThread);
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_FULL;
// 获取当前上下文
GetThreadContext(hThread, &ctx);
// 修改EIP指向shellcode
ctx.Eip = (DWORD)pShellcodeAddr;
// 设置新上下文
SetThreadContext(hThread, &ctx);
// 恢复线程
ResumeThread(hThread);
我个人习惯在劫持前保存原始上下文,这样事后可以恢复,避免目标进程崩溃。有一次我忘了保存,结果目标进程直接蓝屏——嗯,那是在客户的生产环境上,别提多尴尬了。
反射式DLL注入:不落地的艺术
反射式DLL注入(Reflective DLL Injection)是Stephen Fewer在2009年提出的技术。它的核心思想是:DLL自己把自己加载到内存中,不依赖系统的LoadLibrary。
为什么需要这个?因为LoadLibrary会被监控。EDR会hook ntdll!LdrLoadDll,你一调用它就知道了。反射式注入完全绕过这个——我们自己实现PE加载器,手动解析DLL的导入表、重定位表,自己搞定一切。
流程:
- 把DLL文件以字节数组形式读入内存
- 在目标进程中申请内存,写入DLL数据
- 通过远程线程或APC,执行DLL内部的反射加载器函数
- 加载器自己完成:分配内存、解析PE头、修复导入表、执行TLS回调、调用DllMain
代码层面,反射加载器通常是用汇编写的,嵌入在DLL的.data段里。我见过最精简的实现只有不到2KB。
Shellcode注入:最轻量的选择
Shellcode注入,说白了就是只注入一段可执行代码,不涉及DLL。Shellcode通常是用汇编写的,体积小、功能单一——比如弹个计算器、开个反向shell、或者执行一个系统命令。
注入方式可以是前面讲过的任何一种:远程线程、APC、线程劫持。区别在于,你注入的不是DLL路径,而是直接可执行的机器码。
Shellcode的编写有几个要点:
- 位置无关:不能使用绝对地址,所有跳转和调用必须用相对寻址
- 避免空字节:很多漏洞利用场景中,空字节会截断字符串
- API地址动态获取:通过PEB遍历kernel32的导出表,找到GetProcAddress和LoadLibrary
我见过一个很漂亮的shellcode,只有276字节,实现了完整的TCP反向连接。它用了一种叫“哈希查找”的技术——不直接写API名字符串,而是用哈希值匹配,进一步减小体积。
各种注入技术的对比
咱们用一张表来总结一下:
| 技术 | 隐蔽性 | 实现难度 | 依赖条件 | 典型检测点 |
|---|---|---|---|---|
| 远程线程注入 | 低 | 低 | CreateRemoteThread可用 | CreateRemoteThread、WriteProcessMemory |
| APC注入 | 中 | 中 | 目标线程进入alertable状态 | QueueUserAPC、APC队列异常 |
| 线程劫持 | 中高 | 中 | 可挂起/恢复目标线程 | SetThreadContext、异常EIP |
| 反射式DLL注入 | 高 | 高 | 需实现PE加载器 | 内存中PE头异常、无对应文件 |
| Shellcode注入 | 高 | 高 | 需编写位置无关代码 | 内存执行权限异常、无映射文件 |
从检测角度看,反射式DLL注入和Shellcode注入最难抓。因为它们不调用系统加载器,不产生文件映射,EDR很难通过常规的API钩子发现。
检测思路:怎么发现这些注入?
既然讲了攻击,咱们也得聊聊防御。我总结了几条实用的检测思路:
- 监控内存权限变化:如果某段内存从PAGE_READWRITE变成了PAGE_EXECUTE_READWRITE,而且没有对应的映射文件——嗯,这很可疑。
- 检查线程上下文异常:线程劫持后,EIP/RIP会指向一个非模块地址。定期扫描所有线程的上下文,看看有没有指向未知内存区域的。
- APC队列审计:虽然难度大,但可以通过内核回调监控APC的排队行为。如果某个线程的APC队列突然多了个指向非模块内存的回调,基本可以判定有问题。
- 内存扫描:反射式DLL加载后,内存中会出现完整的PE结构。扫描内存中的MZ头和PE头,然后检查这些内存区域是否有对应的磁盘文件——没有?那就是注入。
知识体系结构图
下面这张SVG图,把本章涉及的四种注入技术以及它们之间的关系梳理了一下:
这张图把四种技术的核心机制和检测方法串在了一起。你可以看到,虽然手法不同,但最终都指向同一个目标——在目标进程中执行任意代码。而检测端,也是围绕这个目标来设计的。
好了,这一章的内容就到这里。APC注入、线程劫持、反射式DLL注入、Shellcode注入——每一种都有它的适用场景和局限性。在实际对抗中,我建议你至少掌握两种以上的注入手法,因为环境变了,手法也得跟着变。
记住一句话:没有银弹。无论是攻击还是防御,都要根据具体情况灵活选择。
公众号:蓝海资料掘金营,微信deep3321