Detour开发包介绍(1):概述

微软研究院Detours开发包是一个用来在二进制级别上对程序中的函数(Function)或者过程(Procedure)进行修改的工具库。一般我们将这种技术称为Hook。在现实中,这种技术可以应用在很多场景下。比如截获某些Windows API,在实际调用到系统函数前进行一些过滤工作;软件中使用到了一些没有源代码的第三方库,但是又想增强其中某些函数的功能;修改函数返回值;为调试以及性能测试加入附加的代码;或者截获函数的输入输出作研究;破解使用。等等。Detours是在Windows的二进制PE文件基础上进行API截获。对于Linux平台,作这件事情将会非常的简单,由于最初的操作系统设计者引入了LD_PRELOAD。如果你设置LD_PRELOAD=mylib.so,那么应用程序在载入系统动态链接库时,会先查看mylib.so的符号表,在重定位(relocation)的时候会优先使用mylib.so里的symbol。假如你在mylib.so里有个printf() ,那么这个printf就会替代libc的printf。而在mylib.so里的这个printf可以直接访问libc.so里的printf函数指针来获得真正的printf的入口地址。这样,所有的API拦截在loader加载系统库时就已经完成,非常自然,和平台相关的部分全部交给loader去处理。

Detours的下载地址为http://research.microsoft.com/en-us/projects/detours/,最新为2.1版。Detours Express 2.1可免费使用(用于非商业目的),只支持32位的x86代码。Detours Professional 2.1需要购买版权,它提供的API接口其实与Detours Express是一样的,只不过它还可以支持x64和IA64处理器,以此为基础编写的代码拥有更强的可移植性。安装完Detours Express后,包括Detours库和使用示例的源代码,需要Visual C++环境来进行编译。打开Visual Studio的命令行提示符界面,切换到Detours Expressr的安装目录,用"nmake"命令即可完成构建。编译完后,我们要用到时的有四个文件,有四个文件,分别是detoured.dll、detoured.lib、detours.lib、detours.h。

1、截获二进制函数

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

(1) 进程要执行的指令也放在虚存空间中。

(2) 可以使用QueryProtectEx函数把存放指令的页面的权限更改为可读可写可执行,再改写其内容,从而修改正在运行的程序。

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

Detours定义了三个概念:

(1) Target函数:要拦截的函数,通常为Windows的API。

(2) Trampoline函数:Target函数的部分复制品。因为Detours将会改写Target函数,所以先把Target函数的前5个字节复制保存好,一方面仍然保存Target函数的过程调用语义,另一方面便于以后的恢复。

(3) Detour函数:用来替代Target函数的截获函数。

Detours库使得截获函数调用更容易,截获代码是运行时动态加载的。Detours使用一个无条件转移指令来替换目标函数的最初几条指令,将控制流转移到一个用户提供的截获函数。而目标函数中的一些指令被保存在一个被称为“trampoline”的函数中,这些指令包括目标函数中被替换的代码以及一个转移到目标函数的无条件分支。

当程序执行到达目标函数的时候,会直接跳转到一个用户支持的截获函数。截获函数来执行适当的预处理。截获函数可以直接返回到原来的函数,或者它可以调用“trampoline”函数,后者可以按照截获以前的方式来调用目标函数。当目标函数执行完以后,它将控制返回到截获函数。而截获函数将执行恰当的收尾工作并将控制返回到源函数调用处。下图显示了被截获和未被截获的调用在逻辑上的控制流。

 

Detour开发包介绍(1):概述_第1张图片

图1 有Detours和没有Detours的调用控制流对比

Detours库通过重写目标函数在进程中的二进制映像达到截获目标函数的目的。对每一个目标函数而言,Detours实际上重写了两个函数:目标函数和与之相匹配的trampoline函数。trampoline函数可以静态或者动态的创建。一个静态创建的trampoline函数总是不需要截获就可以调用目标函数。在之前的用于截获的插入中,静态trampoline函数保存了到目标函数的一个简单跳转。这个调整插入以后,trampoline函数保存了目标函数的初始化指令,以及到目标函数的跳转指令。

