0%

Windows DLL 注入技术全景:七种方式一张图

为什么注入有这么多花样

DLL 注入回答的是同一个问题:怎么让你的代码在别人的进程里跑起来。答案有很多个,因为 Windows 提供了多条”合法通道”——消息钩子、远程线程、调试器、APC 队列、输入法框架——每一条都能借来送 DLL 进门。学这一块的目标不是把每种方式的 API 步骤背下来(不现实也没必要),而是建立全景认知:看到一种注入手法,知道它借的是哪条通道、有什么前提和短板。

这篇笔记按王端明《深入浅出 Windows API 程序设计:核心编程篇》的章节组织,把七种方式连同配套工程一起过一遍。所有工程都在随书源码 Chapter6(个别在 Chapter4/8)。

1. Windows 钩子注入(HookDll / DIPSHook)

上一篇详细讲过,这里只收个尾。给目标线程装远程钩子(SetWindowsHookEx),系统为了能在目标进程里执行钩子回调,会主动把你的 DLL 映射进去。钩子是”门票”,注入才是目的——DIPS 工程装完钩子后根本不在乎消息,直接在 Explorer 里弹出自己的对话框干活。

通道:消息机制。前提:目标是 GUI 进程(有消息循环)。

2. 远程线程注入(RemoteApp + RemoteDll)

教科书正招,三步曲:

1
2
3
4
VirtualAllocEx(hProcess, ...);              // 在目标进程申请内存.
WriteProcessMemory(hProcess, ..., dllPath); // 写入 DLL 路径.
CreateRemoteThread(hProcess, ..., // 在目标进程创建线程,
LoadLibraryW, lpRemotePath); // 线程函数就是 LoadLibraryW.

巧妙之处:CreateRemoteThread 的线程函数原型和 LoadLibraryW 签名兼容(都是一个指针参数),所以不用写任何 shellcode,直接让目标进程的新线程替你去 LoadLibrary。卸载同样思路:远程线程跑 FreeLibrary。

配套工程注入后会把目标进程的模块清单回显出来,注入是否成功一目了然。

3. 函数转发机制(FunctionForwarderDll / AheadLib)

严格说这不是”注入进程”,而是劫持 DLL 解析:做一个和系统 DLL 同名的假 DLL,让程序加载你而不是它。假 DLL 里大部分导出用一行 pragma 转发给真 DLL:

1
#pragma comment(linker, "/export:MyMessageBox=User32.MessageBoxW")

调用方拿到的 MyMessageBox 其实就是 MessageBoxW,功能完全正常——但转发链条上你可以对要拦截的函数加私货。书上还配了 AheadLib 工具,自动生成这种转发 DLL 的骨架代码,打算后面拿到虚拟机里试。

4. CreateProcess 写 shellcode 注入(CreateProcessInjectDll)

不创建远程线程,而是劫持目标进程的初始线程:

1
2
3
4
5
CreateProcess(..., CREATE_SUSPENDED)     // 以挂起状态创建目标进程.
VirtualAllocEx + WriteProcessMemory // 写入一段 shellcode:
// 保存现场 -> LoadLibraryW(dll) -> 恢复现场 -> 跳回原入口.
GetThreadContext / SetThreadContext // 把 EIP 掰到 shellcode.
ResumeThread // 放行, 目标先跑你的代码, 再若无其事继续.

shellcode 是 29 字节硬编码 x86 汇编(pushad/pushfd 保护现场,完事后跳回原 EIP),所以只能编 32 位。比方式 2 隐蔽——不产生额外线程,目标进程线程数看起来正常。

5. 调试器写入 shellcode(Chapter4/LoadTest2)

书上这节没给 DLL 注入的完整代码,配套资源里的示例是调试器改代码,原理相通:

1
2
3
4
5
CreateProcess(..., DEBUG_ONLY_THIS_PROCESS);   // 以调试器身份创建进程.
WaitForDebugEventEx(&debugEvent, INFINITE); // 等调试事件.
// CREATE_PROCESS_DEBUG_EVENT 到达时进程代码还没跑,
// 此时 ReadProcessMemory / WriteProcessMemory 可以随便改.
ContinueDebugEvent(...); // 放行.

调试器对被调试进程有完全控制权(这正是调试器的工作原理),改指令、写 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 注入是安全软件、外挂、恶意软件的共用技术,攻防同源。以上实验只在自己的测试机和虚拟机里做,目标进程选记事本这类无害程序,不对他人机器和任何生产环境使用。