API挂钩之Detours
relay 发表于 陕西 二进制安全 1858浏览 · 2024-01-26 01:57

简介

API挂钩是一种可以拦截和修改Windows API行为的一种技术,比如现在很多成熟的EDR产品,都对R0或R3层的Windows API进行了HOOK操作,API挂钩一般使用自定义的API函数来替换现有的API函数进行实现,自定义的API函数会在调用初始函数之前或之后进行添加一些附加的操作,这种其实就是类似于Java中的AOP。

为什么要挂钩?

API挂钩主要用于恶意二进制软件分析和调试的,但是它也是可以用作恶意软件开发,比如:

收集敏感信息或数据 (例如凭据)
出于恶意目的修改和拦截函数调用
通过改变操作系统或程序的行为方式来绕过安全措施,比如AMSI ETW

如何挂钩?

实现API 挂钩有很多方式,我们可以利用微软开源Detours库来进行使用或者minhook库,如下:

https://github.com/microsoft/Detours
https://github.com/TsudaKageyu/minhook

API挂钩-Detours

简介

Detours Hook Library是一个软件库,主要用于拦截和重定向Windows中的函数调用,这个函数可以将指定的函数调用重定向到用户自定义的替换函数,用户自定义的这个函数可以执行其他一些操作或者修改原来的函数的一些行为。

Transactions

Detours库用于无条件跳转到用户提供的自定义函数来替换即将要挂钩的函数的前几条指令,其实就是jmp。

Detours库使用事务从目标函数进行挂钩或者取消挂钩,使用事务时,可以启动一个新的事务,添加函数挂钩之后,然后提交,提交事务之后,添加到事务中的所有函数挂钩都将应用于程序,就像取消挂钩的情况一样。

使用

首先我们需要编译上面的Detours 库,编译详细如下:

https://blog.csdn.net/qing666888/article/details/81540683

这里我已经编译好了并且已经引入进来了。

最后将其通过#pragma comment进行引入即可。需要注意的是还需要将detours.h头文件引入。

32位和64位的Detours库

_M_X64:为面向 x64 处理器或 ARM64EC 的编译定义为整数文本值 100。 其他情况下则不定义。

_M_IX86: 为面向 x86 处理器的编译定义为整数文本值 600。 对于面向 x64 或 ARM 处理器的编译,则不定义此宏。

如下代码:

#ifdef _M_X64
#pragma comment (lib, "C:\\Users\\Admin\\Desktop\\Detours-4.0.1\\lib.X64\\detours.lib")
#endif 

#ifdef _M_IX86
#pragma comment (lib, "C:\\Users\\Admin\\Desktop\\Detours-4.0.1\\lib.X86\\detours.lib")
#endif

这里的#ifdef _M_X64检查宏_M_X64是否已定义,如果定义的话那么就包含如下的路径中的.lib文件,同样x86这里也是一样的。

无论是x86还是x64的detours.lib都是在编译Detours库时创建的,只是不同版本编译不同的而已。

比如说我们现在VS使用x86的编译,这里_M_IX86就亮亮了。

Detours API

当我们去挂钩函数时,第一步需要去获取到挂钩函数的地址,拿到它的地址来确定跳转指令的放置位置。

Detours库提供如下的一个API函数:

DetourTransactionBegin 附加或分离新事务,挂钩或取消挂钩应首先调用此函数。
DetUpdateThread  更新当前事务。Derours库是使用它来登记当前事务中的线程
DetourAttach 在当前事务的目标函数上安装挂钩 在调用DetourTransactionCommit之前此操作不会提交
DetourTransactionCommit 提交当前事务以附加或者分离。

替换Hooked API

接下来我们需要创建一个函数来替换挂钩的函数,替换函数需要具有相同的数据类型,并且可以选择相同的参数,但是需要注意的是你替换的这个函数不能比原始函数的参数多,如果多的话它将无法访问地址,从而引发访问冲突异常。

挂钩中的问题

那么我们替换之后,当调用挂钩函数的时候他会触发挂钩,然后去执行我们自定义的函数,我们自定义函数中必须返回原始挂钩函数应该返回的有效值,比如说GetProcAddress返回的是函数的地址,那么我们自定义函数中也需要返回函数的地址,这样是为了继续执行流程,一种简单的方式其实就是通过在自定义函数中调用原始函数来返回相同的值,就比如说我们挂钩的函数时GetProcAddress,那么我们自定义函数中也调用一下GetProcAddress函数,但是这样的话会出现一个问题死循环的问题,就是说当我们在自定义函数中去调用挂钩函数,那么又会回到我们自定义的函数中。

