拦截二进制函数
Detours库可以在运行过程中动态拦截函数调用。detours将目标函数前几个指令替换为一个无条件跳转,跳转到用户定义的detour函数。被拦截的函数保存在trampoline函数中。trampoline保存了目标函数移除的指令和一个无条件跳转,可以跳转到目标函数的执行体部分(未被移除的部分)。
当执行到目标函数的时候,直接跳转到用户提供的detours拦截函数。拦截函数开始执行自己的代码。detour函数可以直接返回或调用trampoline函数,将流程返回到拦截前。当目标函数执行完以后,再将控制交给detour函数。detour函数执行适当的代码返回,下图分别表示没有拦截和拦截以后的执行流程:
detours库通过在目标进程二进制映像中写入指令进行拦截。对于目标函数,detours实际上写入两个函数,目标函数和trampoline函数,以及一个函数指针pointer(怀疑文档中有错误)。trampoline函数由detours动态分配。拦截之前,trampoline只包含一条跳转到目标函数的语句。拦截以后,trampoline包含目标函数的初始几条语句和跳转到目标函数剩余内容的跳转指令。
目标指针最初被初始化为指向目标函数。用detour依附(attach)到目标函数以后,目标指针就被修改为指向trampoline函数。当detour从目标函数分离(detach)以后,目标指针像开始一样指向目标函数。
上图展示了detours拦截过程。为了拦截目标函数,首先为动态trampoline函数分配内存(如果没有静态的trampoline),然后修改目标函数和trampoline为可写。拦截的第一步,detours从目标函数中复制至少5字节指令到trampoline(足够一条无条件跳转指令)。如果目标函数小于5字节,detours退出并返回一个错误码
复制指令的过程中,detours采用了一种简单的表格驱动反汇编器。detours在trampoline最后添加一条跳转命令,跳转到目标函数第一条没有被复制的指令处。detours在目标函数的第一条指令处写入一条无条件跳转指令,跳转到detour函数中。最后,detours将目标函数和trampoline函数恢复为原始状态,然后通过调用借口FlushInstructionCache刷新cpu指令缓存。
使用Detours
为了detour目标函数,需要一个指向目标函数的指针和一个detour函数。为了能够正确拦截目标函数,detour函数和目标指针的调用规则需要一致,包括参数和调用规则。调用规则一致确保寄存器能正确保存,detour和目标函数的栈能够合理分配。
下面的代码描述了detours库的使用方法,用户必须包含头文件detours.h,连接过程包含库detours.lib
- #include <windows.h>
- #include <detours.h>
- static LONG dwSlept = 0;
- // Target pointer for the uninstrumented Sleep API.
- //
- static VOID (WINAPI * TrueSleep)(DWORD dwMilliseconds) = Sleep;
- // Detour function that replaces the Sleep API.
- //
- VOID WINAPI TimedSleep(DWORD dwMilliseconds)
- {
- // Save the before and after times around calling the Sleep API.
- DWORD dwBeg = GetTickCount();
- TrueSleep(dwMilliseconds);
- DWORD dwEnd = GetTickCount();
- InterlockedExchangeAdd(&dwSlept, dwEnd - dwBeg);
- }
- // DllMain function attaches and detaches the TimedSleep detour to the
- // Sleep target function. The Sleep target function is referred to
- // through the TrueSleep target pointer.
- //
- BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved)
- {
- if (DetourIsHelperProcess()) {
- return TRUE;
- }
- if (dwReason == DLL_PROCESS_ATTACH) {
- DetourRestoreAfterWith();
- DetourTransactionBegin();
- DetourUpdateThread(GetCurrentThread());
- DetourAttach(&(PVOID&)TrueSleep, TimedSleep);
- DetourTransactionCommit();
- }
- else if (dwReason == DLL_PROCESS_DETACH) {
- DetourTransactionBegin();
- DetourUpdateThread(GetCurrentThread());
- DetourDetach(&(PVOID&)TrueSleep, TimedSleep);
- DetourTransactionCommit();
- }
- return TRUE;
- }
拦截目标函数通过与detour交互,调用DetourAttach实现。与detour的交互通过调用DetourTransactionBegin和DetourTransactionCommit实现。DetourAttach包含2个参数:目标函数指针的地址和detour函数地址。目标函数不能直接作为参数传入,因为需要传入一个目标指针。
交互过程中DetourUpdateThread更新所有线程,确保所有拦截点都能正确更新。
DetourAttach为调用目标函数分配并准备好一个trampoline。detour运行以后,目标函数和trampoline会被重写,目标指针被更新,指向trampoline函数。
一旦目标函数被detour,目标函数的调用会被转到detour函数。目标函数通过trampoline被执行的时候,detour函数将参数复制过来,因为现在目标函数变成了detour函数的一个子程序。
要想移除拦截,可以调用detoursDetach。与DetourAttach类似,DetourDetach包含两个参数:目标函数指针地址和detour函数指针。函数执行以后,目标函数被重写为其原来的状态。trampoline函数被删除,目标函数指针恢复为原来的目标函数。
如果需要拦截的程序没有源代码,detour函数需要打包为一个dll。将dll加载到一个新的进程中,进程启动的时候调用DetourCreateProcessWithDllEx加载dll。如果通过调用DetourCreateProcessWithDllEx插入dll,DllMain函数必须调用DetourRestoreAfterWith函数。如果希望dll能够在32位和64为机器上使用,DllMain必须调用DetourIsHelperProcess函数。Dll必须导出DetourFinishHelperProcess函数.
注意:微软不保证或支持任何被修改的微软或第三方的代码,不管是采用detour修改还是其它方式修改。
detours自带例子withdll采用DetourCreateProcesswithDllEx函数启动一个新进程载入一个命名dll
Payloads和DLL导入编辑
除了连接和分离detours函数,detours包还提供了注入其他数据段的API,如二进制文件,或修改dll导入表,叫做payloads。Detours中的二进制修改API是完全可逆的,Detours在二进制中保存了恢复信息,使得所有的修改都可以还原。
windowsPE格式的二进制文件结构如上图所示。PE格式的windows二进制文件时COFF格式(通用文件格式)的一个扩展。windows二进制文件包含一个DOS兼容的头,PE头,包含代码的text段,包含初始化数据的data段,包含导入的dll和函数的导入表,包含代码导出函数的导出表,调试信息。二进制文件必须包含两个头,其它的段都是可选的。
为了修改windows二进制文件,Detours创建了一个.detours段,在导出表和调试符号中间,如上图所示。windows二进制文件必须把调试符号放在最后。新的段包含一个detours头,和原始PE头的一份拷贝,如果修改导入表,detours创建一个新的导入表,将其放在复制的PE头后面,然后修改原始的PE头,将其指向新的导入表。最后,Detours在.detours段的最后写入用户的payloads,然后加上调试信息完成文件。修改后的windows二进制文件可以很容易恢复:从.detours段中恢复PE头,移除.detours段。上图表示的是Detours修改过的windows二进制文件格式。
创建一个导入表有2个目的,首先是保持原始的导入表不变,以便以后恢复文件,其次,新建的导入表包含重命名的导入DLL和函数,或者是整个dll和函数。例如,detours保的例子中setdll.exe这个程序,讲一个用户dll插入到目标二进制程序中。由于新的导入表在应用程序中第一个位置,所以用户的dll在程序中会首先被执行。
Detours提供了很多有用接口,包括编辑导入表(DetourBinaryEditImports),添加payloads(DetourBinarySetPayload),枚举payloads(DetourBinaryEnumeratePayloads),移除payloads(DetourBinaryPurgePayloads)。DetourEnumerateModules可以枚举一个地址空间中的二进制文件,DetourFindPayload可以定位二进制文件中映射的payloads。每一个payload通过一个128位的guid表示。payloads可以将程序配置数据注入到二进制程序中。
DetourCopyPayloadToProcess可以直接将Payloads复制到目标进程
Detour 32位和64位进程
注意:只有专业版的Detours支持64位程序。非商业版,express版本只支持32位x86系统
detours最常见的使用情况是在不修改原始二进制程序的情况下的改变函数执行情况。这时,用户提供的detour函数打包到一个dll中,在程序运行开始的时候调用DetourCreateProcesswithDll加载该dll。父进程调用DetourCreateProcesswithDll,通过插入一个导入表来改变程序内存中的拷贝。新的插入表使得系统在程序开始的时候,程序逻辑代码还没有执行的时候加载dll。这样deoturDLL就可以hook目标进程中的目标函数。
在64位的处理器上,windows支持32位或64位的应用程序。为了支持32位或64位程序,需要创建32位和64位的detourDLL。调用DetourCreateProcessWithDll的地方需要改为DetourCreateProcessWithDllEx。DetourCreateProcessWithDllEx会根据系统选择合适的Dll来注入目标程序。
需要这样做:
为了在一个系统上支持32位和64位程序,需要创建2个DLL。一个包含32位代码,另一个包含64位代码。两个DLL放在同一个目录中并且起相同的名字,并加上相应的后缀“32”和“64”,比如foo32.dll和foo64.dll。
调用DetourCreateProcessWithDllEx来启动一个包含相应DLL的进程。另外,你的DLL需要:
1.导出DetourFinishHelperProcess
2.在DllMain中调用DetourIsHelperProcess,如果DetourIsHelperProcess返回TRUE,则马上返回TRUE。
3.通过调用DetourCreateProcessWithDllEx而不是DetourCreateProcessWithDll来创建新的进程。
工作原理
如果目标进程跟dll的父进程同为32位或同为64位,DetourCreateProcessWithDllEx运行过程类似DetourCreateProcessWithDll。
如果父进程与目标进程不同,一个是32位,一个是64位,DetourCreateProcessWithDllEx会创建一个辅助进程,将DLL加载到rundll32.exe进程,然后调用DetourFinishHelperProcess。这个API通过使用正确的32位或64位代码来修补导入表。
应用示例
下面测试辅助进程,首先在32位环境中编译Detours例子,然后在64位环境中编译64位例子。然后进入例子中\tryman目录,在64位环境中输入"nmake size64",这样可以递归运行进程,包括32位和64位进程。
附注
关于rundll32.exe,请参考http://support.microsoft.com/kb/164787
Related APIs:
DetourCreateProcessWithDllEx, DetourFinishHelperProcess,DetourIsHelperProcess,DetourRestoreAfterWith.
Related Samples
Simple, Simple, Slept, Traceapi, Tracebld, Tracelnk, Tracemem,Tracereg,Traceser,Tracetcp,Tryman.