Detours: Binary Interception of Win32 Functions
Detours: 在二进制代码上截获Win32函数调用
Galen Hunt and Doug Brubacher
Microsoft Research
One Microsoft Way
Redmond, WA 98052
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_CoCreateInstance就相当于没有通过截获直接调用原始的CoCreateInstance函数一样。Coign的内部函数可以在任何时候通过调用Coign_CoCreateInstance来生成一个组件对象而不需要考虑是否原始函数由于被截获而改变了执行流程。
Figure 2. trampoline和目标函数,在截获代码插入前后(从左到右)。
Figure 2 显示了截获过程的插入前后。要截获一个目标函数,Detours首先为动态trampoline函数分配内存(如果没有提供静态的trampoline函数),然后会让目标和trampoline函数可写。在开始了第一条指令之后,Detours会从目标函数拷贝至少五个字节的指令到trampoline函数(五个字节足够放下一条无条件转移指令)。如果目标函数少于5个字节,Detours会终止执行并返回一个错误码。为了拷贝指令,Detours使用一个简单的表驱动的反汇编引擎。Detours会在trampoline函数的执行尾部添加一条跳转指令,这样执行完trampoline函数后,程序会跳转到目标函数没有拷贝的剩余部分继续执行。Detours会在截获函数中写入一条无条件跳转指令作为到目标函数的第一条指令。最后,Detours将保存目标函数和trampoline函数的原始的页面权限,并使用FlushInstructionCache函数将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在目标进程中写入一个LoadLibrary的调用代码,并使用CreateRemoteThread来进行这个调用(译注:指使用一个新线程来调用写入的代码,包括LoadLibrary,在DLL的加载过程中, DllMain函数得以执行)。