4
votes

I have a DLL I inject into other processes using SetWindowsHookEx. Inside the DLL I increment the module's reference counter by calling GetModuleHandleEx so I can control when the module is unloaded.

At this point the module reference count "should be" 2 from both of those API calls. When the calling process shuts down, it calls UnhookWindowsHookEx, decrementing the reference count to 1. The DLL has a thread that waits on a few things, one of them being the handle of the process that called SetWindowsHookEx. When the process goes away the DLL does some cleanup, terminates all threads, cleans up memory and handles and then calls FreeLibraryAndExitThread. This decrements the counter and the DLL gets unloaded.

Here's my problem. There are a few processes, especially those without a UI, where the DLL never gets unloaded. I'm pretty confident I have cleaned up everything. And I know for a fact that none of my threads are running.

First of all, if you have any troubleshooting tips to help uncover the cause, that would be helpful. Otherwise, I was thinking about using some API like NtQueryInformationProcess to get the module address and confirm the module handle count is in fact zero, then call CreateRemoteThread to inject a call to LdrUnloadDll to unload the module address from within the process. What are your thoughts to this approach? Does anyone have any example code? I'm having difficulty finding out how to get the module handle count.

2
Instead of calling NtQueryInformationProcess yourself, you should be able to get that information from windbg, at least during these troubleshooting phases.Ben Voigt
Hook DLLs are injected and removed when the target process pumps messages. (Since that's the only time it is safe to inject code into a thread.) If the process has no UI, it may have stopped pumping messages, at which point there is no opportunity for the window manager to get control. Injecting an Unload call merely creates a new problem: When the target process finally decides to pump messages again, you will have a double-free bug.Raymond Chen
I have confirmed that the application is not getting the message that triggers decrementing the reference count. I added code that find out the count, and decremented the number and the DLLs get unloaded immediately, but as you suspected I believe I'm seeing a double-free problem when the message finally arrives. I was thinking that if the problem is the lack of a running message pump that perhaps all I need to do is to create one? I've tried this, however, it's not being called. Any thoughts on this?tdemay
You say that You use SetWindowsHookEx. But is SetWindowsHookEx injecting the dll to the processes that do not have UI? As far as I knew, it doesn't do that... So how did Your dll get loaded in the first place? Did these processes really stop pumping messages in the middle of run, after being injected?Roland Pihlakas
Our CBTProc gets called in these processes. So, yes...i guess they do. These Processes don't have a UI. I'm guessing they are getting a message pump by using COM or maybe using it temporarily for Inter process communication? Not sure. But I have to believe this is a common problem that most people using this API have seen since one of the processes is the windows desktop manager (dwm.exe) . Is there a way I can create my own message pump to perhaps solve this, if so how?tdemay

2 Answers

6
votes

Okay.. here goes.. There are many ways to get the module info from a process. The undocumented way and the "documented" way.

Results (documented):

enter image description here

Here is the "documented" way..

#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);
}

The problem with the undocumented API is that I've never figured out why the load counts are always "6" for dynamic modules and "-1" for static modules.. For this reason, I will not post it..

It is BEST NOT to use undocumented API if you want just the load count. The undocumented API's only advantage is that you can use it to "un-link/hide" a module within a process (like viruses do).. It will "unlink/hide" it.. NOT "unload" it. This means that at any time, you can "re-link" it back into the process's module list.

Since you only need the module-reference-count, I've only included "documented" API which does exactly that.

3
votes

I found the cause of the problem and the solution. I honestly feel quite stupid for missing it and struggling with it for so long.

As I mentioned in the original problem, the processes that are problematic do not have a UI. Turns out they do have a message pump running. The problem is nothing is sending messages to these processes without a UI after we call UnhookWindowsHookEx that would trigger the unloading. (In fact, I believe MSDN does state that window messages are not sent to processes when calling UnhookWindowsHookEx.)

By broadcasting WM_NULL to all processes after the injecting process calls UnhookWindowsHookEx the message pump wakes up in the injected processes and the DLL reference count is decremented. The DLL is unloaded immediately when the injected DLL finally calls FreeLibraryAndExitThread.

This is only part of the solution. If the injecting process is killed or crashes, no message is broadcasted so the DLL does not get unloaded from processes that do not have a UI. As I mention before I have a thread running in the DLL that waits on the injecting process handle. When the injecting process ends the DLL is signaled and then calls PostThreadMessage to send WM_NULL to each thread in the process. Then it waits until the DLL reference count is decremented before continuing and cleaning up before calling FreeLibraryAndExitThread. As a result, the DLL is unloaded almost immediately from all processes, UI or no UI.