3 如何使用Detours
Figure 5中的代码片断描述了如何使用Detours库。要使用Detours必须包含detours.h并将detours.lib链接到工程中。
Figure 5. 一个截获函数的例子。
trampoline函数可以动态或者静态的创建。要使用静态的trampoline函数来截获目标函数,应用程序生成trampoline的时候必须使用DETOUR_TRAMPOLINE宏。DETOUR_TRAMPOLINE有两个输入参数:trampoline的原型和目标函数的名字。
注意,对于正确的截获模型,包括目标函数,trampoline函数,以及截获函数都必须是完全一致的调用形式,包括参数格式和调用约定。当通过trampoline函数调用目标函数的时候拷贝正确参数是截获函数的责任。由于目标函数仅仅是截获函数的一个可调用分支,这种责任几乎就是一种下意识的行为。
使用相同的调用约定可以确保寄存器中的值被正确的保存,并且保证调用堆栈在截获函数调用目标函数的时候能正确的建立和销毁。
可以使用DetourFunctionWithTrampoline函数来截获目标函数。这个函数有两个参数:trampoline函数以及截获函数的指针。因为目标函数已经被加到trampoline函数中,所有不需要在参数中特别指定。
我们可以使用DetourFunction函数来创建一个动态的trampoline函数,它包括两个参数:一个指向目标函数的指针和一个截获函数的指针。DetourFunction分配一个新的trampoline函数并将适当的截获代码插入到目标函数中去。
如果目标函数本身是一个链接符号,使用静态的trampoline函数将非常简单。如果目标函数不能在链接时可见,那么可以使用动态trampoline函数。通常可以使用其他函数获得目标函数的指针。这个时候,当目标函数不是很容易使用的时候,DetourFindFunction函数可以找到那个函数,不管它时DLL中导出的函数,或者是可以通过二进制目标函数的调试符号找到。
DetourFindFunction接受两个参数:库的名字和函数的名字。如果DetourFindFunction函数找到了指定的函数,返回该函数的指针,否则将返回一个NULL指针。DetourFindFunction会首先使用Win32函数LoadLibrary 和GetProcAddress来定位函数,如果函数没有在DLL的导出表中找到,DetourFindFunction将使用ImageHlp库来搜索有效的调试符号(译注:这里的调试符号是指Windows本身提供的调试符号,需要单独安装,具体信息请参考Windows的用户诊断支持信息)。DetourFindFunction返回的函数指针可以用来传递给DetourFunction以生成一个动态的trampoline函数。
我们可以调用DetourRemoveTrampoline来去掉对一个目标函数的截获。
注意,因为Detours中的函数会修改应用程序的地址空间,请确保当加入截获函数或者去掉截获函数的时候没有其他线程在进程空间中执行,这是程序员的责任。一个简单的方法保证这个时候是单线程执行就是在加载Detours库的时候在DllMain中呼叫函数。
4 评价
存在一些其他的技术可以截获函数调用,这些技术包括:
通过源代码在应用程序中替换被呼叫的函数:通过修改应用程序的源代码,将对目标函数的调用替换成对截获函数的调用。这种方法的主要弊端就在于它需要源代码。
在应用程序的二进制文件中替换被呼叫的函数:通过修改应用程序的二进制文件将对目标函数的调用替换成对截获函数的调用。虽然这种技术不需要源代码,但是这种方法需要标识出可以使用的调用地点,这需要二进制文件中有可用的符号信息,而通常的应用程序并不会提供这种信息。
DLL重定向:如果目标函数驻留在一个动态库中,可以通过修改二进制文件的导入表将调用重定向到一个截获用的DLL。重定向过程可以是在应用程序加载以前替换导入表中原始的DLL,或者是在加载以后在间接导入跳转表中替换函数地址[2]。不幸的是,在应用程序中通过导入表来重定向到截获函数的方法对于那些DLL的内部函数调用以及那些使用LoadLibrary和GetProcAddress加载的函数指针毫无作用。
断点陷阱:不同于替换DLL,目标函数可以通过插入一个调试断点的方法被捕获。
截获函数可以被调试中断句柄调用。这种技术的主要弊端在于,断点陷阱会将应用程序所有的线程挂起。另外,调试中断必须在另外一个操作系统进程中捕获。经由断点陷阱进行的捕获,在执行时对效率有很大的牺牲。
Table 1 列出了捕获一个空函数和一个CoCreateInstance 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实例函数的调用,例如:CoCreateInstance 函数。截获函数通过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接口,例如IWin32FileHandle,来访问操作系统提供的功能。由于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:SetUnhandledExceptionFilter,在应用程序不存在其他异常过滤句柄的情况下可以通过这个API来为应用程序指定一个异常过滤器。对于SDSM这样的应用程序来说,程序员总是希望想插入第一次机会异常过滤器,这样可以将由于SDSM对虚拟内存(VM)页权限的操作造成的页错误去掉。Windows NT没有提供例如第一次机会异常过滤的机制。一个简单的截获可以将异常入口点从内核模式转换到用户模式(KiUserExceptionDispatcher)。只用了很少的几行代码,截获函数调用一个由用户提供的第一次机会异常过滤器并处理这个异常,如果异常没有被处理,缺省的异常处理会通过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平台上进行全新一代的系统研究成为可能。
参考材料
[1] Aral, Ziya, Illya Gertner, and Greg Schaffer. Efficient Debugging Primitives for Multiprocessors. Proceedings of the Third International Conference on Architectural Support for Programming Languages and Operating Systems, pp. 87-95. Boston, MA, April 1989.
[2] Balzer, Robert and Neil Goldman. Mediating Connectors. Proceedings of the 19th IEEE International Conference on Distributed Computing Systems Workshop, pp. 73-77. Austin, TX, June 1999.
[3] Digital Equipment Corporation. DDT Reference Manual, 1972.
[4] Evans, Thomas G. and D. Lucille Darley. DEBUG - An Extension to Current Online Debugging Techniques. Communications of the ACM, 8(5), pp. 321-326, May 1965.
[5] Gill, S. The Diagnosis of Mistakes in Programmes on the EDSAC. Proceedings of the Royal Society, Series A, 206, pp. 538-554, May 1951.
[6] Hollingsworth, Jeffrey K. and Bryan Buck. DyninstAPI Programmer's Guide, Release 1.2. Computer Science Department, University of Maryland, College Park, MD, September 1998.
[7] Hunt, Galen C. and Michael L. Scott. The Coign Automatic Distributed Partitioning System. Proceedings of the Third Symposium on Operating System Design and Implementation (OSDI '99), pp. 187-200. New Orleans, LA, February 1999. USENIX.
[8] Hunt, Galen C. and Michael L. Scott. Intercepting and Instrumenting COM Applications. Proceedings of the Fifth Conference on Object-Oriented Technologies and Systems (COOTS'99), pp. 45-56. San Diego, CA, May 1999. USENIX.
[9] Kessler, Peter. Fast Breakpoints: Design and Implementation. Proceedings of the ACM SIGPLAN '90 Conference on Programming Language Design and Implementation, pp. 78-84. White Plains, NY, June 1990.
[10] Larus, James R. and Eric Schnarr. EEL: Machine-Independent Executable Editing. Proceedings of the ACM SIGPLAN Conference on Programming Language Design and Implementation, pp. 291-300. La Jolla, CA, June 1995.
[11] Li, Li, Alessandro Forin, Galen Hunt, and Yi-Min Wang. High-Performance Distributed Objects over a System Area Network. Proceedings of the Third USENIX NT Symposium. Seattle, WA, July 1999.
[12] Romer, Ted, Geoff Voelker, Dennis Lee, Alec Wolman, Wayne Wong, Hank Levy, Brian Bershad, and J. Bradley Chen. Instrumentation and Optimization of Win32/Intel Executables Using Etch. Proceedings of the USENIX Windows NT Workshop 1997, pp. 1-7. Seattle, WA, August 1997. USENIX.
[13] Srivastava, Amitabh and Alan Eustace. ATOM: A System for Building Customized Program Analysis Tools. Proceedings of the SIGPLAN '94 Conference on Programming Language Design and Implementation, pp. 196-205. Orlando, FL, June 1994.
[14] Stets, Robert J., Galen C. Hunt, and Michael L. Scott. Component-based Operating System APIs: A Versioning and Distributed Resource Solution. IEEE Computer, 32(7), July 1999.
[15] Stockham, T.G. and J.B. Dennis. FLIT- Flexowriter Interrogation Tape: A Symbolic Utility Program for the TX-0. Department of Electical Engineering, MIT, Cambridge, MA, Memo 5001-23, July 1960.
[16] Tamches, Ariel and Barton P. Miller. Fine-Grained Dynamic Instrumentation of Commodity Operating System Kernels. Proceedings of the Third Symposium on Operating Systems Design and Implementation (OSDI '99), pp. 117-130. New Orleans, LA, February 1999. USENIX.
[17] Zhang, Xiaolan, Zheng Wang, Nicholas Gloy, J. Bradley Chen, and Michael D. Smith. System Support for Automated Profiling and Optimization. Proceedings of the Sixteenth ACM Symposium on Operating System Principles. Saint-Malo, France, October 1997.