0
点赞
收藏
分享

微信扫一扫

detours内在原理分析

Detours库类似于WTL的来历,是由Galen Hunt and Doug Brubacher自己开发出来,于99年7月发表在一篇名为《Detours: Binary Interception of Win32 Functions.》的论文中。基本原理是改写函数的头5个字节(因为一般函数开头都是保存堆栈环境的三条指令共5个字节:8B FF 55 8B EC)为一条跳转指令,直接跳转到自己的函数开头,从而实现API拦截的。后来得到MS的支持并在其网站上提供下载

 

也就是是inline hook,

inline hook的实现原理可以见这个文章:https://xz.aliyun.com/t/9166

Detours的实现原理是将目标函数的前几个字节改为jmp指令跳转到自己的函数地址,以此接管对目标函数的调用,并插入自己的处理代码

 

从实际例子看detours原理

要绕过目标函数,必须具备两个条件:一个是包含目标函数地址的目标指针,另一个是绕过函数。为了正确拦截目标函数、detour函数和目标指针必须具有完全相同的调用签名,包括参数数和调用约定。使用相同的调用约定可以确保适当地保留寄存器,并确保堆栈在detour函数和目标函数之间正确对齐。

用户代码必须包含detours.h头文件并与detours链接detorus.lib库。

#include <Windows.h>
#include "detours.h"
 
#pragma comment(lib, "detours.lib")
 
static int (WINAPI* OLD_MessageBoxW)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) = MessageBoxW;
 
 
int WINAPI New_MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCation, UINT uType)
{
    int ret = OLD_MessageBoxW(hWnd, L"输入的参数已修改", L"[测试]", uType);
    return ret;
}
 
void HOOK()
{
    DetourRestoreAfterWith();
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
 
    DetourAttach(&(PVOID&)OLD_MessageBoxW, New_MessageBoxW);
 
    DetourTransactionCommit();
}
 
void UnHook()
{
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
 
    DetourDetach(&(PVOID&)OLD_MessageBoxW, New_MessageBoxW);
 
    DetourTransactionCommit();
}
 
void main()
{
    ::MessageBoxW(NULL, L"正常消息框", L"测试", MB_OK);
    HOOK();
    ::MessageBoxW(NULL, L"正常消息框", L"测试", MB_OK);
    UnHook();
    return;
}

 

Detours相对其他一些Hook库和自己实现的代码来说,通常有以下这些优点:

  • 考虑全面,代码非常稳定,并且经过了微软自己众多产品的验证。
  • 可以简单的用纯C/C++代码实现对类的成员函数的Hook。
  • 购买版权之后的Detours Professional还可以支持x64和IA64处理器。以此为基础编写的代码拥有更强的可移植性。
  • 使用简单,不需要了解汇编指令以及技术细节。

detours API

一般来说,使用Detours的代码都具有固定的模式。Detours 1.5和Detours 2.1的接口函数变了很多,这里按照2.1版本对基本的使用方法进行说明。

常用的函数有下面几个:

  • DetourTransactionBegin() :开始一次Hook或者Unhook过程。
  • DetourUpdateThread() :列入一个在DetourTransaction过程中要进行update的线程。这个函数的作用稍微有一些复杂,会在后面专门说明。
  • DetourAttach() :添加一个要Hook的函数。
  • DetourDetach () :添加一个要Unhook的函数。
  • DetourTransactionCommit() :执行当前的Transaction过程。在这个函数中才会真正进行Hook或者Unhook操作。前面三个函数都只是做一些记录工作。

在使用的时候,这几个函数的调用步骤基本上也是按照上面列出来的顺序。举例来说,现在想Hook掉API函数MessageBoxA,将消息框弹出的消息修改掉,可以按下面的方法做。

