[逆向工程]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
結語
如果本教程對您有幫助,請點贊??收藏?關注支持!歡迎在評論區留言交流技術細節!