在前文实现了通过 APC 将 DLL 注入到指定进程后,为了实现“无痕”操作,我们还需要在 DLL 完成任务后将其从目标进程中安全卸载。本文将深入探讨如何利用 APC 机制,卸载已注入到目标进程中的 DLL。
在 Windows 系统中,卸载进程中的 DLL 通常使用 FreeLibrary API,该函数需要一个指向目标 DLL 的模块句柄作为参数,这个句柄正是当初通过 LoadLibrary 加载 DLL 时获得的。

然而,DLL 已注入到目标进程,LoadLibrary 的调用发生在彼时,我们并未保存其返回值。因此,为了再次通过 APC 卸载它,我们需要另辟蹊径获取该 DLL 的句柄,这时 GetModuleHandle API 便派上了用场。

GetModuleHandle 可根据 DLL 名称返回其模块句柄。获得句柄后,即可调用 FreeLibrary 进行卸载(前提是该 DLL 模块的引用计数为零)。
总结来说,我们的目标是在注入的 APC 例程中,先调用 GetModuleHandle 定位 DLL 句柄,再调用 FreeLibrary 执行卸载。但这带来了新的挑战:之前注入 DLL 时,我们直接将系统 API LoadLibrary 作为 APC 例程,而卸载需要连续调用两个函数,因此必须编写一个自定义的 APC 执行函数。
由于此自定义函数不属于系统 DLL(如 Kernel32.dll),它原本并不存在于目标进程的地址空间中。因此,我们必须解决两个核心问题:
- 代码注入:如何将这段 APC 执行函数的二进制代码拷贝到目标进程?方法与之前通过
WriteProcessMemory 写入 DLL 路径类似,只不过此次拷贝的对象是编译后的函数机器码。
- 地址重定位:自定义函数中调用的
GetModuleHandle 和 FreeLibrary,在编译时会被处理为基于当前模块的相对地址调用。但当代码被拷贝到目标进程的随机地址后,这些相对地址将完全失效。为解决此问题,我们不能直接调用这两个函数,而应采用函数指针进行间接调用。由于 GetModuleHandle 和 FreeLibrary 均来自 Kernel32.dll,其基址在所有进程中是一致的,我们只需将这两个函数的绝对地址作为参数传给 APC 函数,在函数内部通过指针间接调用即可。
要拷贝函数,就需要知道其二进制代码的起始地址和长度。起始地址即函数符号的地址,但获取长度较为棘手。一个实用的迂回策略是:利用编译器指令,将 APC 执行函数单独放置在一个自定义的代码段中。程序运行时,通过解析 PE文件格式 的节区头(Section Headers),即可定位该自定义段并获取其长度。
理论清晰后,开始编码。首先定义 APC 执行函数 ApcFunc 及其参数结构体 APC_PARAMETER。APC_PARAMETER 包含三个成员:待卸载的 DLL 名称指针、GetModuleHandleA 的函数指针和 FreeLibrary 的函数指针。ApcFunc 的逻辑是:通过传入的函数指针调用 GetModuleHandleA 获取句柄,再将该句柄传给 FreeLibrary 函数指针完成卸载。

请注意,ApcFunc 的定义使用了 #pragma code_seg(".MyCode"),这是 MSVC 编译器的预处理指令,用于将函数编译到自定义的 .MyCode 段,而非默认的 .text 段。随后的 #pragma comment(linker, "/SECTION:.MyCode,ER") 则指定了该段的属性为可执行(E)与可读(R)。
接下来是获取自定义代码段长度的函数。通过 GetModuleHandle(NULL) 获取当前模块句柄,将其转换为 PE 结构的 DOS 头,进而找到 NT 头和节区头数组。遍历所有节区,找到名为 .MyCode 的节区,其 SizeOfRawData 字段(经过文件对齐后的尺寸)即可作为拷贝长度。

然后是初始化 APC 参数的函数。我们需要在目标进程内分配远程内存,用于存放待卸载的 DLL 名称字符串,并使用 WriteProcessMemory 进行拷贝。接着,将 GetModuleHandle 和 FreeLibrary 的绝对地址赋值给参数结构体中的对应函数指针,确保 APC 函数在目标进程中能正确调用它们。

最后,整合所有步骤:获取 APC 函数长度、初始化参数、在目标进程为函数体和参数分配并写入远程内存。随后的操作与 APC 注入 DLL 的流程一致:枚举目标进程的所有线程,并向其线程 APC 队列提交卸载任务。

下面验证效果。首先,使用之前的注入工具 InjectDllByAPC.exe 将测试 DLL SetAntiScreenShot.dll 注入到任务管理器进程。

使用 Process Explorer 可以捕获到任务管理器加载该 DLL 的瞬间(绿色高亮行),证明注入成功。

随后,运行本次编写的卸载程序 UnloadDllbyAPC.exe,尝试从任务管理器进程中移除该 DLL。

再次观察 Process Explorer,可见 SetAntiScreenShot.dll 所在行变为红色,表示其已被成功卸载。

总结
本文的核心在于演示了如何向目标进程注入一段自定义的二进制代码(shellcode)并执行。此方法同样适用于远程线程注入等场景。关键在于处理代码的重定位问题,对于调用外部函数,最稳妥的方式是使用函数指针进行间接调用,并以绝对地址形式传入系统 API 的入口点。这种技术对深入理解 Windows 进程内存与安全 有重要意义,同时也提醒了安全人员注意此类隐蔽的进程内存操作手段。