进行Hook的步骤:

  1. 首先需要定义目标函数的原型。如果目标函数是Windows API,可以到MSDN中查阅,但是需要注意ANSI版本和Unicode版本的区别。如果没有确切的原型声明,或者目标函数是通过逆向工程找出来的,那么需要定义一个和目标函数原型兼容的声明,即参数个数和调用约定要相同。如MessageBoxA的原型是:typedef int (WINAPI *pfnMessageBoxA)( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
  2. 声明一个指向目标函数的函数指针:pfnMessageBoxA g_pMessageBoxA = ::MessageBoxA;
  3. 编写Hook函数的代码,用于替换目标函数。
  4. 调用 DetourTransactionBegin开始一次Detours事务。
  5. 对进程中每个可能调用到目标函数的线程,都需要使用 DetourUpdateThread加入到update队列中。这是因为Hook时修改目标函数的前几个字节,如果某个线程刚好执行到这几个字节的位置时,粗暴的修改掉会造成该线程出现异常。Detours事务处理时,会先枚举并暂停update队列中所有线程,获取它们的指令指针,如果发现这种情况,则将指令指针修改到跳板代码的对应字节上。这样就避免出现崩溃的问题。
  6. 对每个需要Hook的函数,调用 DetourAttach加入到事务列表中。
  7. 调用 DetourTransactionCommit进行实际的Hook操作。

Unhook的过程和上面的流程基本一样,只是第6步改为调用DetourDetach函数。

 

上面的demo编译运行:

detours内在原理分析_API

 

hook后:

detours内在原理分析_windows开发_02

 

拦截过程如下:

此为未Hook前的messageBoxw的前5个字节(原理关键就在这前5个字节上)

 A.....MessageBoxW(NULL, L"还没有Hook", L"测试", MB_OK);

76E0FECF 8B FF                mov         edi,edi  
76E0FED1 55                   push        ebp  
76E0FED2 8B EC                mov         ebp,esp  
76E0FED4 6A 00                push        0  
76E0FED6 FF 75 14             push        dword ptr [ebp+14h]  
76E0FED9 FF 75 10             push        dword ptr [ebp+10h]  
76E0FEDC FF 75 0C             push        dword ptr [ebp+0Ch]  
76E0FEDF FF 75 08             push        dword ptr [ebp+8]  
76E0FEE2 E8 A3 FF FF FF       call        76E0FE8A

B.....此为执行后Hook()的messageBoxw汇编代码

76E0FECF E9 5C 2D F5 89       jmp         NewMessageBoxW (0D62C30h)  
76E0FED4 6A 00                push        0  
76E0FED6 FF 75 14             push        dword ptr [ebp+14h]  
76E0FED9 FF 75 10             push        dword ptr [ebp+10h]  
76E0FEDC FF 75 0C             push        dword ptr [ebp+0Ch]  
76E0FEDF FF 75 08             push        dword ptr [ebp+8]  
76E0FEE2 E8 A3 FF FF FF       call        76E0FE8A  

C....return OldMesssageBoxW(NULL, L"new MessageBox", L"Please", MB_OK);

6EDF00D8 8B FF                mov         edi,edi  
6EDF00DA 55                   push        ebp  
6EDF00DB 8B EC                mov         ebp,esp  
6EDF00DD E9 F2 FD 01 08       jmp         76E0FED4  

有没有发现规律A与B的红色部分都是5个字节,A部的红色代码为保存环境寄存器的,当Hook之后会强制性将这5个字节换成 jmp         NewMessageBoxW (0D62C30h)也就是强制跳转到我们拦截的新函数里面(这就是为什么会拦截的原因),同时会将OldMesssageBoxW (C部分)这个原本指向A(MessageBoxW) 的函数指针 指向一个全新的地址,而这个地址的前5个字节就是从原本A处复制来的环境寄存器的数据(认真看C部分的红色部分代码),然后再jmp         76E0FED4

认真看这个 76E0FED4是不是就是A的首地址 再往后偏移5个字节,这样OldMesssageBoxW就能完成 原本messageBoxw的调用(所以在NewMessageBoxW不能再调用messageBoxw了,因为messageboxw的前5个字节已经修改成调用NewMessageBoxW,这样会造成死循环,应该调用OldMessageBoxw,因为这个OldMessageBoxw相当于未Hook前的Messageboxw)

以上为Hook的原理,而UnHook则是逆过来还原messageBox的前5个字节即可

说明 :

本例程只可拦截本进程的Api调用,因为进程虚拟地址空间的原因,想拦截其他进程的api,则必须将拦截代码注入到对应的进程虚拟地址空间,注入方法有多种(windows注册表,远程线程 全局钩子等等),detours也支持远程代码注入

通过DetourAttach在Hook事务中调用可以拦截目标函数。通过对DetourTransactionBeginDetourTransactionCommit的调用来标记Hook事务。该DetourAttach带有两个参数:目标函数指针的地址和指向DetourFunction函数的指针。目标函数未作为参数提供,因为它必须已经存储在目标指针中。

DetourUpdateThread提供在目标线程中声明,以便在事务提交时更新指令指针。

DetourAttach分配和准备跳转调用目标函数。Hook事务提交时,将重写目标函数和跳转函数Trampoline,并更新目标指针以指向跳转函数

detours内在原理分析_Windows_03

detours内在原理分析_#include_04

一旦Hook目标函数,对目标函数的任何调用都将通过DetourFunction函数重新路由。通过Trampoline调用目标函数时,DetourFunction函数负责复制参数。这很明确,因为目标函数是DetourFunction函数的内部调用函数。

通过DetourDetach在Hook事件中的调用,可以删除对目标函数的拦截。与DetourAttach一样,DetourDetach也有两个参数:目标指针的地址和指向DetourFunction函数的指针。当Hook事务提交时,目标函数将被重写并恢复为其原始代码,Trampline函数将被删除,目标指针将恢复为指向原始目标函数。

如果需要将Hook的API注入到目标程序中,则Hook需要编译成Dll。可以使用远程线程注入、Dll劫持等技术。也可以使用DetourCreateProcessWithDllExDetourCreateProcessWithDlls将Dll注入到目标进程中。如果使用DetourCreateProcessWithDllExDetourCreateProcessWithDlls,则DllMain函数必须调用DetourRestoreAfterWith。如果Dll可以在x86和x64混合环境中使用,则DllMain函数必须调用DetourIsHelperProcess导出为序号1,rundll32.exe以执行帮助任务来调用该API。

 

一. Detours的原理 

---- 1. WIN32进程的内存管理 

---- 总所周知,WINDOWS NT实现了虚拟存储器,每一WIN32进程拥有4GB的虚存空间, 关于WIN32进程的虚存结构及其操作的具体细节请参阅WIN32 API手册, 以下仅指出与Detours相关的几点: 

---- (1) 进程要执行的指令也放在虚存空间中 
---- (2) 可以使用QueryProtectEx函数把存放指令的页面的权限更改为可读可写可执行,再改写其内容,从而修改正在运行的程序 
---- (3) 可以使用VirtualAllocEx从一个进程为另一正运行的进程分配虚存,再使用 QueryProtectEx函数把页面的权限更改为可读可写可执行,并把要执行的指令以二进制机器码的形式写入,从而为一个正在运行的进程注入任意的代码 

---- 2. 拦截WIN32 API的原理 

---- Detours定义了三个概念: 

---- (1) Target函数:要拦截的函数,通常为Windows的API。 
---- (2) Trampoline函数:Target函数的复制品。因为Detours将会改写Target函数,所以先把Target函数复制保存好,一方面仍然保存Target函数的过程调用语义,另一方面便于以后的恢复。 
---- (3) Detour 函数:用来替代Target函数的函数。 

---- Detours在Target函数的开头加入JMP Address_of_ Detour_ Function指令(共5个字节)把对Target函数的调用引导到自己的Detour函数, 把Target函数的开头的5个字节加上JMP Address_of_ Target _ Function+5作为Trampoline函数。例子如下: 

拦截前:Target _ Function:
;Target函数入口,以下为假想的常见的子程序入口代码
push ebp
mov ebp, esp
push eax
push ebx
Trampoline:
;以下是Target函数的继续部分
……

拦截后: Target _ Function:  ==》就是inline hook
jmp Detour_Function
Trampoline:
;以下是Target函数的继续部分
……

Trampoline_Function:
; Trampoline函数入口, 开头的5个字节与Target函数相同
push ebp
mov ebp, esp
push eax
push ebx
;跳回去继续执行Target函数
jmp Target_Function+5
---- 3. 为一个已在运行的进程装入一个DLL 

---- 以下是其步骤: 

---- (1) 创建一个ThreadFuction,内容仅是调用LoadLibrary。 
---- (2) 用VirtualAllocEx为一个已在运行的进程分配一片虚存,并把权限更改为可读可写可执行。 
---- (3) 把ThreadFuction的二进制机器码写入这片虚存。 
---- (4) 用CreateRemoteThread在该进程上创建一个线程,传入前面分配的虚存的起始地址作为线程函数的地址,即可为一个已在运行的进程装入一个DLL。通过DllMain 即可在一个已在运行的进程中运行自己的代码。 

 

使用Detours的注意事项

总体来说,Detours库的代码是非常稳定的,但是如果使用方法不对,会造成一些问题。有下面一些地方需要特别注意:

  1. 一定要枚举线程并调用DetourUpdateThread函数。否则可能出现很低几率的崩溃问题,这种问题很难被检查出来。
  2. 如果Hook函数在DLL中,那么绝大多数情况下不能在Unhook之后卸载这个DLL,或者卸载存在造成崩溃的危险。因为某些线程的调用堆栈中可能还包含Hook函数,这时卸载掉DLL,调用堆栈返回到Hook函数时内存位置已经不是合法的代码了。
  3. Detours库设计时并没有考虑到卸载的问题,这是因为钩子的卸载本身是不安全的。当Detours库代码存在于DLL中的时候,即使Unhook了所有函数,清理了所有自己使用到的函数,还是会占用一些内存。卸载这个DLL会造成内存泄露,特别是反复的进行加载DLL->Hook->Unhook->卸载DLL的过程,会让这个问题变得非常严重。后面会用一篇专题文章来讨论Detours内存泄露问题的调试和解决。
  4. 有一些非常短的目标函数是无法Hook的。因为jmp指令需要占用一定空间,有些函数太过短小,甚至不够jmp指令的长度,自然是没有办法Hook掉的。

Detours不支持9x内核的Windows系统。因为9x内核下的内存模型和NT内核下有非常大的差别。

 

 

inline hook的补充:

先看Window API Hook的原理与应用

这里我以MessageBox为经典的例子,实践下如何使用Window API

MessageBox函数说明:

int MessageBox(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

这里我们用visual stdio新建一个c++的项目,来学习下如何使用该API:

HookTest.cpp

#include <windows.h>
#include <iostream>
using namespace std;
int main()
{
    int rtCode = MessageBox(NULL, (LPCWSTR)L"内存地址越界,程序已经被终止!", (LPCWSTR)L"程序出现了错误", MB_ICONASTERISK| MB_OKCANCEL);
    cout << rtCode << endl;
    switch (rtCode) {
        case 1:
            cout << "选择了确定按钮!" << endl;
            break;
        case 2:
            cout << "选择了取消按钮!" << endl;
            break;
        default:
            cout << "其他操作!" << endl;
    }

    return 0;
}

detours内在原理分析_Windows_05

 

 

一般推荐直接使用windows.h头文件,避免出现一些其他的问题,windows.h包括了其他的window头文件

0x4 Windows API Hooking

了解了Win API的调用过程之后,我们可以来学习hook(挂钩)的技术。

API Hooking是一种我们可以检测和修改API调用的行为和流程的技术。

流程图大致如下:

detours内在原理分析_API_06

 

 

那么这种技术的实现原理是什么呢?

hook的技术可能有非常多种,笔者这里先以x86环境下的inline hook技术作为讲解,帮助萌新入门。

这里针对理解这个,笔者比较喜欢先run成功再debug分析原理。

0x4.1 实现hook MessageBoxA

选择x86的方式编译,x64的话会失败。

#include <iostream>
#include <Windows.h>

FARPROC messageBoxAddress = NULL;
SIZE_T bytesWritten = 0;
char messageBoxOriginalBytes[6] = {};

int __stdcall HookedMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {

    // print intercepted values from the MessageBoxA function
    std::cout << "Ohai from the hooked function\n";
    std::cout << "Text: " << (LPCSTR)lpText << "\nCaption: " << (LPCSTR)lpCaption << std::endl;

    // unpatch MessageBoxA
    WriteProcessMemory(GetCurrentProcess(), (LPVOID)messageBoxAddress, messageBoxOriginalBytes, sizeof(messageBoxOriginalBytes), &bytesWritten);

    // call the original MessageBoxA
    return MessageBoxA(NULL, lpText, lpCaption, uType);
}

int main()
{
    // show messagebox before hooking
    MessageBoxA(NULL, "hi", "hi", MB_OK);

    HINSTANCE library = LoadLibraryA("user32.dll");
    SIZE_T bytesRead = 0;

    // get address of the MessageBox function in memory
    messageBoxAddress = GetProcAddress(library, "MessageBoxA");

    // save the first 6 bytes of the original MessageBoxA function - will need for unhooking
    ReadProcessMemory(GetCurrentProcess(), messageBoxAddress, messageBoxOriginalBytes, 6, &bytesRead);

    // create a patch "push <address of new MessageBoxA); ret"
    void *hookedMessageBoxAddress = &HookedMessageBox;
    char patch[6] = { 0 };
    memcpy_s(patch, 1, "\x68", 1);
    memcpy_s(patch + 1, 4, &hookedMessageBoxAddress, 4);
    memcpy_s(patch + 5, 1, "\xC3", 1);

    // patch the MessageBoxA
    WriteProcessMemory(GetCurrentProcess(), (LPVOID)messageBoxAddress, patch, sizeof(patch), &bytesWritten);

    // show messagebox after hooking
    MessageBoxA(NULL, "hi", "hi", MB_OK);

    return 0;
}

这里代码中第一次没hook,正常调用原始的,然后经过hook自身线程之后,在调用就会被hook,从而进入我们自定义的执行逻辑:

detours内在原理分析_API_07

 

 

0x4.2 分析Hook的原理

程序首先使用LoadLibrary加载模块(user32.dll),然后返回句柄。

HINSTANCE library = LoadLibraryA("user32.dll");

接着使用GetProcAddress获取模块dll指定导出函数的地址

messageBoxAddress = GetProcAddress(library, "MessageBoxA");

接着调用ReadProcessMemory读取当前进程的内存空间中MessageBoxA函数的开头前6个字节存在于messageBoxOriginalBytes字节数组。

ReadProcessMemory(GetCurrentProcess(), messageBoxAddress, messageBoxOriginalBytes, 6, &bytesRead);

接着在这里就是实现patch内存空间,修改执行流程的操作了,这里直接打一个断点debug

这里先用patch字节数组存储了一些指令,具体是什么debug看就行了,其实也很简单就是jmp hookedMessageBoxAddress

// create a patch "push <address of new MessageBoxA); ret"
    void *hookedMessageBoxAddress = &HookedMessageBox;
    char patch[6] = { 0 };
    memcpy_s(patch, 1, "\x68", 1);
    memcpy_s(patch + 1, 4, &hookedMessageBoxAddress, 4);
    memcpy_s(patch + 5, 1, "\xC3", 1);

写好patch数组,之后开始修改进程的内存空间,修改指令。

WriteProcessMemory(GetCurrentProcess(), (LPVOID)messageBoxAddress, patch, sizeof(patch), &bytesWritten);

主要是修改(patch)了messageBoxA这个导出函数在内存位置的前6个字节为我们定义的指令,至于是啥没关系,我们存储下来,后面再unpatch回来即可了。

patch之后呢?

通过将hookedMessageBoxAddress的地址压入了栈顶,然后ret,其实本质就是pop eip, jmp eip

\x68就是push,\xc3就是ret,然后32位的程序,地址刚好4字节,patch数组的构造原理就是这样。

然后我们重新调用,hook的MessageBoxA(NULL, "hi", "hi", MB_OK);,就会进入HookedMessageBox

int __stdcall HookedMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {

    // print intercepted values from the MessageBoxA function
    std::cout << "Ohai from the hooked function\n";
    std::cout << "Text: " << (LPCSTR)lpText << "\nCaption: " << (LPCSTR)lpCaption << std::endl;

    // unpatch MessageBoxA
    WriteProcessMemory(GetCurrentProcess(), (LPVOID)messageBoxAddress, messageBoxOriginalBytes, sizeof(messageBoxOriginalBytes), &bytesWritten);

    // call the original MessageBoxA
    return MessageBoxA(NULL, lpText, lpCaption, uType);
}

这个就很简单了,执行hook想要执行的操作,然后unhook,然后正常调用就行了。

这种劫持方法,可以说真的蛮简洁的,也非常易懂,比较暴力,没有过多的计算。

0x4.2 探讨32位和64位的区别


 

举报

相关推荐

0 条评论