任务通常足够复杂,需要一些汇编代码(x86/x64 的代码不同)。内联 CL 汇编器的功能不足以完成此任务(并且不支持 x64) - 需要使用 masm[64]。 prolog 结束 epilog 存根需要在外部 asm 文件中实现。这个存根调用了 c++ 代码。
x86 中钩子 2 函数的演示示例(使用 __stdcall 或 __cdecl 调用约定。对于 __fastcall 还需要保存/恢复 ecx,edx 在 asm 存根中)
所以第一个asm代码(编译为ML /c /Cp $(InputName).asm)
.686p
WSTRING macro name, text
ALIGN 2
name:
FORC arg, text
DW '&arg'
ENDM
DW 0
endm
ASTRING macro name, text
name:
FORC arg, text
DB '&arg'
ENDM
DB 0
endm
BSS segment
imp_CreateFileW DD 0 ; cache original function address
imp_CloseHandle DD 0 ; cache original function address
BSS ends
CONST segment
WSTRING kernel32, <kernel32> ; dllname, share for multiple api
ASTRING CreateFileW, <CreateFileW> ; api name
ASTRING CloseHandle, <CloseHandle> ; api name
CONST ends
_TEXT segment
extern ?CommonStub@@YIPAXPB_WPBDPAPAX2@Z : PROC ; void *__fastcall CommonStub(const wchar_t *,const char *,void **,void **)
?hook_CreateFileW@@YGPAXPB_WKKPAU_SECURITY_ATTRIBUTES@@KKPAX@Z proc
push esp
push offset imp_CreateFileW
mov ecx,offset kernel32
mov edx,offset CreateFileW
call ?CommonStub@@YIPAXPB_WPBDPAPAX2@Z
jmp eax
?hook_CreateFileW@@YGPAXPB_WKKPAU_SECURITY_ATTRIBUTES@@KKPAX@Z endp
?hook_CloseHandle@@YGHPAX@Z proc
push esp
push offset imp_CloseHandle
mov ecx,offset kernel32
mov edx,offset CloseHandle
call ?CommonStub@@YIPAXPB_WPBDPAPAX2@Z
jmp eax
?hook_CloseHandle@@YGHPAX@Z endp
extern ?OnCall@RET_INFO@@QAIHH@Z : PROC ; int __fastcall RET_INFO::OnCall(int)
?retstub@CODE_STUB@@SAXXZ proc
pop ecx
mov edx,eax
call ?OnCall@RET_INFO@@QAIHH@Z
?retstub@CODE_STUB@@SAXXZ endp
_TEXT ends
END
这里有 CreateFileW 和 CloseHandle 的 2 个函数序言 - 尽管代码不同 - 模式对于任何挂钩 api 都很常见(__fastcall 除外) - 我们称之为 c++常用prolog函数:
PVOID __fastcall CommonStub(PCWSTR DllName, PCSTR FunctionName, void** ppfn, void** Params);
它需要指向 dll/api 名称的指针(如果我们只从单个 dll 中挂钩,我们可以删除第一个参数),指向 void* 变量的指针,我们保存原始 api 地址(这是优化,仅调用一次 LoadLibrary/GetProcAddress ,然后在顺序调用中使用就绪结果),最后是指向函数调用堆栈的指针(Params[0] 是返回地址,Params[1] - 第一个参数,依此类推)。 CommonStub 必须返回 asm 存根的原始 api 地址。在第一次通话时,我们使用GetProcAddress 得到它并保存在*ppfn 中,然后简单地使用保存的值。
?retstub@CODE_STUB@@SAXXZ 是常见的返回存根(epilog)。这确实是函数挂钩中最难的部分。如果我们想要控制 原始 api 返回,则需要它。如果足够(用于任务)控制仅 before api 调用 - 代码变得更小更简单。所以对于钩子控制 after api return - 我们不经意间需要替换堆栈中的返回地址,以获得这个控制。但是在此之后如何返回原始调用者?需要保存原始退货地址。但是哪里 ?我们不能为此使用堆栈(没有任何堆栈空间),不能使用非易失性寄存器(如果使用它 - 在返回原始调用者之前需要保存和恢复 - 但又在哪里保存它?)。这里唯一的解决方案 - 分配 可执行 内存块 - 在这个块中保存原始返回地址(强制),功能参数和名称(可选) - 在返回时知道哪个 api 调用结束,并且在这个块中必须是一些微小的基本独立代码存根 - 这个存根调用我们的 asm 结语 - ?retstub@CODE_STUB@@SAXXZ 带有 pointer 到这个可执行内存块。通过使用该指针,我们恢复原始返回地址,检查 api 返回值并返回给原始调用者。另请注意 - 这里我假设 api 在单个 eax 寄存器中返回值(rax for x64)这对于 99%+ api 是正确的。但是存在一些返回 2 个寄存器 edx:eax 对的 api。这种情况当然可以处理,但为了简单起见,这里就不展示了(反正代码太大了)
你可以问,我如何在 asm 中格式化/知道这个复杂的 c++ 名称?我得到了 c++ 代码中的宏的帮助:
#if 1 //0
#define __ASM_FUNCTION __pragma(message(__FUNCDNAME__" proc\r\n" __FUNCDNAME__ " endp"))
#define _ASM_FUNCTION {__ASM_FUNCTION;}
#define ASM_FUNCTION {__ASM_FUNCTION;return 0;}
#define CPP_FUNCTION __pragma(message("extern " __FUNCDNAME__ " : PROC ; " __FUNCSIG__))
#else
#define _ASM_FUNCTION
#define ASM_FUNCTION
#define CPP_FUNCTION
#endif
#if 1 需要在编译时使用以获取 c++ 修饰名称(将其粘贴到 asm)。并在最终编译构建之前替换为#if 0。
现在正在寻找 c++ 代码。 90%+ 的代码实现和管理可执行内存缓冲区 - 需要在 api 返回后进行支持控制。
SLIST_HEADER g_head;
PVOID g_BaseAddress, g_pExport;
class CODE_STUB
{
#ifdef _WIN64
PVOID pad;
#endif
union
{
DWORD code;
struct
{
BYTE cc[3];
BYTE call;
};
};
int offset;
public:
void Init(PVOID stub)
{
code = 0xe8cccccc;// int3; int3; int3; call retstub
offset = RtlPointerToOffset(&offset + 1, stub);
}
PVOID Function()
{
return &call;
}
// implemented in .asm
static void __cdecl retstub() _ASM_FUNCTION;
};
struct RET_INFO
{
union
{
SLIST_ENTRY Entry;
struct
{
PCSTR Name;
PVOID params[7];
};
};
INT_PTR __fastcall OnCall(INT_PTR r);
};
struct RET_FUNC : CODE_STUB, RET_INFO
{
};
#pragma bss_seg(".HOOKS")
RET_FUNC g_rf[1024];//max concurent call count
#pragma bss_seg()
#pragma comment(linker, "/SECTION:.HOOKS,RWE")
class RET_FUNC_Manager
{
SLIST_HEADER _head;
public:
RET_FUNC_Manager()
{
PSLIST_HEADER head = &_head;
InitializeSListHead(head);
RET_FUNC* p = g_rf;
DWORD n = RTL_NUMBER_OF(g_rf);
do
{
p->Init(CODE_STUB::retstub);
InterlockedPushEntrySList(head, &p++->Entry);
} while (--n);
}
RET_FUNC* alloc()
{
return static_cast<RET_FUNC*>(CONTAINING_RECORD(InterlockedPopEntrySList(&_head), RET_INFO, Entry));
}
void free(RET_INFO* p)
{
InterlockedPushEntrySList(&_head, &p->Entry);
}
} g_rfm;
INT_PTR __fastcall RET_INFO::OnCall(INT_PTR r)
{
CPP_FUNCTION;
*(void**)_AddressOfReturnAddress() = *params;
g_rfm.free(this);
return r;
}
PVOID __fastcall CommonStub(PCWSTR DllName, PCSTR FunctionName, void** ppfn, void** Params)
{
CPP_FUNCTION;
//++ optional, hook return
if (RET_FUNC* p = g_rfm.alloc())
{
p->Name = FunctionName;
// memcpy(p->params, Params, sizeof(p->params)); // save original return address and params
PVOID StackBase = reinterpret_cast<PNT_TIB>(NtCurrentTeb())->StackBase;
PVOID ParamsBase = Params + RTL_NUMBER_OF(p->params);
ParamsBase = min(StackBase, ParamsBase);
memcpy(p->params, Params, RtlPointerToOffset(Params, ParamsBase));
*Params = p->Function();// replace return address
}
//-- optional
PVOID pfn = *ppfn;
if (!pfn)
{
if (pfn = GetProcAddress(LoadLibraryW(DllName), FunctionName))
{
*ppfn = pfn;
}
else
{
__debugbreak();
}
}
return pfn;
}
我将它命名为RET_FUNC(此缓冲区的结构)并在 PE 正文中预分配:
#pragma bss_seg(".HOOKS")
RET_FUNC g_rf[1024];//max concurent call count
#pragma bss_seg()
#pragma comment(linker, "/SECTION:.HOOKS,RWE")
这对于 x64 支持是强制性的(我在内存块中使用相对调用到 asm 存根 - 所以两个代码必须在范围内 -/+2GB - 当两者都在 PE 内时,这将自动为真)
1024 - 计算我们同时支持多少个 api 调用。在实践中这个值绰绰有余。但是,即使我们为某些 api 调用分配内存块失败 - 我们根本无法控制从该 api 的返回,但不会失败调用 api 并返回给原始调用者。我使用SLIST_HEADER、InterlockedPopEntrySList(用于分配条目)和InterlockedPushEntrySList(用于免费条目)将g_rf[1024]; 的数组推送到无锁堆栈结构。这是最大的快速和有效。
c++ 常见的 prolog 是 CommonStub - 在这里我们可以在调用之前检查函数参数和可选的钩子返回 (*Params = p->Function();)。
c++ 常见的 Epilog 是 INT_PTR __fastcall RET_INFO::OnCall(INT_PTR r) - 这里 r 是寄存器大小 api 返回值(来自 eax 或 rax)。在RET_INFO 类中存在所有需要的关于 api 调用的信息。在这里我们可以查看返回值、api 名称、保存调用堆栈。但是在这个演示代码中,我只实现了强制任务:恢复返回地址*(void**)_AddressOfReturnAddress() = *params;(通过这个技巧,我们在返回后直接返回原始 api 调用者,而不是我们的 asm 存根结语)
_AddressOfReturnAddress 是 CL 内在的(因此其他编译器不支持,但我猜他们有一些等价物)。最后我们释放(推送到堆栈)分配的可执行内存块 - g_rfm.free(this);。函数返回r - api 调用结果(再次注意我猜 api 使用了单个寄存器)。从INT_PTR __fastcall RET_INFO::OnCall(INT_PTR r) 返回后 - 我们将在 eax (rax) 中具有正确堆栈和 api 返回值的原始调用方代码。但是,如果需要,我们可以返回不是 r 而是另一个值 - 所以更改 api 调用结果。
x64 asm 的代码更简单,因为存在通用调用约定。
ml64 /c /Cp /Zd $(InputFileName) -> $(InputName).obj
WSTRING macro name, text
ALIGN 2
name:
FORC arg, text
DW '&arg'
ENDM
DW 0
endm
ASTRING macro name, text
name:
FORC arg, text
DB '&arg'
ENDM
DB 0
endm
BSS segment
imp_CreateFileW DQ 0 ; cache original function address
imp_CloseHandle DQ 0 ; cache original function address
BSS ends
CONST segment
WSTRING kernel32, <kernel32> ; dllname, share for multiple api
ASTRING CreateFileW, <CreateFileW> ; api name
ASTRING CloseHandle, <CloseHandle> ; api name
CONST ends
_TEXT segment
extern ?OnCall@RET_INFO@@QEAA_J_J@Z : PROC ; __int64 __cdecl RET_INFO::OnCall(__int64)
?retstub@CODE_STUB@@SAXXZ proc
pop rcx
mov rdx,rax
call ?OnCall@RET_INFO@@QEAA_J_J@Z
?retstub@CODE_STUB@@SAXXZ endp
extern ?CommonStub@@YAPEAXPEB_WPEBDPEAPEAX2@Z : PROC ; void *__cdecl CommonStub(const wchar_t *,const char *,void **,void **)
?hook_CreateFileW@@YAPEAXPEB_WKKPEAU_SECURITY_ATTRIBUTES@@KKPEAX@Z proc
mov [rsp+32],r9
mov [rsp+24],r8
mov [rsp+16],rdx
mov [rsp+8],rcx
mov r9,rsp
lea r8,imp_CreateFileW
lea rdx,CreateFileW
lea rcx,kernel32
sub rsp,40
call ?CommonStub@@YAPEAXPEB_WPEBDPEAPEAX2@Z
add rsp,40
mov rcx,[rsp+8]
mov rdx,[rsp+16]
mov r8,[rsp+24]
mov r9,[rsp+32]
jmp rax
?hook_CreateFileW@@YAPEAXPEB_WKKPEAU_SECURITY_ATTRIBUTES@@KKPEAX@Z endp
?hook_CloseHandle@@YAHPEAX@Z proc
mov [rsp+32],r9
mov [rsp+24],r8
mov [rsp+16],rdx
mov [rsp+8],rcx
mov r9,rsp
lea r8,imp_CloseHandle
lea rdx,CloseHandle
lea rcx,kernel32
sub rsp,40
call ?CommonStub@@YAPEAXPEB_WPEBDPEAPEAX2@Z
add rsp,40
mov rcx,[rsp+8]
mov rdx,[rsp+16]
mov r8,[rsp+24]
mov r9,[rsp+32]
jmp rax
?hook_CloseHandle@@YAHPEAX@Z endp
_TEXT ends
end
关于RET_INFO - PVOID params[7]; - 这允许保存(用于在 api 调用最多 6 个参数后使用(在 params[0] 中将是返回地址))。但是我们可以重新定义为 PVOID params[15]; - 最多可使用 14 个参数。
但是只需从堆栈中复制固定数量的参数
memcpy(p->params, Params, sizeof(p->params));
不[完全]正确,因为我们可以超出堆栈范围(如果说直接从线程入口点和函数调用,它们调用几乎不使用局部变量 - 所以堆栈非常靠近顶部)。为了正确需要在复制之前检查堆栈基础:
PVOID StackBase = reinterpret_cast<PNT_TIB>(NtCurrentTeb())->StackBase;
PVOID ParamsBase = Params + RTL_NUMBER_OF(p->params);
ParamsBase = min(StackBase, ParamsBase);
memcpy(p->params, Params, RtlPointerToOffset(Params, ParamsBase));
或者 memcpy 甚至可以做下一个优化:
#if defined(_M_IX86)
#define __movsp __movsd
#elif defined (_M_X64)
#define __movsp __movsq
#else
#error
#endif
__movsp((PULONG_PTR)p->params,
(PULONG_PTR)Params,
RtlPointerToOffset(Params, ParamsBase)/ sizeof(ULONG_PTR));
还要注意 x64:
你可以看到:
class CODE_STUB
{
#ifdef _WIN64
PVOID pad;// for what ?
#endif
因为在 win64 中 SLIST_ENTRY 必须是 16 字节对齐的。它在 winnt.h 中声明为 DECLSPEC_ALIGN(16)。结果RET_INFO(包含SLIST_ENTRY)并从它继承struct RET_FUNC : CODE_STUB, RET_INFO {}将是16字节对齐的。必须是:
C_ASSERT(__alignof(RET_FUNC)==16);
无论如何都会这样 - 在CODE_STUB 的开头有和没有PVOID pad;。但是我的代码隐式使用(需要)
C_ASSERT(sizeof(CODE_STUB) == RTL_SIZEOF_THROUGH_FIELD(CODE_STUB, offset));
C_ASSERT(FIELD_OFFSET(RET_FUNC, Entry)==sizeof(CODE_STUB));// !!
或者换句话说,在CODE_STUB(offset 成员的结尾)和RET_INFO 的开头之间没有填充 - 在CODE_STUB - call offset 指令和返回地址中,压入堆栈是.. 指向@ 的指针987654380@ 必须是 - 我从堆栈中弹出返回地址并用作指向 RET_INFO 的指针,用于调用成员函数 RET_INFO::OnCall:
?retstub@CODE_STUB@@SAXXZ proc
pop rcx ; -> RET_INFO
mov rdx,rax
call ?OnCall@RET_INFO@@QEAA_J_J@Z
?retstub@CODE_STUB@@SAXXZ endp
没有PVOID pad - CODE_STUB 是 8 字节(3*1 字节(int 3)+ 5 字节相对调用偏移量)但RET_INFO(由于 16 字节 SLIST_ENTRY Entry; 它是成员align) 将从RET_FUNC 偏移 16 处开始。所以编译器无论如何都会隐式插入8字节填充,但在CODE_STUB的末尾CODE_STUB和RET_INFO之间:
RET_FUNC : CODE_STUB, /* 8 byte pad/ RET_INFO 会。为了避免这种情况 - 需要明确添加这个 8 字节填充,但要以 CODE_STUB 开头。有了这一切都将是正确的。请注意,我们使用替换原始返回地址
*Params = p->Function()
在哪里
PVOID Function()
{
return &call;
}
call offset 指令在CODE_STUB 中的返回地址(而不是CODE_STUB 的地址) - 所以这个正确的处理开始时的任何填充 - 我们无论如何得到了正确的地址或返回存根