Detour

Detours: 在二进制代码上截获Win32函数调用

Galen Hunt and Doug Brubacher

Microsoft Research

One Microsoft Way

Redmond, WA  98052

detours@microsoft.com

http://research.microsoft.com/sn/detours

注: 这篇论文首次发表是授权给USENIX。作者保留著作权。本文允许出于非商业性目的拷贝,例如教育和研究目的。第一次发表在Proceedings of the 3rd USENIX Windows NT Symposium. Seattle, WA, July 1999。

 

摘录

具 有创意的系统级检测研究的关键,在于使得截获函数调用变得更简单以及用来扩展已经存在的操作系统和应用程序的功能。通过得到源代码,我们可以轻而易举的通 过重建(Rebuilding)操作系统或者应用程序的方法在它们中间插入新的功能或者做功能扩展。然而,在今天这个商业化的开发世界里,以及在只有二进 制代码发布的系统中,研究人员几乎没有可能可以得到程序的源代码。

我们开发的Detours 是一个在x86平台上截获任意Win32函数调用的工具库。Detours通过重写目标函数的映像来达到插入到Win32 函数中执行的目的。Detours开发包中同样保留了描述如何附着到任意的Win32二进制文件的DLLs和data节表(被称为一种有效负荷, “payloads”)的文档。

虽然以前的开发人员曾经使用重写二进制代码的方法将调试和性能测试的代码加入到应用程序中,但是据我们所 知,Detours是第一个在任意平台(译注:指Windows平台)都提供了可以将目标函数做为一个截获函数的子过程来调用的开发包。我们独特的 trampoline设计是扩展已存在的二进制软件的关键。

我们将介绍我们使用Detours来生成一个自动化的分布式系统的经验,这个系统被用来分析DCOM协议栈,并且被用来为基于COM的OS API生成一个thunking层。它从一个微观的基准上证明了Detours库的有效性。

1 介绍
具 有创意的系统级检测研究的关键,在于使截获函数更简单可行以及扩展已经存在的操作系统和应用程序的功能,不论这个函数存在于一个应用程序,一个库,或者一 个系统的动态链接库中。我们截获函数执行最直接的原因就是为函数增添功能,修改返回值,或者为调试以及性能测试加入附加的代码。通过访问源代码,我们可以 轻而易举的使用重建(Rebuilding)操作系统或者应用程序的方法在它们中间插入新的功能或者做功能扩展。然而,在今天这个商业化的开发世界里,以 及在只有二进制代码发布的系统中,研究人员几乎没有机会可以得到源代码。

Detours是一个在x86平台上截获任意Win32函数调用的 工具库。中断代码可以在运行时动态加载。Detours使用一个无条件转移指令来替换目标函数的最初几条指令,将控制流转移到一个用户提供的截获函数。而 目标函数中的一些指令被保存在一个被称为“trampoline” (译注:英文意为蹦床,杂技)的函数中,这些指令包括目标函数中被替换的代码以及一个转移到目标函数的无条件分支。而截获函数可以替换目标函数,或者通过 执行“trampoline”的时候将目标函数作为子程序来调用的办法来扩展功能。

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

如果其他人为了调试或者在内部使用其他系统检测手段而试图修改二进制代码, Detours将是一个可以普遍使用的开发包。据我们所知,Detours是第一个可以在任意平台上将未修改的目标代码作为一个可以通过 “trampoline”调用的子程序来保留的开发包。而以前的系统在逻辑上预先将截获代码放到目标代码中,而不是将原始的目标代码做为一个普通的子程序 来调用。我们独特的“trampoline”设计对于扩展现有的软件的二进制代码是至关重要的。
出于使用基本的函数截获功能的目的, Detours同样提供了编辑任何DLL导入表的功能,达到向存在的二进制代码中添加任意数据节表的目的,向一个新进程或者一个已经运行着的进程中注入一 个DLL。一旦向一个进程注入了DLL,这个动态库就可以截获任何Win32函数,不论它是在应用程序中或者在系统库中。

