gpt4 book ai didi

winapi - 如何准备序言和结语汇编以拦截具有参数的函数?

转载 作者:行者123 更新时间:2023-12-02 09:55:09 26 4
gpt4 key购买 nike

我非常精通编程,尤其是C ++,但是对于API挂钩和汇编(学习)的概念还是很陌生的。目前,我正在研究dll代理,与link here: ethicalhacker.net上的文章之后的其他方法相比,它应该相当容易。

我设法按照本文中的以下示例代码来使代理dll工作,

__declspec ( naked ) void myGetProcessDefaultLayout(void)
{
HINSTANCE handle;
FARPROC function;
DWORD retaddr;

__asm{
pop retaddr
}
handle = LoadLibraryA("user33.dll");
if(!handle){
MessageBoxA(NULL,"Failed to load user33.dll!","Error",MB_OK | MB_ICONERROR);
ExitProcess(0);
}

function = GetProcAddress(handle,"GetProcessDefaultLayout");
if(!function){
MessageBoxA(NULL,"Failed to load GetProcessDefaultLayout!","Error",MB_OK | MB_ICONERROR);
ExitProcess(0);
}

MessageBoxA(NULL,"GetProcessDefaultLayout called!","Hooked!",MB_OK);

__asm{
call far dword ptr function
push retaddr
retn
}

}


尽管本文在函数的开头和结尾解释了汇编代码的目的,但是由于我还是汇编语言的新手,所以我仍然对它的实际工作方式“模糊”。这仍然是一个简单明了的示例,但是我想知道当函数调用具有更多参数(例如,如下所示)时应如何设计汇编代码?

funcA(char* srcBuffer, int srcBuffer_size, char* dstBuffer, int* dstBuffer_size, BOOL AllowCallbacks = TRUE);


另外,在拦截此函数时,如何访问其参数以进行某些检查?抱歉,如果这是一个琐碎的问题,也许我搜索并研究了错误的资料。

最佳答案

任务通常足够复杂,需要一些汇编代码(因此x86 / x64的代码不同)。内联CL汇编器的功能不足以执行此任务(并且不支持x64)-需要使用masm [64]。 Prolog结尾Epilog存根需要在外部asm文件中实现。此存根已调用c ++代码。

x86中的hook 2函数的演示示例(具有__stdcall或__cdecl调用约定。对于__fastcall,还需要在asm存根中保存/恢复ecx,edx)

所以第一个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


这里有两个 CreateFileWCloseHandle的函数序言-尽管代码不同-模式对于任何挂钩的api都是通用的(__fastcall除外)-我​​们将c ++称为序言函数:

PVOID __fastcall CommonStub(PCWSTR DllName, PCSTR FunctionName, void** ppfn, void** Params);


它需要指向dll / api名称的指针(如果仅从单个dll进行钩子,则可以删除第一个参数),指向 void*变量的指针,该变量用于保存原始api地址(这是优化,对于调用 LoadLibrary/GetProcAddress仅一次,然后在顺序调用上使用ready结果),最后指向函数调用堆栈的指针( Params[0]是返回地址, Params[1]-第一个参数,依此类推)。 CommonStub必须返回asm存根的原始api地址。第一次调用时,我们用 GetProcAddress将其保存并保存在 *ppfn中,然后仅使用保存的值即可。

?retstub@CODE_STUB@@SAXXZ是常见的返回存根(epilog)。确实,这是函数挂钩中最难的部分。如果我们想在原始api返回后进行控制,则需要它。如果仅在api调用之前有足够的(用于任务)控制,则代码变得更小,更简单。所以对于api返回后的钩子控制-我们显然需要在堆栈中替换返回地址,以获得此控件。但是此后如何返回到原始呼叫者?需要保存原始寄信人地址。但是哪里 ?我们不能为此使用堆栈(没有任何堆栈空间),不能使用非易失性寄存器(如果使用它-在返回原始调用者之前需要保存并恢复-但还是在哪里保存?)。此处唯一的解决方案-分配可执行内存块-在该块中保存原始返回地址(必填),功能参数和名称(可选)-要知道返回时哪个api调用结束,并且在此块中必须是一些与基数无关的小代码-stub-此存根调用我们的asm Epilog- ?retstub@CODE_STUB@@SAXXZ,并带有指向此可执行内存块的指针。通过使用此指针,我们可以还原原始返回地址,检查api返回值并返回到原始调用方。另请注意-这里我假设单个eax寄存器(x64为rax)中的api返回值对于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中都将自动为true)

