卸载注入的 DLL
Posted
技术标签:
【中文标题】卸载注入的 DLL【英文标题】:Unloading an Injected DLL 【发布时间】:2014-08-29 22:35:24 【问题描述】:我有一个使用SetWindowsHookEx
注入其他进程的DLL。在 DLL 中,我通过调用 GetModuleHandleEx
来增加模块的引用计数器,这样我就可以控制何时卸载模块。
此时,来自这两个 API 调用的模块引用计数“应该”为 2。当调用进程关闭时,它调用UnhookWindowsHookEx
,将引用计数减为1。DLL 有一个线程等待一些事情,其中之一是调用SetWindowsHookEx
的进程的句柄。当进程消失时,DLL 会进行一些清理,终止所有线程,清理内存和句柄,然后调用FreeLibraryAndExitThread
。这会递减计数器并卸载 DLL。
这是我的问题。有一些进程,尤其是那些没有 UI 的进程,DLL 永远不会被卸载。我很有信心我已经清理了所有东西。而且我知道我的所有线程都没有运行。
首先,如果您有任何故障排除提示来帮助找出原因,那将很有帮助。否则,我正在考虑使用诸如NtQueryInformationProcess
之类的API 来获取模块地址并确认模块句柄计数实际上为零,然后调用CreateRemoteThread
注入对LdrUnloadDll
的调用以从内部卸载模块地址过程。您对这种方法有何看法?有没有人有任何示例代码?我很难找出如何获取模块句柄计数。
【问题讨论】:
与其自己打电话给NtQueryInformationProcess
,不如至少在这些故障排除阶段,您应该能够从windbg获取该信息。
挂钩 DLL 在目标进程泵送消息时被注入和删除。 (因为这是唯一一次将代码注入线程是安全的。)如果进程没有 UI,它可能已经停止发送消息,此时窗口管理器没有机会获得控制权。注入 Unload 调用只会产生一个新问题:当目标进程最终决定再次泵送消息时,您将遇到双重释放错误。
我已经确认应用程序没有收到触发递减引用计数的消息。我添加了找出计数的代码,并减少了数字,DLL 立即被卸载,但正如您所怀疑的那样,我相信当消息最终到达时我看到了双重释放问题。我在想,如果问题是缺少一个正在运行的消息泵,那么我可能需要做的就是创建一个?我试过这个,但是,它没有被调用。对此有什么想法吗?
你说你使用SetWindowsHookEx
。但是SetWindowsHookEx
是否将 dll 注入到没有 UI 的进程中?据我所知,它并没有这样做......那么你的 dll 是如何被加载的呢?这些进程在被注入后真的在运行过程中停止了泵送消息吗?
我们的 CBTProc 在这些过程中被调用。所以,是的……我想他们会的。这些进程没有 UI。我猜他们是通过使用 COM 来获取消息泵,还是暂时将其用于进程间通信?没有把握。但我必须相信这是大多数使用此 API 的人都见过的常见问题,因为其中一个进程是 windows 桌面管理器 (dwm.exe)。有没有办法我可以创建自己的消息泵来解决这个问题,如果可以的话怎么办?
【参考方案1】:
好的.. 好了.. 有很多方法可以从进程中获取模块信息。无证方式和“有证”方式。
结果(记录在案):
这是“记录在案”的方式..
#include <windows.h>
#include <TlHelp32.h>
#include <iostream>
#include <sstream>
int strcompare(const char* One, const char* Two, bool CaseSensitive)
#if defined _WIN32 || defined _WIN64
return CaseSensitive ? strcmp(One, Two) : _stricmp(One, Two);
#else
return CaseSensitive ? strcmp(One, Two) : strcasecmp(One, Two);
#endif
PROCESSENTRY32 GetProcessInfo(const char* ProcessName)
void* hSnap = nullptr;
PROCESSENTRY32 Proc32 = 0;
if ((hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)) == INVALID_HANDLE_VALUE)
return Proc32;
Proc32.dwSize = sizeof(PROCESSENTRY32);
while (Process32Next(hSnap, &Proc32))
if (!strcompare(ProcessName, Proc32.szExeFile, false))
CloseHandle(hSnap);
return Proc32;
CloseHandle(hSnap);
Proc32 = 0 ;
return Proc32;
MODULEENTRY32 GetModuleInfo(std::uint32_t ProcessID, const char* ModuleName)
void* hSnap = nullptr;
MODULEENTRY32 Mod32 = 0;
if ((hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ProcessID)) == INVALID_HANDLE_VALUE)
return Mod32;
Mod32.dwSize = sizeof(MODULEENTRY32);
while (Module32Next(hSnap, &Mod32))
if (!strcompare(ModuleName, Mod32.szModule, false))
CloseHandle(hSnap);
return Mod32;
CloseHandle(hSnap);
Mod32 = 0;
return Mod32;
std::string ModuleInfoToString(MODULEENTRY32 Mod32)
auto to_hex_string = [](std::size_t val, std::ios_base &(*f)(std::ios_base&)) -> std::string
std::stringstream oss;
oss << std::hex << std::uppercase << val;
return oss.str();
;
std::string str;
str.append(" =======================================================\r\n");
str.append(" Module Name: ").append(Mod32.szModule).append("\r\n");
str.append(" =======================================================\r\n\r\n");
str.append(" Module Path: ").append(Mod32.szExePath).append("\r\n");
str.append(" Process ID: ").append(std::to_string(Mod32.th32ProcessID).c_str()).append("\r\n");
str.append(" Load Count (Global): ").append(std::to_string(static_cast<int>(Mod32.GlblcntUsage != 0xFFFF ? Mod32.GlblcntUsage : -1)).c_str()).append("\r\n");
str.append(" Load Count (Process): ").append(std::to_string(static_cast<int>(Mod32.ProccntUsage != 0xFFFF ? Mod32.ProccntUsage : -1)).c_str()).append("\r\n");
str.append(" Base Address: 0x").append(to_hex_string(reinterpret_cast<std::size_t>(Mod32.modBaseAddr), std::hex).c_str()).append("\r\n");
str.append(" Base Size: 0x").append(to_hex_string(Mod32.modBaseSize, std::hex).c_str()).append("\r\n\r\n");
str.append(" =======================================================\r\n");
return str;
int main()
PROCESSENTRY32 ProcessInfo = GetProcessInfo("notepad.exe");
MODULEENTRY32 ME = GetModuleInfo(ProcessInfo.th32ProcessID, "uxtheme.dll");
std::cout<<ModuleInfoToString(ME);
未记录的 API 的问题在于,我从来没有弄清楚为什么动态模块的加载计数总是“6”而静态模块的加载计数总是“-1”。因此,我不会发布它。
如果您只需要加载计数,最好不要使用未记录的 API。未记录的 API 的唯一优点是您可以使用它来“取消链接/隐藏”进程中的模块(就像病毒一样)。它将“取消链接/隐藏”它..而不是“卸载”它。这意味着您可以随时将其“重新链接”回进程的模块列表。
由于您只需要模块引用计数,因此我只包含了“记录”的 API,它正是这样做的。
【讨论】:
【参考方案2】:我找到了问题的原因和解决方案。老实说,我为错过它并为此挣扎了这么久而感到很愚蠢。
正如我在原始问题中提到的,有问题的进程没有 UI。事实证明他们确实有一个消息泵正在运行。问题是在我们调用UnhookWindowsHookEx
之后,没有 UI 向这些进程发送消息,这会触发卸载。 (事实上,我相信MSDN确实声明了在调用UnhookWindowsHookEx
时不会将窗口消息发送到进程。)
通过在注入进程调用UnhookWindowsHookEx
后向所有进程广播 WM_NULL,消息泵在注入进程中唤醒,DLL 引用计数减少。当注入的DLL最终调用FreeLibraryAndExitThread
时立即卸载DLL。
这只是解决方案的一部分。如果注入进程被杀死或崩溃,则不会广播任何消息,因此 DLL 不会从没有 UI 的进程中卸载。正如我之前提到的,我有一个在等待注入进程句柄的 DLL 中运行的线程。当注入进程结束时,DLL 会收到信号,然后调用PostThreadMessage
将 WM_NULL 发送到进程中的每个线程。然后它会等到 DLL 引用计数减少,然后再继续并在调用 FreeLibraryAndExitThread
之前进行清理。因此,DLL 几乎立即从所有进程(UI 或无 UI)中卸载。
【讨论】:
以上是关于卸载注入的 DLL的主要内容,如果未能解决你的问题,请参考以下文章