通过 Dll 注入实现应用层跨进程 inline 挂钩

一、Detours 挂钩的原理

Detours 是微软提供的一个开发库,可以简单、高效且稳定的实现 API inline hook。

Detours 是一个可以在 x86、x64 和 IA64 平台上挂钩任意 win32 函数的程序开发库,它通过在需要进行挂钩的目标函数头部重写内存代码而达到接管函数控制权,进而实现自己功能的目的。除此之外,Detours 还可将任意的 Dll 或数据片段(有效载荷)注入到任意 win32 二进制文件中。

利用 Detours 库挂钩的原理其实很简单,首先我们要搞清楚 Detours 里面的三个概念:

  • Target 函数:被挂钩的函数,通常情况下为 Windows API。
  • Trampoline 函数:也叫蹦床函数,顾名思义,用来跳转的。由于我们需要在 Target 函数头写入 jmp 指令跳到我们的 Detours 函数,那么就会覆盖头部对应长度的指令,而这部分指令是没有执行的,那么我们要将这部分指令写入到我们创建的蹦床内存,对这段未在 Target 函数中执行的部分头部代码进行执行,最后再跟一个 jmp 指令,跳回 Target 函数中剩余的指令继续执行。
  • Detours 函数:该函数就是我们挂钩后实现功能的函数,用来代替 Target 函数,这里面首先实现了 Target 函数的功能(通过调用 Trampoline 函数执行了原函数的完整代码),返回后继续实现我们自己的功能。

由于是在 Detours 函数中调用 Trampoline 来间接完整调用原函数的功能,然后 Trampoline 函数返回后程序的执行流程又被 Detours 函数接管了,此时实现我们自己的功能,完成挂钩过程。

所以 Detours 挂钩的原理就很简单了。首先我们需要在 Target 函数头部写入一个 5 字节的 jmp 指令,在 x64 系统下该 jmp 会跳到另一个中转的 jmp其实这是一个 Stub 函数),然后才会跳转到我们的 Detours 函数。

在 Windows 系统中,许多系统调用(如 MessageBoxA)实际上是通过 Stub 函数 实现的,它是一个简单的跳板函数,也称做桩函数,通过这种设计有以下好处:
动态加载和延时绑定:Windows 系统允许某些 DLL 在运行时动态加载,Stub 函数的存在使得程序可以在运行时动态解析和绑定实际的函数地址。例如,user32.dll 中的 MessageBoxA 函数可能在程序启动时尚未加载,Stub 函数可以通过 jmp 指令动态跳转到正确的地址。
兼容性和版本更新:Stub 函数提供了一种兼容性机制,即使系统更新后函数的实现发生了变化,程序仍然可以通过 Stub 函数找到正确的入口点。例如,Windows 的不同版本可能对函数的实现进行了优化或修改,但 Stub 函数的地址保持不变,从而保证了程序的兼容性。
此外,在 x64 系统中,系统调用(Syscall)的实现也有类似的机制,用户态程序通过 syscall 指令进入内核态,内核态的入口点可能是一个 Stub 函数,它会进一步跳转到实际的系统调用处理函数。

在 Detours 函数中,首先会执行一些和 CRT 相关的代码,由 VS 编译器加入的和调试相关的代码(Debug 版本),然后会调用 Trampoline 函数,执行完 Target 函数的功能返回后,此时就可以执行我们自己的代码了,下面是流程图:

注:如果我们在 Detours 函数中调用 Target 函数就会又跳到 Detours 函数,形成死循环!!!此时 Trampoline 函数就发挥作用了,在将 jmp 指令写入 Target 函数头部之前,将即将被覆盖的汇编指令拷贝到 Trampoline 函数中,最后再跟上一个 jmp 指令,跳转到 Target 剩余的部分继续执行,完成一个完整的 Target 函数功能调用。

上面流程图中有两个值得注意的点:

(1)我们只写入了 5 字节的 jmp 指令,为什么第 6、7 个字节变成了 CC?

实际上我们虽然写入了一个 5 字节的 jmp 指令,但我们看原 Target 函数第一条指令只有 4 个字节,那么 jmp 指令的第 5 个字节就会让原 Target 函数的第二条指令变形,所以如果函数头部开始的 5 字节不是完整的汇编指令集,Detours 通过反汇编引擎计算指令长度,自动 hook 大于 5 字节且保证覆盖最少的字节数,来保证汇编指令的完整性

