当然,在 dll 为 self 设置了一些回调(线程过程、工作项、窗口过程、I/O 回调等)之后 - 它不能被卸载,直到回调离开 dll 主体
当然,如果您在 api 中设置回调并且在 api 返回后将不再调用回调,这当然不是问题。所以回调只能在 api 调用中调用。例如EnumWindows。
但现在让我们看看情况,当回调可以在任何时间设置后被调用。例如窗口过程
为此 - 首先模块必须始终有额外的引用,直到回调处于活动状态(可以被调用或内部调用)并且在回调返回之后 - 取消引用模块。但我们不能直接通过调用FreeLibrary( LdrUnloadDll) 来执行此操作,因为当我们返回 dll 时 - 它可能已经被卸载
所以需要改用callasmjmp指令。但这可能只能从 asm 代码中完成。回调存根必须在 asm 中实现,一开始它必须“旋转”堆栈 - 交换堆栈中的 agruments(如果存在)并返回地址。在堆栈顶部设置返回地址。用 c/c++ 或其他语言调用“真实”回调,然后在它返回后call __imp_FreeLibrary使用jmp __imp_FreeLibrary. FreeLibrary当然覆盖回调的返回码。但有些回调确实没有使用返回码(比如 I/O 回调)。一些(如 Windows 程序)使用返回码,但并非针对所有消息。通常我们会在窗口被破坏时卸载,最后消息已经返回代码,无论如何。所以我们不能打电话FreeLibrary在一般情况下每次回调之后。LdrAddrefDll/LdrUnloadDll 也很昂贵,需要经常这样做。需要在 dll 中使用中间引用计数器(“fastref”)并FreeLibrary仅在内部计数器变为 0 时调用。这不会损坏返回代码并且会更快。
通常回调也可以与某些对象相关联。并且此对象的生存时间比回调更长 - 在回调处于活动状态之前,不得销毁对象。所以改为为每个回调引用/取消引用 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
我们将调用包装ATL::CWindowImplBaseT::WindowProc在@@FastReferenceDll/?DereferenceDll@@YAXXZ调用中。这需要在我们调用的析构函数中不直接卸载 dll DereferenceDll();。可以在回调中调用析构函数。因为在回调入口点我们已经有至少 1 个引用,可能会@@FastReferenceDll使用?ReferenceDll@@YAXXZ. 最后我们退出jmp(尾调用),如果仍然存在对 dll 或to 的DereferenceDll引用,则不执行任何操作(并且不更改返回代码)并且永远不会返回到 dll 主体(可以在此 api 执行后卸载)。jmpLdrUnloadDll
对于 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 个或更多参数 - 这也已经需要)
用法示例this