Detours在Target函数的开头加入JMP Address_of_ Detour_ Function指令(共5个字节)把对Target函数的调用引导到自己的Detour函数, 把Target函数的开头的5个字节加上JMP Address_of_ Target _ Function+5共10个字节作为Trampoline函数。如下图显示了截获过程的插入前后。要截获一个目标函数,Detours首先为动态trampoline函数分配内存(如果没有提供静态的trampoline函数),然后会让目标和trampoline函数可写。在开始了第一条指令之后,Detours会从目标函数拷贝至少五个字节的指令到trampoline 函数(五个字节足够放下一条无条件转移指令)。如果目标函数少于5个字节,Detours会终止执行并返回一个错误码。为了拷贝指令,Detours使用一个简单的表驱动的反汇编引擎。Detours会在trampoline函数的执行尾部添加一条跳转指令,这样执行完trampoline函数后,程序会跳转到目标函数没有拷贝的剩余部分继续执行。Detours会在截获函数中写入一条无条件跳转指令作为到目标函数的第一条指令。最后,Detours将保存目标函数和trampoline函数的原始的页面权限,并使用Flush­Instruction­Cache函数将CPU的指令缓冲区清空。

Detour开发包介绍(1):概述_第2张图片

 

图2 目标函数和跳板函数代码(左边没有插入detour,右边插入了detour)

 

目标函数:函数体(二进制)至少有5个字节以上。按照微软的说明文档Trampoline函数的函数体是拷贝前5个字节加一个无条件跳转指令的话(如果没有特殊处理不可分割指令的话),那么前5个字节必须是完整指令,也就是不能第5个字节和第6个字节是一条不可分割的指令,否则会造成Trampoline 函数执行错误,一条完整的指令被硬性分割开来,造成程序崩溃。对于第5字节和第6个字节是不可分割指令需要调整拷贝到杂技函数(Trampoline)的字节个数,这个值可以查看目标函数的汇编代码得到。此函数是目标函数的修改版本,不能在Detour函数中直接调用,需要通过对Trampoline函数的调用来达到间接调用。

Trampoline函数:此函数默认分配了32个字节,函数的内容就是拷贝的目标函数的前5个字节,加上一个JMP Address_of_ Target _ Function+5指令,共10个字节。此函数仅供您的Detour函数调用,执行完前5个字节的指令后再绝对跳转到目标函数的第6个字节继续执行原功能函数。

Detour函数:此函数是用户需要的截获API的一个模拟版本,调用方式,参数个数必须和目标函数相一致。如目标函数是__stdcall,则Detour函数声明也必须是__stdcall,参数个数和类型也必须相同,否则会造成程序崩溃。此函数在程序调用目标函数的第一条指令的时候就会被调用(无条件跳转过来的),如果在此函数中想继续调用目标函数,必须调用Trampoline函数(Trampoline 函数在执行完目标函数的前5个字节的指令后会无条件跳转到目标函数的5个字节后继续执行),不能再直接调用目标函数,否则将进入无穷递归(目标函数跳转到 Detour函数,Detour函数又跳转到目标函数的递归,因为目标函数在内存中的前5个字节已经被修改成绝对跳转)。通过对Trampoline函数的调用后可以获取目标函数的执行结果,此特性对分析目标函数非常有用,而且可以将目标函数的输出结果进行修改后再传回给应用程序。

这是一种执行时注入的方式,Detours是执行时被插入的。即修改的是内存中的目标函数代码,而不是在硬盘上的DLL文件,因而可以在一个很好的粒度上使得截获二进制函数的执行变得更容易。例如,一个应用程序执行时加载的DLL中的函数过程可以被插入一段截获代码(detoured),与此同时,这个DLL还可以被其他应用程序按正常情况执行(也就是按照不被截获的方式执行,因为DLL二进制文件没有被修改,所以发生截获时不会影响其他进程空间加载这个DLL)。不同于DLL的重新链接或者静态重定向,Detours库中使用的这种中断技术确保不会影响到应用程序中的方法或者系统代码对目标函数的定位。