比如说我们如下代码:

当我们在GetProcAddress上挂钩时,他会跳到我们自定义的MyProcAddress函数,然后我们里面又调用了

GetProcAddress,又会触发挂钩,所以又会到我们自定义的MyProcAddress函数,这样就变成了死循环。

#include <Windows.h>
#include <stdio.h>
#include "detours.h" 
#ifdef _M_X64
#pragma comment (lib, "C:\\Users\\Admin\\Desktop\\Detours-4.0.1\\lib.X64\\detours.lib")
#endif
#ifdef _M_IX86
#pragma comment (lib, "C:\\Users\\Admin\\Desktop\\Detours-4.0.1\\lib.X86\\detours.lib")
#endif

FARPROC WINAPI MyProcAddress(HMODULE hmodule, LPCSTR functionName) {
    //.... 

    return GetProcAddress(hmodule,functionName);
 }


int main() {

}

解决方案

其实解决的话也好解决,我们可以通过在挂钩之前保存指向原始函数的指针来解决此问题,但是这样的话有点麻烦,最简单的办法就是调用它名子不一样但是功能是一样的函数,比如MessageBoxA和MessageBoxW一个是A系函数一个是W系函数,其实功能都是一样的,这样的话就很好的解决了死循环的问题。

例如如下代码:

#include <Windows.h>
#include <stdio.h>
#include "detours.h" 
#ifdef _M_X64
#pragma comment (lib, "C:\\Users\\Admin\\Desktop\\Detours-4.0.1\\lib.X64\\detours.lib")
#endif
#ifdef _M_IX86
#pragma comment (lib, "C:\\Users\\Admin\\Desktop\\Detours-4.0.1\\lib.X86\\detours.lib")
#endif
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {

    // ...

    return MessageBoxW(hWnd, L"OK", L"OK", uType);

}
int main() {

}

挂钩程序

Detours库使用事务进行工作,所以我们如果需要挂钩API函数的话就必须创建事务,向事务提交相关的操作,比如挂钩或者取消挂钩,最后提交事务即可。

如下代码:

#include <Windows.h>
#include <stdio.h>

#include "detours.h" 
#ifdef _M_X64
#pragma comment (lib, "C:\\Users\\Admin\\Desktop\\Detours-4.0.1\\lib.X64\\detours.lib")
#endif
#ifdef _M_IX86
#pragma comment (lib, "C:\\Users\\Admin\\Desktop\\Detours-4.0.1\\lib.X86\\detours.lib")
#endif



typedef HMODULE (WINAPI* fnGetModuleHandleA)(
    _In_opt_ LPCWSTR lpModuleName
);
fnGetModuleHandleA pGetModuleHandleA = GetModuleHandle;

HMODULE WINAPI MyGetModuleHandleA(LPCWSTR lpModuleName) {

    return GetModuleHandleA("NTDLL.DLL");
}
BOOL HookTest() {
    DWORD   dwDetoursErr = NULL;

    if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
        return FALSE;
    }
    if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
        return FALSE;
    }
    if ((dwDetoursErr = DetourAttach((PVOID*)&pGetModuleHandleA, MyGetModuleHandleA)) != NO_ERROR) {
        return FALSE;
    }

    if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
        return FALSE;
    }

    return TRUE;
}

int main() {
    //没有挂钩之前:
    HMODULE hmodule = GetModuleHandle(L"kernel32.dll");

    //进行HOOK
    HookTest();

    HMODULE hmodule2 = GetModuleHandle(L"kernel32.dll");

    getchar();
}

这里我们在hmodule2这里下一个断点然后按F10可以发现他会跳转到我们的自定义的那个函数。

它这里原本获取的是kernel32.dll,这里我们给他改成获取NTDLL.DLL。


可以发现已经更改了。


我们进入反汇编来看一下。

我们跟进CALL。

这里会发现我们已经HOOK成功了。它跳转的地址是我们自定义函数的地址。

取消挂钩程序

这里的取消挂钩和上面的挂钩是差不多的。

BOOL Unhook() {

    DWORD   dwDetoursErr = NULL;

    if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
        return FALSE;
    }
    if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
        return FALSE;
    }
    if ((dwDetoursErr = DetourDetach((PVOID*)&pGetModuleHandleA, MyGetModuleHandleA)) != NO_ERROR) {
        return FALSE;
    }

    if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
        return FALSE;
    }

    return TRUE;
}

然后我们再去获取kernel32.dll会发现成功获取到kernel32.dll的基地址而不是NTDLL.DLL的基地址了。

0 条评论
某人
表情
可输入 255