(2)为什么我自己汇编的 jmp 指令长度不止 5 字节,而 Detours 汇编的 jmp 指令长度固定为 5 字节?

首先我们自己在构建 jmp 指令的时候,可能进行的是远跳转,如果我们是通过 malloc 指令为 Trampoline 函数分配的堆空间,则在构建绝对地址跳转的时候汇编代码会较长,而 Detours 是在原被 hook 指令的附近分配内存空间,用的是相对地址跳转,因此一般情况下 jmp 指令的长度固定为 5 字节。

二、导入 Detours 库

首先,我们要想挂钩其他进程中的函数,我们可以新建一个 Dll 动态链接库工程,将需要挂钩的功能写到 Dll 中,然后我们将这个 Dll 注入到目标进程,即可实现跨进程挂钩的功能。

新建好 Dll 工程后,我们就可以导入 Detours 库了,我们可以直接去 github 上下载:
https://github.com/microsoft/Detours

下载完之后我们将其中的 Detours 文件夹导入工程,如下图所示:

注:这个 DetoursHook.cpp 和 DetoursHook.h 是自己新建的用于实现 Detours 功能的文件。

选中所有导入的 cpp 后缀文件名文件(包括自己建立的),然后 右键 -> 属性 -> C/C++ -> 预编译头 -> 不使用预编译头(预编译头主要用于大幅度提高大型项目的代码编译速度)。

然后右键 uimports.cpp 文件 -> 属性 -> 常规 -> 从生成中排除 -> 是,将该文件在编译中排除。

此时我们就完成了 Detours 库的导入(当然也可以导入静态库),直接对项目进行编译即可。

三、使用 Detours 库函数进行 hook

我们以挂钩系统函数 MessageBoxA 为例。

step 1

首先我们需要定义一个指向 MessageBoxA 的函数指针:

// 定义函数指针
typedef int (__stdcall *PFUNMESSAGEBOXA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
PFUNMESSAGEBOXA pFunMessageBox = (PFUNMESSAGEBOXA)MessageBoxA;

其实这个函数指针就是 Trampoline 函数,后续当程序流程跳转到 Detours 函数时,我们通过该函数指针来执行完原 Target 函数的流程,然后会继续返回到 Trampoline 函数,这样我们就达到了接管执行控制权的目的。

注:指定函数调用约定指定为 __stdcall 是为了兼容性考虑(x32 和 x64 编译环境下 MessageBoxA 的调用约定不一致,指定为 __stdcall 可以保证程序同时在 x32 和 x64 环境下运行)。

step 2

然后我们来构建 Detours 函数:

// MessageBoxA 的 Detours 函数
int __stdcall DetoursMessageBoxA(IN HWND hWnd, IN LPCSTR lpText, IN LPCSTR lpCaption, IN UINT uType)
{
	// 调用原来的 Api
	int result = 0;
	result = ((PFUNMESSAGEBOXA)pFunMessageBox)(hWnd, lpText, lpCaption, uType);

	// 进行自己的操作
	outputMsg("调用了 MessageBox 函数,挂钩地址为: %p\r\n", MessageBoxA);

	return result;
}

Detours 函数的函数原型必须和 Target 函数一致,我们可以看到我们在 Detours 函数中,通过 PFUNMESSAGEBOXA 函数指针调用执行了原 Target 函数,然后当 PFUNMESSAGEBOXA(Trampoline)函数返回的时候,再执行我们自己的流程,这里我们进行了一个格式输出,因为我们是把生成的 Dll 注入到其他进程内,因此我们使用普通的 printf 等调试输出指令是无法显示消息的,因此我们需要自己重写一个格式输出函数,执行函数的时候通过 DbgView 等软件对输出的调试信息进行捕获,outputMsg 格式输出函数的定义如下:

void outputMsg(const char* pszFormat, ...) {
//#ifdef DEBUG
	char szbufFormat[0x1000];
	char szbufFormat_Game[0x1100] = "";
	va_list argList;
	va_start(argList, pszFormat);
	vsprintf_s(szbufFormat, pszFormat, argList);
	strcat_s(szbufFormat_Game, "log: ");  // 加上输入头特征,用于调试信息过滤
	strcat_s(szbufFormat_Game, szbufFormat);
	OutputDebugStringA(szbufFormat_Game);
	va_end(argList);
//#endif
}

step 3

然后我们编写 Target 的挂钩函数:

// 开启挂钩
void HookOn()
{
	// 开始事务
	DetourTransactionBegin();

	// 更新线程信息
	DetourUpdateThread(GetCurrentThread());

	// 对函数进行挂钩
	DetourAttach(&pFunMessageBox, DetoursMessageBoxA);

	// 提交事务(提交后挂钩才生效)
	DetourTransactionCommit();
}

// 解除挂钩
void HookOff()
{
	// 开始事务
	DetourTransactionBegin();

	// 更新线程信息
	DetourUpdateThread(GetCurrentThread());

	// 对函数进行挂钩
	DetourDetach(&pFunMessageBox, DetoursMessageBoxA);

	//提交事务(提交后挂钩才生效)
	DetourTransactionCommit();
}

其中开始事务、更新线程信息提交事务是必须的,可以一次性对多个函数进行挂钩和解挂,其中 DetourTransactionBeginDetourUpdateThread 函数用于保证多线程情况下的安全性,最后执行完 DetourTransactionCommit 函数挂钩和解挂才会生效。而 DetourAttachDetourDetach 函数比较简单,第一个参数为 Target 函数指针的地址,第二个参数为 Detours 函数,如果需要获取关于挂钩的更多信息,可以调用 DetoursAttachEx 函数:

LONG DetourAttachEx(
    _Inout_   PVOID * ppPointer, // 指向 Target 或 Trampoline 函数的二级指针(这个二级指针在 DetourAttachEx 函数执行前
                                 //  指向 Target 函数,执行完 DetourAttachEx 函数后指向 Trampoline 函数。
    _In_      PVOID pDetour,  // Detours 函数 = Trampoline 函数(执行 Target 函数完整功能) + 实现自己功能的代码
    _Out_opt_ PDETOUR_TRAMPOLINE * ppRealTrampoline // 用于接收 Trampoline 函数地址的信息
    _Out_opt_ PVOID * ppRealTarget  // 用于接收 Target 函数的最终地址
    _Out_opt_ PVOID * ppRealDetour  // 用于接收 Detours 函数的最终地址
    );

struct _DETOUR_TRAMPOLINE
{
    BYTE            rbCode[30];     // target code + jmp to pbRemain Trampoline 函数地址
    BYTE            cbCode;         // size of moved target code.  修改前几个字节(5)
    BYTE            cbCodeBreak;    // padding to make debugging easier.
    BYTE            rbRestore[22];  // original target code.
    BYTE            cbRestore;      // size of original target code.
    BYTE            cbRestoreBreak; // padding to make debugging easier.
    _DETOUR_ALIGN   rAlign[8];      // instruction alignment array.
    PBYTE           pbRemain;       // first instruction after moved code. [free list]
    PBYTE           pbDetour;       // first instruction of detour function.
};

step 4

最后,我们就在主函数中进行调用:

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        HookOn();
        MessageBoxA(NULL, "测试用例", NULL, NULL);
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        HookOff();
        break;
    }
    return TRUE;
}

我们打开 DbgView 软件,设置过滤头为 log:

点击 Capture 设置如下:

然后就可以通过进程注入工具将我们生成的 Dll 文件注入到目标进程中:

当我们点击完确定,执行完 Trampoline 函数,就会继续返回到我们构建的 Detours 函数,执行我们预定的调试输出信息,被 DbgView 捕获:

此时说明 MessageBoxA 函数已经被我们成功挂钩。

三、其他的 Detours 库函数

1. 用于查找目标函数的 API

DetourFindFunction

PVOID DetourFindFunction(
    _In_ LPCSTR pszModule,  //应在其中找到函数的DLL或二进制文件的路径。
    _In_ LPCSTR pszFunction  //要找到的函数的名称。
    );
//如果成功,则返回函数pszFunction的地址;否则,返回NULL。

DetourFindFunction 尝试通过动态链接命名模块的导出表来检索命名函数的函数指针,如果失败,则使用DbgHelp API(如果可用)调试符号。

DetourCodeFromPointer