在下一节里我们将 讲述Detours是如何工作的。第3节概述了如何使用Detours库,第4节描述了截获函数使用的一般技术以及如何通过一个微观标准来衡量 Detours。第5节详细描述了如果使用Detours从本地应用程序产生分布式应用程序,用来量化DCOM的花费,为一个新的基于COM的 Win32API建立一个thunking层,并且实现捕获第一次机会异常。我们会在第6节将Detours和其他人的相关工作做一个比较并且在第7节作 出总结。

2 截获
Detours提供了三种很重要的功能:在x86机器上任意中断Win32二进制函数的执行的能力,编辑二进制文件导入表的能力,以及向二进制文件中附着任意数据节表的能力。

我们会描述每一种截获功能。

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

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


Figure 1.  被截获和未被截获的函数调用。

Detours 库通过重写目标函数在进程中的二进制映像达到截获目标函数的目的。对每一个目标函数而言,Detours实际上重写了两个函数:目标函数和与之相匹配的 trampoline函数。trampoline函数可以静态或者动态的创建。一个静态创建的trampoline函数总是不需要截获就可以调用目标函 数。在之前的用于截获的插入中,静态trampoline函数保存了到目标函数的一个简单跳转。这个调整插入以后,trampoline函数保存了目标函 数的初始化指令,以及到目标函数的跳转指令。对于进行截获调用的程序员而言是极度有用的,例如,在Coign[7]中,调用 Coign_­Co­Create­Instance就相当于没有通过截获直接调用原始的Co­Create­Instance函数一样。Coign的内 部函数可以在任何时候通过调用Coign_­Co­Create­Instance来生成一个组件对象而不需要考虑是否原始函数由于被截获而改变了执行流 程。


Figure 2.  trampoline和目标函数,在截获代码插入前后(从左到右)。

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

