【问题标题】:Keep DLL module loaded in Explorer until COM class is destroyed在资源管理器中保持加载 DLL 模块,直到 COM 类被销毁
【发布时间】:2021-06-02 14:08:16
【问题描述】:

我正在设置一个钩子以将我的 DLL 模块放入 Explorer 的进程/线程空间,并从那里实例化一个 COM 类,该类子类化一些窗口、接收一些事件等。

由于我的对象通过子类化和事件接收器完成所有操作,因此一旦对象被实例化,我就不需要 hook proc。为了性能,我想尽快停止钩子。

唯一的问题是立即取消挂钩(在我的对象刚刚创建之后)会导致 DLL 模块卸载,进而导致访问冲突和崩溃。所以我想知道是否有一种优雅的方法可以解开钩子,同时确保 DLL 模块至少在我的对象被销毁之前保留在内存中。

到目前为止,我发现唯一可行的解​​决方案是在我自己加载的模块上使用GET_MODULE_HANDLE_EX_FLAG_PIN 调用GetModuleHandleEx。这确保模块在 Explorer 进程的生命周期内保持加载。但我不确定这是否味道不佳。

是否有任何替代的、安全的方法来确保在我的对象执行其FinalRelease 之前不会卸载 DLL? (顺便说一句,我不认为从FinalRelease 中调用LoadLibrary 然后FreeLibrary 有效。这仍然会导致访问冲突。)感谢您的任何意见。

【问题讨论】:

  • 尝试自己调用LoadLibrary,这将增加模块引用计数,然后在您的FinalRelease 中调用FreeLibrary。或者编写一个 Shell 扩展(而不是挂钩)以进入 Explorer 的地址空间。
  • @RichardCritten - 如果 call FreeLibrary 而不是 jmp - 崩溃将在 FreeLibrary 返回卸载模块之后。解决方案存在,但不简单
  • 怎么样FreeLibraryAndExitThread"...函数没有返回..." - docs.microsoft.com/en-us/windows/win32/api/libloaderapi/…
  • @RichardCritten - 这仅对单独的线程有帮助 - 如果我们在 dll 中有自己的线程并且在线程退出时需要卸载。但这是最简单的情况。我也有通用案例的解决方案,但不容易描述它..
  • 也许我会坚持GET_MODULE_HANDLE_EX_FLAG_PIN

标签: c++ shell winapi explorer


【解决方案1】:

当然,在 dll 设置一些回调给 self(线程过程、工作项、窗口过程、I/O 回调等)之后 - 它不能被卸载,直到回调离开 dll 主体

当然,如果你在 api 中设置回调并且回调将在 api 返回后不再被调用,这不是问题。所以回调只能在 api 调用中调用。例如EnumWindows

但是现在让我们看看情况,当回调可以在任何时间设置后被调用。比如窗口过程

为此 - 首先模块必须始终有额外的引用,直到回调被激活(可以被调用或内部调用) 并在回调返回后 - 取消引用模块。但我们不能直接通过调用FreeLibrary (LdrUnloadDll) 来执行此操作,因为当我们返回到 dll 时 - 它可能已经被卸载了

所以需要 call 使用 jmp asm 指令。但这可能只能从 asm 代码中完成。回调存根必须在 asm 中实现,在开始时它必须“旋转”堆栈 - 交换堆栈中的 agruments(如果存在)并返回地址。在堆栈顶部设置返回地址。用 c/c++ 或其他语言调用“真实”回调,然后在它返回 call __imp_FreeLibrary 之后使用 jmp __imp_FreeLibraryFreeLibrary 当然会覆盖回调的返回码。但有些回调确实没有使用返回码(比如 I/O 回调)。一些(如 Windows 程序)使用返回码,但并非针对所有消息。通常我们会在窗口被破坏时卸载,最后消息已经返回代码,无论如何。所以在一般情况下,我们不能在每次回调后调用FreeLibrary。 LdrAddrefDll/LdrUnloadDll 也很昂贵,需要经常这样做。需要在 dll 中使用中间引用计数器(“fastref”)并仅在内部计数器变为 0 时调用 FreeLibrary。这不会损坏返回代码,并且会更快。