1024-是我们同时支持的api调用数。实际上,此值绰绰有余。但是,即使我们无法为某些api调用分配内存块-我们也无法控制此api的返回,但不会失败调用api,而只是返回到原始调用者。我使用 g_rf[1024];SLIST_HEADER(用于分配条目)和 InterlockedPopEntrySList(用于免费条目)将 InterlockedPushEntrySList的数组推入无锁堆栈结构。这是最快,最有效的。

C ++通用序言是 CommonStub-在这里我们可以在调用和可选的钩子返回( *Params = p->Function();)之前检查函数参数。

c ++常见结语是 INT_PTR __fastcall RET_INFO::OnCall(INT_PTR r)-此处 r是寄存器大小api返回值(来自eax或rax)。在 RET_INFO类中,存在有关api调用的所有必需信息。在这里,我们可以查看返回值,api名称,保存调用堆栈。但是,在此演示代码中,我仅执行强制性任务:恢复返回地址 *(void**)_AddressOfReturnAddress() = *params;(通过此技巧,我们在返回后直接返回到原始api调用者,而不返回到我们的asm存根Epilog)

_AddressOfReturnAddress是CL固有的(因此不受其他编译器支持,但我想它们具有某些等效功能)。最后我们释放(推入堆栈)已分配的可执行内存块- g_rfm.free(this);。函数return r-api调用结果(再次注意,我猜api使用单个寄存器)。从 INT_PTR __fastcall RET_INFO::OnCall(INT_PTR r)返回之后-我们将处于原始调用者代码中,并且正确的堆栈和api返回值以eax(rax)表示。但是,如果需要,我们可以不返回 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个字节。它使用 DECLSPEC_ALIGN(16)在winnt.h中声明。结果 RET_INFO(包含 SLIST_ENTRY)并从中继承 struct RET_FUNC : CODE_STUB, RET_INFO {}将对齐16个字节。一定是:

C_ASSERT(__alignof(RET_FUNC)==16);


无论如何-在 PVOID pad;的开头加上和不加上 CODE_STUB。但是我的代码隐式使用(需要)

C_ASSERT(sizeof(CODE_STUB) == RTL_SIZEOF_THROUGH_FIELD(CODE_STUB, offset));
C_ASSERT(FIELD_OFFSET(RET_FUNC, Entry)==sizeof(CODE_STUB));// !!


或换句话说,在 CODE_STUBoffset成员的结尾)和 RET_INFO的开始之间没有填充-在 CODE_STUB- call offset指令和返回地址中,压入堆栈的是..必须指向 RET_INFO的指针是-我从堆栈中弹出返回地址,并用作调用成员函数 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;成员对齐),将从 。因此,无论如何编译器都会隐式插入8字节填充,但要在 RET_FUNCCODE_STUB之间的 CODE_STUB末尾:

RET_INFO将。为避免这种情况-需要显式添加8字节填充,但必须以 RET_FUNC : CODE_STUB, /* 8 byte pad/ RET_INFO开头。这一切都是正确的。请注意,对于替换原始寄信人地址,我们使用

*Params = p->Function()


哪里

PVOID Function()
{
return &call;
}


CODE_STUB中的呼叫偏移指令的返回地址(而不是 CODE_STUB的地址)-因此,此正确处理开始时的任何填充-我们无论如何都获得了正确的地址或返回存根

关于winapi - 如何准备序言和结语汇编以拦截具有参数的函数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47839146/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com