如何准备prolog和epilog汇编来拦截带参数的函数?

问题描述 投票:2回答:2

我非常精通编程,尤其是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);

此外,当拦截此功能时,如何访问其参数以执行某些检查?对不起,如果这是一个微不足道的问题,也许我搜索并研究了错误的材料。

winapi assembly visual-c++ x86 hook
2个回答
3
投票

任务一般足够复杂,需要一些汇编代码(因此x86 / x64的代码不同)。内联CL汇编程序不足以完成此任务(并且不支持x64) - 需要使用masm [64]。 prolog end 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

这里有CreateFileWCloseHandle的2个函数序言 - 尽管代码不同 - 模式对于任何钩子api都是常见的(除了__fastcall) - 我们称之为c ++ common 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返回后进行控制,则需要它。如果在api调用之前足够(用于任务)控制 - 代码变得更小和简单。所以对于api返回后的钩子控制 - 我们不经意地需要在栈中替换返回地址,以获得此控制。但是之后怎么回到原来的来电者?需要保存原始退货地址。但是哪里 ?我们不能使用堆栈(没有任何堆栈空间),不能使用非易失性寄存器(如果使用它 - 需要在返回原始调用者之前保存并恢复 - 但是再次保存它吗?)。这里只有解决方案 - 分配可执行内存块 - 在这个块中保存原始返回地址(强制),功能参数和名称(可选) - 知道返回哪个api调用结束,并且在这个块中必须是一些微小的基本独立代码-stub - 这个存根调用我们的asm epilog - ?retstub@CODE_STUB@@SAXXZ,带有指向这个可执行内存块的指针。通过使用此指针,我们恢复原始返回地址,检查api返回值并返回原始调用者。另请注意 - 这里我假设单个eax寄存器中的api返回值(xx为rax)这对99%+ api来说是正确的。但是存在一些api,它返回2个寄存器edx:eax对。这种情况当然可以处理,但为了简单起见我不在这里展示(代码太大了)

你可以问,我如何在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 stub - 因此两个代码必须在范围内 - / + 2GB - 当在PE内部时这将自动为真)

1024 - 是我们支持并发的api调用次数的计数。在实践中这个价值绰绰有余。然而,即使我们失败了为某些api调用分配内存块 - 我们根本无法控制从这个api的返回,但是没有失败调用api并且只返回到原始调用者。 g_rf[1024];的数组我通过使用SLIST_HEADERInterlockedPopEntrySList(用于分配条目)和InterlockedPushEntrySList(用于免费条目)推送无锁堆栈结构。这是最快速,最有效的。

c ++ common 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存根epilog)

_AddressOfReturnAddress是CL Intrinsic(所以不支持其他编译器,但我猜它们有一些等价物)。最后我们释放(推送到堆栈)分配的可执行内存块 - 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);

这将是无论如何 - 在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;它成员对齐)将从RET_FUNC的16偏移处开始。所以编译器无论如何隐式插入8字节填充,但在CODE_STUBCODE_STUB之间的RET_INFO结束时:

RET_FUNC : CODE_STUB, /* 8 byte pad/ RET_INFO将是。为了避免这种情况 - 需要明确地添加这个8字节的填充,但是要开始使用CODE_STUB。这一切都是正确的。请注意,我们使用替换原始返回地址

*Params = p->Function()

哪里

PVOID Function()
{
    return &call;
}

返回CODE_STUB中的调用偏移指令的地址(而不是CODE_STUB的地址) - 所以这个正确处理开始处的任何填充 - 我们无论如何都得到了正确的地址或返回存根


2
投票

那个指南太可怕了。

...我们将原始user32.dll文件复制到Internet Explorer目录中

这会立即使其在其他系统上无法使用。即使您在目标系统上制作副本也没有多大帮助,因为user32导出的函数会随着时间的推移而变化,而中间的.DLL中的人可能不会包含正确的导出/转发器。

代码示例也没有多大意义。你不能真正将__declspec(naked)与本地C变量结合起来。 MSVC甚至可能期望存在堆栈帧并使用EBP来访问这些局部变量。编写的函数只能在C中编码,无需任何内联汇编。

内联汇编的目的可能是处理函数具有参数的情况。

如果我们想象你正在挂钩SetLastError。当调用hook(mySetLastError)时,堆栈在32位x86上看起来像这样:

- Return address (Top of stack, pushed by the parent functions `call`)
- Param 1
- Unknown (probably parent functions local variables etc)

如果你然后删除了返回地址,你将留下参数,这将是一个完美的call到钩子内的实际功能。问题是这实际上并没有起作用。你不能轻易地pop返回地址,因为你无处存储它。我能想到的唯一方法就是使用TLS来存储指向你自己的线程返回地址堆栈的指针。

这可能看起来像这样:

FARPROC GetRealSetLastError()
{
  static FARPROC cache = 0;
  if (!cache) cache = GetProcAddress(LoadLibraryA("kernel32"), "SetLastError");
  return cache;
}

void WINAPI SaveReturn(void*RetAddr)
{
  // TODO: Append to an array stored in TLS
}

void* RestoreReturnHelper()
{
  // TODO: Remove from array and return it
}
__declspec(naked) void* RestoreReturn()
{ __asm {
   push eax ; Save real return value (must also protect edx if you are hooking something that returns a int64)
   call RestoreReturnHelper
   pop ecx
   push eax ; Return address
   mov eax, ecx ; Restore real return value
   ret
} }

__declspec(naked) void WINAPI mySetLastError(UINT error)
{ __asm {

  call SaveReturn ; Removes the return address from the stack
  call GetRealSetLastError ; Could be replaced by push, call, push, push, call to LoadLibrary and GetProcAddress but you probably want to cache the function pointer if speed is important
  call eax ; Call the real function
  call RestoreReturn ; Restore the original return address (without messing up eax)
  ret 
} }

如果你的目标是32位x86以外的任何东西,这个技巧将无法工作。如果目标是AMD64,那么SaveReturn也必须在汇编中编码,因为第一个参数没有存储在堆栈中,因此“pop as a function call”技巧将不起作用。

这实际上取决于你正在挂钩的应用程序,但是如果你可以修改应用程序或使用注入线程的启动器那么你可以使用IAT hookingDetours代替并节省维护Microsoft .DLL副本的痛苦。

© www.soinside.com 2019 - 2024. All rights reserved.