[逆向工程]C++实现DLL卸载(二十六)
引言
DLL注入(DLL Injection)是Windows系统下实现进程间通信、功能扩展、监控调试的核心技术之一。本文将从原理分析、代码实现、实战调试到防御方案,全方位讲解如何用C++实现DLL注入,并提供可直接编译的完整项目代码。
一、资源准备
1.资源准备
gmp.exe 被注入的程序
injector.exe 注入器
mandaohook.dll 需注入的dll
unloader.exe 卸载dll程序
2.任务目标
将编写好的mandaohook.dll通过injector.exe注入器注入到gmp.exe可运行程序中。注入之后将mandaohook.dll卸载。
二、DLL卸载的核心原理
DLL(动态链接库)卸载的核心原理可以从操作系统的角度和程序运行机制的角度来理解。
1. 操作系统角度
-
引用计数机制
- 在 Windows 操作系统中,DLL 的加载和卸载是基于引用计数的。当一个程序(可以是可执行程序,也可以是其他 DLL)第一次调用
LoadLibrary
函数加载一个 DLL 时,操作系统的加载器会将该 DLL 加载到内存中。同时,系统会为该 DLL 创建一个引用计数器,并将其值设置为 1。 - 如果其他程序或模块再次调用
LoadLibrary
加载同一个 DLL,引用计数器的值会递增。这个引用计数器的作用是记录当前有多少个模块正在使用这个 DLL。 - 当一个模块调用
FreeLibrary
函数来释放 DLL 时,引用计数器的值会递减。只有当引用计数器的值为 0 时,操作系统才会真正地从内存中卸载这个 DLL。例如,一个应用程序在运行过程中先后调用了两次LoadLibrary
加载了同一个 DLL,那么引用计数为 2。当它调用了一次FreeLibrary
之后,引用计数变为 1,DLL 仍然保留在内存中。只有当它再次调用FreeLibrary
,引用计数变为 0,DLL 才会被卸载。
- 在 Windows 操作系统中,DLL 的加载和卸载是基于引用计数的。当一个程序(可以是可执行程序,也可以是其他 DLL)第一次调用
-
内存映射机制
- DLL 文件本身是一种可执行文件格式(PE 格式)。当 DLL 被加载时,操作系统会根据其文件结构将 DLL 的代码段、数据段等映射到进程的虚拟地址空间中。DLL 的代码段是共享的,多个进程加载同一个 DLL 时,它们共享同一个代码段的物理内存,这样可以节省内存资源。
- 在卸载 DLL 时,操作系统会撤销这种内存映射。对于代码段,如果它是共享的,当最后一个进程卸载 DLL 时,操作系统才会真正释放对应的物理内存。而对于数据段,由于每个进程加载 DLL 时可能会有自己的数据副本(如果 DLL 中定义了可写数据),操作系统会清理每个进程的虚拟地址空间中对应的区域。
2. 程序运行机制角度
-
DLL 的入口函数
- DLL 文件中有一个特殊的入口函数
DllMain
。当 DLL 被加载时,操作系统会调用DllMain
函数,并传入DLL_PROCESS_ATTACH
参数。这个函数可以用于初始化 DLL 的资源,比如分配内存、初始化全局变量等。 - 当 DLL 被卸载时,操作系统会调用
DllMain
函数,并传入DLL_PROCESS_DETACH
参数。DLL 的编写者可以在DllMain
函数中处理DLL_PROCESS_DETACH
情形,进行资源清理工作,例如释放分配的内存、关闭文件句柄、清理线程等。这一步是 DLL 卸载过程中很重要的环节,因为如果 DLL 没有正确清理资源,可能会导致内存泄漏、资源占用等问题。
- DLL 文件中有一个特殊的入口函数
-
线程和资源的清理
- 如果 DLL 创建了线程,那么在卸载之前需要确保这些线程已经正确结束。否则,可能会出现线程还在运行,但 DLL 已经被卸载的情况,这会导致线程访问无效的代码和数据,从而引发程序崩溃。
- DLL 卸载还需要清理它所占用的资源,比如文件句柄、网络连接、注册表项等。这些资源的清理工作通常在
DllMain
的DLL_PROCESS_DETACH
处理分支中完成。如果 DLL 没有正确清理这些资源,它们可能会被操作系统回收,导致其他程序无法正常使用这些资源,或者出现资源泄漏的情况。
三、完整C++实现代码
1. 卸载器代码(unloader.cpp)
#include <Windows.h>
#include <TlHelp32.h>
#include <iostream>
#include <memory>
#include <string>
#include <shellapi.h> // 自动释放资源模板
template<typename T>
struct HandleDeleter {void operator()(T* handle) const {if (handle) CloseHandle(handle);}
};
using UniqueHandle = std::unique_ptr<void, HandleDeleter<void>>;// 查找模块基址(HMODULE)
HMODULE FindModuleBase(DWORD pid, const wchar_t* moduleName) {MODULEENTRY32W me = { sizeof(me) };UniqueHandle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid));if (!snapshot.get()) return nullptr;if (Module32FirstW(snapshot.get(), &me)) {do {// 提取模块基名const wchar_t* baseName = wcsrchr(me.szExePath, L'\\');baseName = baseName ? baseName + 1 : me.szExePath;if (_wcsicmp(baseName, moduleName) == 0) {return me.hModule;}} while (Module32NextW(snapshot.get(), &me));}return nullptr;
}// 主卸载函数
bool UnloadDll(DWORD pid, const wchar_t* dllName) {// 1. 打开目标进程UniqueHandle hProcess(OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE,FALSE, pid));if (!hProcess) return false;// 2. 查找目标模块基址HMODULE hModule = FindModuleBase(pid, dllName);if (!hModule) {std::wcerr << L"错误:未找到模块 " << dllName << std::endl;return false;}// 3. 获取FreeLibrary地址auto pFreeLibrary = reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "FreeLibrary"));if (!pFreeLibrary) return false;// 4. 创建远程线程卸载DLLUniqueHandle hThread(CreateRemoteThread(hProcess.get(), nullptr, 0,pFreeLibrary,reinterpret_cast<LPVOID>(hModule), // 直接传递HMODULE0, nullptr));if (!hThread) return false;// 5. 等待卸载完成return WaitForSingleObject(hThread.get(), 5000) == WAIT_OBJECT_0;
}int main(int argc, char* argv[]) {// 转换命令行参数为宽字符LPWSTR* szArglist = CommandLineToArgvW(GetCommandLineW(), &argc);if (!szArglist || argc != 3) {std::wcerr << L"参数错误! 正确用法: unloader.exe <PID> <DLL名称>" << std::endl;LocalFree(szArglist);return EXIT_FAILURE;}DWORD pid = _wtoi(szArglist[1]);const wchar_t* dllName = szArglist[2];if (UnloadDll(pid, dllName)) {OutputDebugStringW(L"DLL成功卸载了-!");std::wcout << L"DLL卸载成功!" << std::endl;LocalFree(szArglist);return EXIT_SUCCESS;} else {DWORD err = GetLastError();std::wcerr << L"卸载失败! 错误代码: 0x" << std::hex << err << std::endl;LocalFree(szArglist);return EXIT_FAILURE;}
}
编译:
g++ -o unloader.exe unloader.cpp -lpsapi -lmingw32 -static
四、关键API函数解析
代码实现了一个远程卸载 DLL 的功能,通过注入远程线程调用目标进程的 FreeLibrary
函数来卸载指定的 DLL。以下是代码中关键的 API 函数解析:
1. CreateToolhelp32Snapshot
UniqueHandle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid));
- 功能:创建一个进程或线程的快照。
- 参数:
TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32
:指定快照的类型,这里表示获取指定进程的模块信息。pid
:目标进程的进程 ID。
- 返回值:返回一个句柄,用于后续的模块遍历操作。
- 作用:用于获取目标进程加载的所有模块信息,以便查找特定的 DLL。
2. Module32FirstW
和 Module32NextW
if (Module32FirstW(snapshot.get(), &me)) {do {// 遍历模块} while (Module32NextW(snapshot.get(), &me));
}
- 功能:
Module32FirstW
用于获取快照中的第一个模块信息,Module32NextW
用于获取下一个模块信息。 - 参数:
snapshot.get()
:快照句柄。&me
:指向MODULEENTRY32W
结构的指针,用于存储模块信息。
- 返回值:成功返回非零值,失败返回零。
- 作用:遍历目标进程加载的所有模块,通过比较模块名称来查找指定的 DLL。
3. OpenProcess
UniqueHandle hProcess(OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE,FALSE, pid));
- 功能:打开一个已存在的进程,并返回一个句柄。
- 参数:
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE
:请求的访问权限,这里包括创建线程、查询信息、操作虚拟内存等权限。FALSE
:是否继承句柄。pid
:目标进程的进程 ID。
- 返回值:成功返回进程句柄,失败返回
NULL
。 - 作用:获取对目标进程的访问权限,以便后续操作。
4. GetProcAddress
auto pFreeLibrary = reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "FreeLibrary"));
- 功能:获取指定模块中导出函数的地址。
- 参数:
GetModuleHandleW(L"kernel32.dll")
:获取kernel32.dll
的模块句柄。"FreeLibrary"
:需要获取地址的函数名称。
- 返回值:成功返回函数地址,失败返回
NULL
。 - 作用:获取目标进程中的
FreeLibrary
函数地址,用于后续卸载 DLL。
5. CreateRemoteThread
UniqueHandle hThread(CreateRemoteThread(hProcess.get(), nullptr, 0,pFreeLibrary,reinterpret_cast<LPVOID>(hModule),0, nullptr
));
- 功能:在目标进程中创建一个线程。
- 参数:
hProcess.get()
:目标进程句柄。nullptr
:安全属性,这里为NULL
表示默认。0
:堆栈大小,这里为0
表示默认。pFreeLibrary
:线程函数入口点,这里为FreeLibrary
。reinterpret_cast<LPVOID>(hModule)
:线程函数的参数,这里传递的是目标 DLL 的模块句柄。0
:创建标志,这里为0
表示默认。nullptr
:线程 ID 指针,这里为NULL
表示不获取线程 ID。
- 返回值:成功返回线程句柄,失败返回
NULL
。 - 作用:在目标进程中创建一个线程,调用
FreeLibrary
函数来卸载指定的 DLL。
6. WaitForSingleObject
return WaitForSingleObject(hThread.get(), 5000) == WAIT_OBJECT_0;
- 功能:等待指定对象变为信号状态。
- 参数:
hThread.get()
:对象句柄,这里为远程线程句柄。5000
:等待时间(毫秒),这里为 5 秒。
- 返回值:如果对象变为信号状态,返回
WAIT_OBJECT_0
;超时返回WAIT_TIMEOUT
。 - 作用:等待远程线程完成卸载操作,超时返回失败。
7. CommandLineToArgvW
LPWSTR* szArglist = CommandLineToArgvW(GetCommandLineW(), &argc);
- 功能:将命令行字符串分解为参数列表。
- 参数:
GetCommandLineW()
:获取命令行字符串。&argc
:参数个数。
- 返回值:成功返回指向参数列表的指针,失败返回
NULL
。 - 作用:解析命令行参数,获取目标进程的 PID 和 DLL 名称。
8. LocalFree
LocalFree(szArglist);
- 功能:释放由
CommandLineToArgvW
分配的内存。 - 参数:
szArglist
:需要释放的内存指针。
- 作用:清理命令行参数列表占用的内存。
五、测试结果
gmp.exe injector.exe mandaohook.dll unloader.exe 放入同一个文件夹下:
1.先打开gmp.exe 工具
2.再打开DebugView
3.执行injector.exe 注入器注入
4.查看注入信息
5.卸载mandaohook.dll
unloader.exe PID mandaohook.dll
结语
如果本教程对您有帮助,请点赞❤️收藏⭐关注支持!欢迎在评论区留言交流技术细节!