PVOID DetourCodeFromPointer(
    _In_      PVOID pPointer,  //指向函数的指针。
    _Out_opt_ PVOID *ppGlobals //变量,用于接收函数的全局数据的地址。
    );
//返回指向实现该功能的代码的指针。

DetourCodeFromPointer返回指向实现函数指针所指向的函数的代码的指针。当二进制文件与DLL静态链接时,指向DLL函数的指针通常不指向DLL中的代码,而是指向二进制文件的导入表中的跳转语句。DetourCodeFromPointer返回实际目标函数的地址,而不是跳转语句。

2. 用于获取加载的二进制文件信息的 API

DetourEnumerateModules

HMODULE DetourEnumerateModules(
    _In_opt_ HMODULE hModuleLast  //终止模块的句柄,NULL枚举全部
    );

枚举模块

DetourGetEntryPoint

PVOID DetourGetEntryPoint(
    _In_opt_ HMODULE hModule   //模块句柄
    );

返回模块的入口点。

DetourGetModuleSize

ULONG DetourGetModuleSize(
    _In_ HMODULE hModule   //模块句柄
    );

返回模块的加载大小。

DetourEnumerateExports

BOOL DetourEnumerateExports(
    _In_     HMODULE hModule,  //模块的句柄
    _In_opt_ PVOID pContext,   //程序的上下文
    _In_     PF_DETOUR_ENUMERATE_EXPORT_CALLBACK pfExport  //枚举回调函数
    );
 
typedef BOOL (CALLBACK *PF_DETOUR_ENUMERATE_EXPORT_CALLBACK)(
    _In_opt_ PVOID pContext, 
    _In_ ULONG nOrdinal, 
    _In_opt_ LPCSTR pszName, 
    _In_opt_ PVOID pCode);

枚举模块的导出函数。

DetourEnumerateImports

BOOL DetourEnumerateImports(
    _In_opt_ HMODULE hModule,  //模块的句柄
    _In_opt_ PVOID pContext,  //特定于程序的上下文,该上下文将传递给pfImportFile和 pfImportFunc
    _In_opt_ PF_DETOUR_IMPORT_FILE_CALLBACK pfImportFile, //每个模块导入的文件将调用一次回调函数
    _In_opt_ PF_DETOUR_IMPORT_FUNC_CALLBACK pfImportFunc  //每个模块导入的函数将调用一次回调函数
    );
 
typedef BOOL (CALLBACK *PF_DETOUR_IMPORT_FILE_CALLBACK)(
    _In_opt_ PVOID pContext,
    _In_opt_ HMODULE hModule,
    _In_opt_ LPCSTR pszFile);
 
typedef BOOL (CALLBACK *PF_DETOUR_IMPORT_FUNC_CALLBACK)(
    _In_opt_ PVOID pContext, 
    _In_ DWORD nOrdinal, 
    _In_opt_ LPCSTR pszFunc, 
    _In_opt_ PVOID pvFunc);

枚举导入函数

DetourEnumerateImportsEx

BOOL DetourEnumerateImportsEx(
    _In_opt_ HMODULE hModule,   //模块的句柄
    _In_opt_ PVOID pContext,  //上下文将传递给pfImportFile和 pfImportFunc
    _In_opt_ PF_DETOUR_IMPORT_FILE_CALLBACK pfImportFile,  //每个模块导入的文件将调用一次回调函数
    _In_opt_ PF_DETOUR_IMPORT_FUNC_CALLBACK_EX pfImportFunc  //每个模块导入的函数将调用一次回调函数
    );
 
 
typedef BOOL (CALLBACK *PF_DETOUR_IMPORT_FUNC_CALLBACK_EX)(
    _In_opt_ PVOID pContext, 
    _In_ DWORD nOrdinal, 
    _In_opt_ LPCSTR pszFunc, 
    _In_opt_ PVOID* ppvFunc);

枚举从模块导入函数

DetourFindPayload

_Writable_bytes_(*pcbData)
_Readable_bytes_(*pcbData)
_Success_(return != NULL)
PVOID DetourFindPayload(
    _In_opt_ HMODULE hModule,  //模块句柄
    _In_     REFGUID rguid,
    _Out_    DWORD * pcbData
    );

定位二进制文件中映射的payloads

DetourGetContainingModule

HMODULE DetourGetContainingModule(
    _In_ PVOID vpAddr         //函数地址
    );

