为什么注入有这么多花样
DLL 注入回答的是同一个问题:怎么让你的代码在别人的进程里跑起来。答案有很多个,因为 Windows 提供了多条”合法通道”——消息钩子、远程线程、调试器、APC 队列、输入法框架——每一条都能借来送 DLL 进门。学这一块的目标不是把每种方式的 API 步骤背下来(不现实也没必要),而是建立全景认知:看到一种注入手法,知道它借的是哪条通道、有什么前提和短板。
这篇笔记按王端明《深入浅出 Windows API 程序设计:核心编程篇》的章节组织,把七种方式连同配套工程一起过一遍。所有工程都在随书源码 Chapter6(个别在 Chapter4/8)。
1. Windows 钩子注入(HookDll / DIPSHook)
上一篇详细讲过,这里只收个尾。给目标线程装远程钩子(SetWindowsHookEx),系统为了能在目标进程里执行钩子回调,会主动把你的 DLL 映射进去。钩子是”门票”,注入才是目的——DIPS 工程装完钩子后根本不在乎消息,直接在 Explorer 里弹出自己的对话框干活。
通道:消息机制。前提:目标是 GUI 进程(有消息循环)。
2. 远程线程注入(RemoteApp + RemoteDll)
教科书正招,三步曲:
1 | VirtualAllocEx(hProcess, ...); // 在目标进程申请内存. |
巧妙之处:CreateRemoteThread 的线程函数原型和 LoadLibraryW 签名兼容(都是一个指针参数),所以不用写任何 shellcode,直接让目标进程的新线程替你去 LoadLibrary。卸载同样思路:远程线程跑 FreeLibrary。
配套工程注入后会把目标进程的模块清单回显出来,注入是否成功一目了然。
3. 函数转发机制(FunctionForwarderDll / AheadLib)
严格说这不是”注入进程”,而是劫持 DLL 解析:做一个和系统 DLL 同名的假 DLL,让程序加载你而不是它。假 DLL 里大部分导出用一行 pragma 转发给真 DLL:
1 |
调用方拿到的 MyMessageBox 其实就是 MessageBoxW,功能完全正常——但转发链条上你可以对要拦截的函数加私货。书上还配了 AheadLib 工具,自动生成这种转发 DLL 的骨架代码,打算后面拿到虚拟机里试。
4. CreateProcess 写 shellcode 注入(CreateProcessInjectDll)
不创建远程线程,而是劫持目标进程的初始线程:
1 | CreateProcess(..., CREATE_SUSPENDED) // 以挂起状态创建目标进程. |
shellcode 是 29 字节硬编码 x86 汇编(pushad/pushfd 保护现场,完事后跳回原 EIP),所以只能编 32 位。比方式 2 隐蔽——不产生额外线程,目标进程线程数看起来正常。
5. 调试器写入 shellcode(Chapter4/LoadTest2)
书上这节没给 DLL 注入的完整代码,配套资源里的示例是调试器改代码,原理相通:
1 | CreateProcess(..., DEBUG_ONLY_THIS_PROCESS); // 以调试器身份创建进程. |
调试器对被调试进程有完全控制权(这正是调试器的工作原理),改指令、写 shellcode、下断点都可以。示例工程把目标入口附近 2 字节改成 NOP(0x9090)。注意:示例里偏移量和校验值是写死的,只能配书上原版 Test.exe,自己重编的目标会因为偏移不同而报”版本不符”。
6. APC 注入(APCInjectApp)
前半段和方式 2 相同(申请内存、写 DLL 路径),但最后不创建新线程,而是枚举目标进程的所有线程,挨个把 LoadLibraryW 排进它们的 APC 队列:
1 | QueueUserAPC(pfnLoadLibraryW, hThread, (ULONG_PTR)lpDllPathRemote); |
APC(异步过程调用)的特点是:排队的函数只在线程进入可告警等待(alertable wait,如 SleepEx、WaitForSingleObjectEx)时才执行。所以注入后目标进程可能毫无动静,去点点它的菜单、敲敲键盘,DLL 才被加载。不产生新线程,更隐蔽;触发时机不受你控制,这是代价。
7. 输入法注入(MyIME + MyIMEInstaller + MyIMETestDll)
借系统框架的东风。输入法(.ime)本质是特殊 DLL,任何进程只要切换到该输入法,系统就把它加载进那个进程——注入是输入法框架的”本职工作”。工程分三件套:Installer 注册输入法,MyIME.ime 被加载时从共享内存读出 MyIMETestDll 的路径并 LoadLibrary。
使用门槛也最高:需要管理员权限注册输入法,位数必须匹配(64 位进程要 x64 版 .ime),用完要卸载清理。
七种方式一张表
| 方式 | 借的通道 | 产生新线程 | 隐蔽性 | 前提条件 |
|---|---|---|---|---|
| 钩子注入 | 消息机制 | 否 | 中 | 目标是 GUI 进程 |
| 远程线程 | CreateRemoteThread | 是 | 低(线程数露馅) | OpenProcess 权限 |
| 函数转发 | DLL 解析顺序 | 否 | 高 | 能替换/放置同名 DLL |
| CreateProcess shellcode | 劫持初始线程 | 否 | 高 | 能启动目标进程;x86 |
| 调试器写入 | 调试子系统 | 否 | 高 | 调试权限 |
| APC | 线程 APC 队列 | 否 | 高 | 线程进入可告警等待 |
| 输入法 | 输入法框架 | 否 | 高 | 管理员;位数匹配 |
我的取舍
作者把七种方式摆出来,用意是告知注入技术有多种实现,而不是要求每种都烂熟。我的学习策略:
- 方式 1(钩子)和方式 2(远程线程)吃透——一个代表”借系统设施”,一个代表”自己动手”,API 调用链都亲手跑过。
- 方式 4-7 知道原理和适用场景即可,真用到时回来翻代码。工具类(AheadLib)后续在虚拟机里试,用到再消化。
- 书上的案例偏教学演示、实用性一般,后续打算用更接近实战的方式巩固:比如自己写一个带注入需求的小工具,把几种方式在同一靶子上各实现一遍,对比效果。
一句话总结全部:注入 = 找一条能让目标进程替你执行 LoadLibrary 的通道。七种方式的区别只在”通道”是什么。
安全边界
DLL 注入是安全软件、外挂、恶意软件的共用技术,攻防同源。以上实验只在自己的测试机和虚拟机里做,目标进程选记事本这类无害程序,不对他人机器和任何生产环境使用。