通常回调也可以与一些对象相关联。并且此对象的生存时间比回调更长 - 在回调处于活动状态之前,不得销毁对象。所以改为为每个回调引用/取消引用 dll(这并不总是可能的。windowproc 回调不可能 - 因为新的回调可以随时出现 - 我们不能在每个回调之前引用 dll) - 创建对象时需要引用 dll (在构造函数内部)并在对象被销毁时(在析构函数内部)取消引用 dll。

首先实现快速引用dll过程

对于 x64:

extern __imp_LdrUnloadDll:QWORD
extern __imp_LdrAddRefDll:QWORD
extern __ImageBase:BYTE


.DATA?

    align 4
@@UsageCount    DD ?

.code

@@FastReferenceDll proc
    lock inc[@@UsageCount]
    ret
@@FastReferenceDll endp

?ReferenceDll@@YAXXZ proc
    mov eax,1
    lock xadd[@@UsageCount],eax
    test eax,eax
    jz @@AddRefDll
    ret
@@AddRefDll:
    lea rdx, __ImageBase
    xor ecx,ecx
    jmp __imp_LdrAddRefDll
?ReferenceDll@@YAXXZ endp

?DereferenceDll@@YAXXZ proc
    lock dec[@@UsageCount]
    jz @@UnloadDll
    ret
@@UnloadDll:
    lea rcx, __ImageBase
    jmp __imp_LdrUnloadDll
?DereferenceDll@@YAXXZ endp

end

和 x86:

.686

.MODEL flat

extern __imp__LdrAddRefDll@8:DWORD
extern __imp__LdrUnloadDll@4:DWORD
extern ___ImageBase:BYTE

.DATA?

    align 4
@@UsageCount    DD ?
    
.CODE

@@FastReferenceDll proc
    lock inc[@@UsageCount]
    ret
@@FastReferenceDll endp

?ReferenceDll@@YGXXZ proc
    mov eax,1
    lock xadd[@@UsageCount],eax
    test eax,eax
    jnz @@nop
    lea eax, ___ImageBase
    push eax
    xor eax,eax
    push eax
    call __imp__LdrAddRefDll@8
@@nop:
    ret
?ReferenceDll@@YGXXZ endp

?DereferenceDll@@YGXXZ proc
    lock dec[@@UsageCount]
    jz @@UnloadDll
    ret
@@UnloadDll:
    lea eax, ___ImageBase
    xchg [esp],eax
    push eax
    jmp __imp__LdrUnloadDll@4
?DereferenceDll@@YGXXZ endp

end

使用 ATL 进行子类化的基类

class MySubClassBaseT : public CWindowImplBaseT<>
{
    ULONG dwRefCount = 1;

    static LRESULT CALLBACK StubWindowProc(
        _In_ HWND hWnd,
        _In_ UINT uMsg,
        _In_ WPARAM wParam,
        _In_ LPARAM lParam)ASM_FUNCTION;

    virtual WNDPROC GetWindowProc()
    {
        // force reference/include WindowProc
        return _ReturnAddress() ? StubWindowProc : WindowProc;
        //return StubWindowProc;
    }

protected:

    virtual void OnFinalMessage(_In_ HWND /*hwnd*/)
    {
        Release();
    }

    virtual ~MySubClassBaseT()
    {
        DereferenceDll();
    }

public:

    MySubClassBaseT()
    {
        ReferenceDll();
    }

    BOOL SubclassWindow(_In_ HWND hWnd)
    {
        if (__super::SubclassWindow(hWnd))
        {
            AddRef();
            return TRUE;
        }

        return FALSE;
    }

