【问题标题】:How to prepare prolog and epilog assembly to intercept function with parameters?如何准备 prolog 和 epilog 程序集以拦截带参数的函数?
【发布时间】:2017-12-15 20:02:41
【问题描述】:

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

另外,当拦截这个函数时,我如何访问它的参数来执行一些检查?对不起,如果这是一个微不足道的问题,也许我搜索和研究了错误的材料。

【问题讨论】:

  • 通过弹出返回地址,看起来函数试图完全透明,即当包装器调用真正的函数时,args 将按预期在堆栈上。但随后它使用一些本地人在调用过程中将内容保存在堆栈上(例如retaddr),这违背了这样做的目的,因为esp 将低于调用者的参数。或者也许我读错了。无论如何,这显然是一个仅限 3​​2 位的 hack,Windows 对 x86-64 使用不同的调用约定(带有一些寄存器参数)。 (哦,MSVC inline-asm 也只能在 32 位中工作。)
  • 我能说这很糟糕吗? (我质疑您正在使用的文章的作者)?裸属性删除所有堆栈帧序言,这意味着 EBP 甚至不会正确设置。这意味着您将在您不期望的地方将局部变量存储到堆​​栈中。如果此代码运行时没有崩溃,那只是偶然的恕我直言
  • 哇哦,那更糟了。我假设如果你使用任何本地人它仍然会为本地人保留堆栈空间(并相对于[esp]解决它们)。 naked 真的可以让你创建严重损坏的函数!

标签: winapi assembly visual-c++ x86 hook


【解决方案1】:

任务通常足够复杂,需要一些汇编代码(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

这里有 CreateFileWCloseHandle 的 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_HEADERInterlockedPopEntrySList(用于分配条目)和InterlockedPushEntrySList(用于免费条目)将g_rf[1024]; 的数组推送到无锁堆栈结构。这是最大的快速和有效。

c++ 常见的 prolog 是 CommonStub - 在这里我们可以在调用之前检查函数参数和可选的钩子返回 (*Params = p-&gt;Function();)。

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

_AddressOfReturnAddressCL 内在的(因此其他编译器不支持,但我猜他们有一些等价物)。最后我们释放(推送到堆栈)分配的可执行内存块 - 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) -&gt; $(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_STUBoffset 成员的结尾)和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_STUBRET_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 的地址) - 所以这个正确的处理开始时的任何填充 - 我们无论如何得到了正确的地址或返回存根

【讨论】:

  • 非常好。 RET_INFO 中存在参数限制,因此人们需要注意这一点。
  • @Anders - 是的,在这一点上添加了说明(我们真的可以增加数组元素 cout)。但是最初,只是从堆栈中复制参数不正确(我们可能会超出堆栈顶部限制) - 添加检查这一点的代码并在这种情况下复制更少的参数。 + asm 代码 x64 和 x64 上特殊对齐的说明(在 CODE_STUB 的开头填充)
【解决方案2】:

那个指南太可怕了。

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

这立即使它无法在其他系统上使用。即使您在目标系统上进行复制也无济于事,因为 user32 导出的函数会随着时间而变化,而您在中间的 .DLL 可能不包含正确的导出/转发器。

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

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

如果我们想象你正在挂钩 SetLastError。当调用钩子 (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 也必须在汇编中编码,因为第一个参数不存储在堆栈中,因此“作为函数调用弹出”技巧将不起作用。

这实际上取决于您要挂钩的应用程序,但如果您可以修改应用程序使用注入线程的启动器,那么您可以改用 IAT hookingDetours 并保存自己维护 Microsoft .DLL 副本的痛苦。

【讨论】:

  • 哈哈我几乎同时在这个问题下发表了评论。你说它可怕,我说它可怕。我想我更喜欢你的形容词
  • 真的这几乎是不可能用 c/c++ 实现的(裸函数/内联汇编器)。需要 masm 或其他 asm 用于写入潜艇。和主要说明 - 使用 TLS 或这里不需要。存在更清洁的解决方案
  • @RbMm 你至少可以在这里描述你的解决方案。
猜你喜欢
  • 1970-01-01
  • 2010-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-02
相关资源
最近更新 更多