如果其他人为了调试或者在内部使用其他系统检测手段而试图修改二进制代码,Detours将是一个可以普遍使用的开发包。据我所知,Detours是第一个可以在任意平台上将未修改的目标代码作为一个可以通过“trampoline”调用的子程序来保留的开发包。而以前的系统在逻辑上预先将截获代码放到目标代码中,而不是将原始的目标代码作为一个普通的子程序来调用。独特的“trampoline”设计对于扩展现有的软件的二进制代码是至关重要的。

出于使用基本的函数截获功能的目的,Detours同样提供了DLL注入的方式,它可编辑任何DLL或EXE导入表的功能,达到向存在的二进制代码中添加任意数据节表的目的,向一个新进程或者一个已经运行着的进程中注入一个DLL。一旦向一个进程注入了DLL,这个动态库就可以截获任何Win32函数,不论它是在应用程序中或者在系统库中。

2、有效负荷和DLL导入表的编辑

Detours库提供了被称为有效负荷(payloads)的功能,它可以对Win32二进制文件附加任意数据节表的可逆支持(译注:可以添加,并卸栽)以及编辑DLL导入表。

下图显示了Win32的PE二进制文件的基本结构。PE格式的Win32二进制文件是COFF(普通对象文件格式)的一种扩展。一个Win32二进制文件包括一个对DOS兼容的文件头,一个PE头,一个包含了程序代码的text节表,一个数据节表保存了初始化数据,一个列出导入的DLL和函数的导入表,一个列出导出函数代码的导出表,以及调试符号。除了两个文件头以外,文件的每个节表都是可选的,二进制文件可以不包含它们。

Detour开发包介绍(1):概述_第3张图片Detour开发包介绍(1):概述_第4张图片

 

图3 Win32 PE可执行文件的结构

为了修改一个Win32二进制文件,Detours在导出节表和调试符号之间生成了一个新的.detours节表。注意调试符号必须永远处于Win32二进制文件的最后面。这个新节表保存了一个截获文件头的记录和原始的PE头,如果修改了导入表,Detours会生成一个新的导入表,并将它附着到拷贝的PE头上,然后修改原始的PE头,让它的内部指向新的导入表。

最后,Detours会将一些其他信息写到.detours节表的最后并将调试信息附加到文件的最后面。Detours可以将二进制文件恢复到被它修改以前的状况,因为它可以恢复在.detours节表中保存的原始的PE文件头,并删除.detours节表。下图显示了一个被Detours修改过的Win32二进制文件的格式。

Detour开发包介绍(1):概述_第5张图片

 

4 一个被Detours修改过的二进制文件的格式

生成一个新的导入表有两个目的。第一,它保留了原始的导入表,这样万一程序员想恢复到修改前的状况就不会出现问题。第二,新的导入表可以包含被更名的导入DLL和函数或者全新的DLL和函数。例如Detours包中的setdll.exe示例程序可以把一个用户Dll的初始化入口表插入到目标应用程序中,以作为目标应用程序导入表中的第一个入口,这样在应用程序地址空间中第一个运行的动态库总是这个用户Dll(这是指动态库加载时运行DllMain函数)。

Detours提供了API用来编辑导入表(DetourBinaryEditImprots),添加有效负荷(DetourBinarySetPayload),枚举有效负荷(DetourBinaryEnumeratePayloads),删除有效负荷(DetourBinaryPurgePayloads),再绑定动态库的函数。Detours同时还提供了API用来枚举映射到地址空间中的二进制文件(DetourEnumerateModules),以及定位这些被映射的二进制文件中的有效负荷(DetourFindPayload)。每一个有效负荷被用一个128位的全局唯一标识符(GUID)标识出来。有效负荷可以用来将每个应用程序的配置信息关联到应用程序的二进制代码中。有效负荷还能直接被拷贝到目标进程中(DetourCopyPayloadToProcess)。

    一旦有任何截获行为需要在不修改二进制文件的情况下被插入到应用程序中,Detours提供了函数来将DLL注入到一个新的或者是已经存在的进程。为了注入一个DLL,Detours使用AllocEx和WriteProcessMemory这些API在目标进程中写入一个LoadLibrary的调用代码,并使用CreateRemoteThread来进行这个调用(指使用一个新线程来调用写入的代码,包括LoadLibrary,在DLL的加载过程中,DllMain函数得以执行)。

你可能感兴趣的:(Detour开发包介绍(1):概述)