    HWND UnsubclassWindow(_In_ BOOL bForce /*= FALSE*/)
    {
        if (HWND hwnd = __super::UnsubclassWindow(bForce))
        {
            m_dwState |= WINSTATE_DESTROYED;
            m_hWnd = hwnd;
            return hwnd;
        }

        return 0;
    }

    void AddRef()
    {
        dwRefCount++;
    }

    void Release()
    {
        if (!--dwRefCount)
        {
            delete this;
        }
    }
};

我们在构造函数中调用ReferenceDll();,在析构函数中调用DereferenceDll();。所以直到对象(继承自MySubClassBaseT)没有被销毁,dll 将不会被卸载。我们还需要将WindowProc 替换为StubWindowProc,我们实现的是asm(因为需要返回jmp

x64:

extern ?WindowProc@?$CWindowImplBaseT@VCWindow@ATL@@V?$CWinTraits@$0FGAAAAAA@$0A@@2@@ATL@@SA_JPEAUHWND__@@I_K_J@Z : PROC

?StubWindowProc@MySubClassBaseT@@CA_JPEAUHWND__@@I_K_J@Z proc
    call @@FastReferenceDll
    sub rsp,28h
    call ?WindowProc@?$CWindowImplBaseT@VCWindow@ATL@@V?$CWinTraits@$0FGAAAAAA@$0A@@2@@ATL@@SA_JPEAUHWND__@@I_K_J@Z
    add rsp,28h
    jmp ?DereferenceDll@@YAXXZ
?StubWindowProc@MySubClassBaseT@@CA_JPEAUHWND__@@I_K_J@Z endp

我们在@@FastReferenceDll / ?DereferenceDll@@YAXXZ 调用中封装对ATL::CWindowImplBaseT::WindowProc 的调用。这需要在析构函数中不直接卸载dll,我们称之为DereferenceDll();。可以在回调中调用析构函数。因为在回调入口点我们已经有至少 1 个引用,可能使用 @@FastReferenceDll 而不是 ?ReferenceDll@@YAXXZ。最后我们退出 jmp (tail call ) 到 DereferenceDll 如果仍然存在对 dll 或 jmpLdrUnloadDll 的引用并且永远不会返回到 dll 主体(可以是在此 api 执行后卸载)。

对于 x86:

?StubWindowProc@MySubClassBaseT@@CGJPAUHWND__@@IIJ@Z proc
    mov eax,[esp]
    xchg [esp+4*4],eax
    xchg [esp+3*4],eax
    xchg [esp+2*4],eax
    xchg [esp+1*4],eax
    mov [esp],eax
    call @@FastReferenceDll
    call ?WindowProc@?$CWindowImplBaseT@VCWindow@ATL@@V?$CWinTraits@$0FGAAAAAA@$0A@@2@@ATL@@SGJPAUHWND__@@IIJ@Z
    jmp ?DereferenceDll@@YGXXZ
?StubWindowProc@MySubClassBaseT@@CGJPAUHWND__@@IIJ@Z endp

这里我们需要先“旋转”堆栈。所以从

arg4
arg3
arg2
arg1
ret

ret
arg4
arg3
arg2
arg1

(对于 x64 不需要这个,因为这里的寄存器中的所有参数,但如果回调有 5 个或更多参数 - 这也已经需要)

example 的用法

【讨论】:

  • 实际上,不应该简单地在类上调用CoCreateInstance 将DLL 在内存中保留正确的时间吗?因为这时COM才会生效,而DLL只有在DllCanUnloadNow允许的时候才会被卸载——即在对象被销毁之后。
  • @user15284017 - 并非所有任务都可以使用 CoCreateInstance 解决。这只是非常狭窄的情况
  • 嗯,我的意思是专门调用CoCreateInstance在 DLL 模块中声明的类。那COM不接手吗?
  • @user15284017 - 在这个具体案例中是的
猜你喜欢
  • 1970-01-01
  • 2011-04-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-18
  • 2021-02-15
  • 2010-12-06
  • 2020-10-24
相关资源
最近更新 更多