根据函数地址,查找所在的模块

DetourGetSizeOfPayloads

DWORD DetourGetSizeOfPayloads(
    _In_ HMODULE hModule
    );

获取Payload的大小

3. 修改 PE 文件相关的 API

DetourBinaryOpen:打开一个 PE 二进制文件

DetourBinaryEnumeratePayloads:枚举二进制文件中的 payloads

DetourBinaryFindPayload:查找指定的 payload

DetourBinarySetPayload:设置或者替换一个指定的 payload

DetourBinaryDeletePayload:删除一个指定的 payload

DetourBinaryPurgePayloads:删除二进制中的所有 payloads

DetourBinaryEditImportsModifythePEbinaryimporttable:修改 PE 二进制文件的导入表

DetourBinaryResetImports:复位PE二进制导入地址表

DetourBinaryWrite:把PE映像写入到一个文件中

DetourBinaryClose:关闭PE二进制映像

DetourBinaryBind:使用 BindImage 绑定二进制映像

4. Dll 和区段注入进程相关的 API

DetourCreateProcessWithDllEx

BOOL DetourCreateProcessWithDllEx(
    _In_opt_    LPCTSTR lpApplicationName,  //CreateProcess API定义的应用程序名称
    _Inout_opt_ LPTSTR lpCommandLine,   //CreateProcess API定义的命令行
    _In_opt_    LPSECURITY_ATTRIBUTES lpProcessAttributes, //CreateProcess API定义的流程属性
    _In_opt_    LPSECURITY_ATTRIBUTES lpThreadAttributes, //CreateProcess API定义的线程属性
    _In_        BOOL bInheritHandles,  //CreateProcess API定义的句柄标志
    _In_        DWORD dwCreationFlags,  //CreateProcess API定义的创建标志
    _In_opt_    LPVOID lpEnvironment,    //CreateProcess API定义的流程环境变量
    _In_opt_    LPCTSTR lpCurrentDirectory, //CreateProcess API定义的当前目录
    _In_        LPSTARTUPINFOW lpStartupInfo,  //CreateProcess API定义的启动信息
    _Out_       LPPROCESS_INFORMATION lpProcessInformation,  //CreateProcess API定义的处理句柄信息
    _In_        LPCSTR lpDllName, //要插入到新进程中的DLL的路径名。为了同时支持32位和64位应用程序,如果DLL包含32位代码,则DLL名称应以“ 32”结尾;如果DLL包含64位代码,则DLL名称应以“ 64”结尾。如果目标进程的大小与父进程的大小不同,则Detours会自动在路径名中将“ 32”替换为“ 64”或将“ 64”替换为“ 32”。
    _In_opt_    PDETOUR_CREATE_PROCESS_ROUTINEW pfCreateProcessW //指向程序特定替代CreateProcess API的指针;如果应使用标准CreateProcess API创建新进程,则为NULL。
    );

启动程序并注入DLL

DetourCreateProcessWithDlls

BOOL DetourCreateProcessWithDlls(
    _In_opt_          LPCTSTR lpApplicationName,
    _Inout_opt_       LPTSTR lpCommandLine,
    _In_opt_          LPSECURITY_ATTRIBUTES lpProcessAttributes,
    _In_opt_          LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_              BOOL bInheritHandles,
    _In_              DWORD dwCreationFlags,
    _In_opt_          LPVOID lpEnvironment,
    _In_opt_          LPCTSTR lpCurrentDirectory,
    _In_              LPSTARTUPINFOW lpStartupInfo,
    _Out_             LPPROCESS_INFORMATION lpProcessInformation,
    _In_              DWORD nDlls,
    _In_reads_(nDlls) LPCSTR *rlpDlls,
    _In_opt_          PDETOUR_CREATE_PROCESS_ROUTINEW pfCreateProcessW
    );

启动程序并注入DLL

DetourCopyPayloadToProcess

BOOL DetourCopyPayloadToProcess(
    _In_                     HANDLE hProcess,  //进程句柄
    _In_                     REFGUID rguid,    //区段id
    _In_reads_bytes_(cbData) PVOID pvData,     //指向区段的指针
    _In_                     DWORD cbData      //区段的大小
    );
posted @ 2024-09-11 21:09  lostin9772  阅读(3)  评论(0)    收藏  举报