2.2 有效负荷和DLL导入表的编辑
虽 然现在有大量的现成工具可以编辑二进制文件[[10, 12, 13, 17],不过大多数系统研究并不需要用这些笨拙的工具对二进制文件进行大量访问和修改。取而代之,通常需要为应用程序和系统的二进制文件添加一个额外的 DLL或者数据节表。对截获函数而言,Detours库提供了被称为有效负荷(payloads)的功能,它可以对Win32二进制文件附加任意数据节表 的可逆支持(译注:可以添加,并卸栽)以及编辑DLL导入表。

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


Figure 3.  Win32 PE 可执行文件的结构。

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

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

生成一个新的导入表有两个目 的。第一,它保留了原始的导入表,这样万一程序员想恢复到修改前的状况就不会出现问题。第二,新的导入表可以保存被更名的导入DLL和函数或者全新的 DLL和函数。例如,Coign [7]使用Detours来为每一个要被截获的程序插入一个coignrte.dll动态库的初始化入口。做为应用程序导入表中的第一个入口,这样 coignrte.dll总是第一个在应用程序地址空间中运行的动态库(译注:这是指动态库加载的时候运行DllMain函数)。


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

Detours 提供了编辑导入表,添加有效负荷,枚举有效负荷,删除有效负荷,再绑定动态库的函数。Detours同时还提供了枚举映射到地址空间中的二进制文件以及枚 举这些二进制文件映射到地址空间中的有效负荷的能力。每一个有效负荷被用一个全局唯一标识符(GUID)标识出来。Coign使用Detours将每个应 用程序的配置信息附着到应用程序的二进制代码中。

一旦有任何截获行为需要在不修改二进制文件的情况下被插入到应用程序中,Detours提 供了函数来将DLL注入到一个新的或者是已经存在的进程。为了注入一个DLL,Detours使用AllocEx和WriteProcessMemory 这些API在目标进程中写入一个Load­Library的调用代码,并使用Create­Remote­Thread来进行这个调用(译注:指使用一个 新线程来调用写入的代码,包括Load­Library,在DLL的加载过程中, DllMain函数得以执行)。
 
 
3 如何使用Detours
Figure 5中的代码片断描述了如何使用Detours库。要使用Detours必须包含detours.h并将detours.lib链接到工程中。


Figure 5.  一个截获函数的例子。

trampoline 函数可以动态或者静态的创建。要使用静态的trampoline函数来截获目标函数,应用程序生成trampoline的时候必须使用 DETOUR­_TRAMPOLINE宏。DETOUR­_TRAMPOLINE有两个输入参数:trampoline的原型和目标函数的名字。

注 意,对于正确的截获模型,包括目标函数,trampoline函数,以及截获函数都必须是完全一致的调用形式,包括参数格式和调用约定。当通过 trampoline函数调用目标函数的时候拷贝正确参数是截获函数的责任。由于目标函数仅仅是截获函数的一个可调用分支,这种责任几乎就是一种下意识的 行为。

使用相同的调用约定可以确保寄存器中的值被正确的保存,并且保证调用堆栈在截获函数调用目标函数的时候能正确的建立和销毁。

可以使用Detour­Function­With­Trampoline函数来截获目标函数。这个函数有两个参数:trampoline函数以及截获函数的指针。因为目标函数已经被加到trampoline函数中,所有不需要在参数中特别指定。

我们可以使用DetourFunction函数来创建一个动态的trampoline函数,它包括两个参数:一个指向目标函数的指针和一个截获函数的指针。DetourFunction分配一个新的trampoline函数并将适当的截获代码插入到目标函数中去。

如 果目标函数本身是一个链接符号,使用静态的trampoline函数将非常简单。如果目标函数不能在链接时可见,那么可以使用动态trampoline函 数。通常可以使用其他函数获得目标函数的指针。这个时候,当目标函数不是很容易使用的时候,Detour­Find­Function函数可以找到那个函 数,不管它时DLL中导出的函数,或者是可以通过二进制目标函数的调试符号找到。

Detour­Find­Function接受两个参数: 库的名字和函数的名字。如果Detour­Find­Function函数找到了指定的函数,返回该函数的指针,否则将返回一个NULL指针。 Detour­Find­Function会首先使用Win32函数LoadLibrary 和GetProcAddress来定位函数,如果函数没有在DLL的导出表中找到,Detour­Find­Function将使用ImageHlp库来 搜索有效的调试符号(译注:这里的调试符号是指Windows本身提供的调试符号,需要单独安装,具体信息请参考Windows的用户诊断支持信息)。 Detour­Find­Function返回的函数指针可以用来传递给Detour­­Function以生成一个动态的trampoline函数。

我们可以调用Detour­Remove­Trampoline来去掉对一个目标函数的截获。

注意,因为Detours中的函数会修改应用程序的地址空间,请确保当加入截获函数或者去掉截获函数的时候没有其他线程在进程空间中执行,这是程序员的责任。一个简单的方法保证这个时候是单线程执行就是在加载Detours库的时候在DllMain中呼叫函数。

4 评价
存在一些其他的技术可以截获函数调用,这些技术包括:

通过源代码在应用程序中替换被呼叫的函数:通过修改应用程序的源代码,将对目标函数的调用替换成对截获函数的调用。这种方法的主要弊端就在于它需要源代码。

在应用程序的二进制文件中替换被呼叫的函数:通过修改应用程序的二进制文件将对目标函数的调用替换成对截获函数的调用。虽然这种技术不需要源代码,但是这种方法需要标识出可以使用的调用地点,这需要二进制文件中有可用的符号信息,而通常的应用程序并不会提供这种信息。

DLL 重定向:如果目标函数驻留在一个动态库中,可以通过修改二进制文件的导入表将调用重定向到一个截获用的DLL。重定向过程可以是在应用程序加载以前替换导 入表中原始的DLL,或者是在加载以后在间接导入跳转表中替换函数地址[2]。不幸的是,在应用程序中通过导入表来重定向到截获函数的方法对于那些DLL 的内部函数调用以及那些使用LoadLibrary和GetProcAddress加载的函数指针毫无作用。

断点陷阱:不同于替换DLL,目标函数可以通过插入一个调试断点的方法被捕获。

截获函数可以被调试中断句柄调用。这种技术的主要弊端在于,断点陷阱会将应用程序所有的线程挂起。另外,调试中断必须在另外一个操作系统进程中捕获。经由断点陷阱进行的捕获,在执行时对效率有很大的牺牲。

Table 1 列出了捕获一个空函数和一个Co­Create­Instance API所花费的时间。这个小测试是在一台主频200MHz的Pentium Pro机器上执行的。分别列出了没有使用截获花费的时间,使用调用替换,使用DLL替换,使用Detours库,或者使用断点陷阱所花费的时间。你可以看 到,使用Detours库仅仅比其他一些方法多一点点时间(比最快的方法不超过400纳秒)。


Table 1.  捕获技术在时间花费上的对比。

5 经验
在过去的两年里,Detours库被广泛的用于Win32应用程序和Windows NT操作系统的研究和功能扩展。

Detours 本来是为Coign Automatic Distributed Partition System [7]所开发的。Coign将本地桌面应用程序从COM组件转换为分布式客户端/服务器应用程序。在进行系统检测分析的时候,Coign使用 Detours来截获对COM实例函数的调用,例如:Co­Create­Instance 函数。截获函数通过trampoline函数来调用原始的库函数,然后在一个附加的检测输出层封装一个输出接口指针(请参考[8])。这个检测输出层决定 应用程序组件怎样通过网络来执行。这样,通过分布式执行,一个新的Coign截获函数会截获到COM实例函数的调用并利用分布式机制将这些调用重新分配调 用路径。本质上来说,Coign扩展了COM库并支持灵活的远程调用。

尽管DCOM对一些COM实例函数支持远程调用,Coign通过迂回 扩展(即截获行为)支持对大约50个COM函数进行远程调用。Coign使用Detours的DLL重定向函数将一个运行时加载器附着到应用程序的二进制 码上,同时使用负载函数(payload)将一个系统统计数据节表也附着到应用程序的二进制码上。

我们的一些同事也使用Detours来检 测DCOM协议栈的用户模式部分,包括Marshaling proxies,DCOM运行库,RPC运行库,WinSock运行库,以及Marshaling stubs [11]。对结果的分析被用于对DCOM的结构进行重新构建,以生成到一个速度更快的用户模式网络。并且他们可以使用源代码来产生一个特殊版本的DCOM 来进行系统检测分析,在进行系统检测分析的计算机上,这个基于源代码的检测可以做到版本独立并且为所有的DCOM应用程序所共享。通过基于Detours 的二进制检测方法,系统分析工具可以附着到任意的Windows NT 4版本的DCOM并且只影响被检测的进程。

在另一次进行功能扩展的 试验中,Detours被用于为COP(基于组件的操作系统代理服务器[14])生成一个thunking层。COP是一个基于COM的Win32 API版本。使用COP的应用程序通过COM接口,例如IWin32­File­Handle,来访问操作系统提供的功能。由于COP接口是由DCOM发 布的,一个COP应用程序可以通过网络上的计算机来使用操作系统资源,包括文件系统,鼠标,键盘,显示器,注册表,等等。为了给子程序提供支持,COP使 用截获函数来捕获所有到Win32 API的调用。本地应用程序的API调用被转换为到COP接口的调用。在底层,COP使用trampoline函数来检测同下面操作系统的通讯。COP不 需要对应用程序的二进制代码做任何的修改。在加载时,COP 的DLL被Detours的注入函数注入到应用程序的地址空间。通过Detours的简单截获,使得这种对Win32 API的笨重扩展变得更简单了。

最 后,为了支持软件分布式内存(SDSM)系统,我们为Win32结构化异常句柄构造了一个第一次机会异常(first-chance exception)过滤器。在Wiin32 API中包括了一个API:Set­Unhandled­Exception­Filter,在应用程序不存在其他异常过滤句柄的情况下可以通过这个 API来为应用程序指定一个异常过滤器。对于SDSM这样的应用程序来说,程序员总是希望想插入第一次机会异常过滤器,这样可以将由于SDSM对虚拟内存 (VM)页权限的操作造成的页错误去掉。Windows NT没有提供例如第一次机会异常过滤的机制。一个简单的截获可以将异常入口点从内核模式转换到用户模式 (Ki­User­Exception­Dispatcher)。只用了很少的几行代码,截获函数调用一个由用户提供的第一次机会异常过滤器并处理这个异 常,如果异常没有被处理,缺省的异常处理会通过trampoline函数进行。

6 相关的工作
Detours可以对普通的代码补丁 技术进行扩展。为了捕获执行过程,一个无条件分支或者跳转被插入到被捕获的目标函数的某一点。被这些跳转指令所覆盖的目标函数种的代码被移到了代码补丁 中。代码补丁中包括了我们插入的检测代码或者是对检测代码的一个呼叫,这些代码都是紧跟着被转移到无条件分支中的目标函数的代码以及一个到目标函数未被修 改部分的第一条指令的跳转。逻辑上来说,一个代码补丁可以被设计为放到函数的开始处,插入到函数中的任意一点,或者是附加到函数的尾部。

尽 管代码补丁会通过一定的机制来继续执行目标代码,但我们的技术将控制完全交给了截获函数,后者可以在它可能的时候通过trampoline函数来调用原来 的目标函数。trampoline函数可以让系统检测的行为完全自由的进行,因为通过使用相同的调用约定,原来的目标函数已经做为一个可调用的子程序可以 在任何时候调用。

代码补丁的技术在数字计算机变得为人所知的时候就已经存在了[3-5, 9, 15]。代码补丁被用于插入调试信息和检测代码。在遥远的过去,代码补丁一般被认为应该是一种更实用的升级方法,而不是将整个应用程序重新编译一遍。另 外,对于调试和检测而言,Detours也用来灵巧的扩展现有系统的功能[7, 14]。

虽然最近的系统对平行的应用程序[1]和系统内核 [16]扩展了代码补丁的方法,但是据我们所知,Detours是唯一的一个可以将目标函数做为可调用子程序的补丁系统。截获函数替换了目标函数,但可以 在任何合适的地方通过trampoline函数调用目标函数。我们独一无二的trampoline设计对已经存在的二进制代码的功能扩展变得轻而易举。

最 近的研究产生了一类对二进制代码进行重写工具,包括Atom[13] ,Etch[12],EEL[10],以及Morph[17]。一般来说,这些工具将应用程序的二进制代码和一个检测用的脚本做为输入。检测脚本传递了一 些在二进制上需要插入代码的指令,基本阻塞,或者函数。而输出的是一个新的用于检测研究用的二进制代码。在更早的一些系统上,DyninstAPI[6] 可以动态的修改应用程序。

Detours同这些二进制重写工具相比,最大的好处就是它的大小。Detours对检测包添加的代码不会超过 18KB,而那些重写工具最少也要添加上百KB。Detours加入的尺寸非常小,其代价就是它无法在指令和基本阻塞间加入代码。而重写工具可以通过一些 特殊的特性,例如自由寄存器探索(free register discovery)在任意的指令间插入检测指令。Detours依赖于调用约定以保存寄存器的值。而重写工具支持在基本的指令单元前后插入代码,他们不 支持将未被改写的目标函数做为子程序来调用。

7 结论
Detours库为系统的研究者们提供了一整套导入工具的军火库。 Detour函数是快速,灵活,友好的。一个对CoCreateInstance的截获,对速度的影响不会超过3%。同使用断点陷阱相比,它在速度上的优 势是数量级的。Detours的库很小。编译后的运行库不超过40KB,虽然对用户的检测程序来说,附加的代码不会超过18KB。

不同于DLL重定向,Detour库同时支持捕获静态和动态绑定的函数调用。最后,Detour库同DLL重定向,以及直接修改应用程序代码相比有很大的灵活性。在每个进程的执行时,对任何函数的截获都是可选的。

我 们独一无二的trampoline设计保留了原有的语意,将目标函数的未改写部分作为一个子程序提供给截获函数调用。使用截获函数和trampoline 函数,能够很容易产生令人注目的系统扩展而不需要得到源代码的支持以及不需要重新重新编译二进制文件。Detours使得在Windows NT平台上进行全新一代的系统研究成为可能。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值