一个Win32API Trace Tool的设计与实现
用VC编程也有不短的时间了,对kernel32、advapi32、user32、gdi32等动态库里的API多数都已经很熟悉了。API是操作系统提供给应用程序的一组服务,很久以前就想要做个小工具,用来跟踪应用程序对API的调用,对于分析程序的行为、功能的实现原理以及Bug的定位都会有很大的帮助。可是长久以来,都没有付诸实际行动。最近,为了定位一个有趣的Bug,终于动手把这个设想实现出来。
PE文件动态链接的细节原理就是:在代码中调用API时,按__stdcall调用约定传参,然后call Import Table中对应的Entry,Import Table中对应的Entry其实是一个绝对地址。这个才是API的真正地址,是在PE文件被加载时由系统加载器填写的。例如:
;x86 code
LPVOID lpMem = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 8); 001D1001 8B 35 08 20 1D 00 mov esi,dword ptr [__imp__GetProcessHeap@0 (1D2008h)] 001D1007 6A 08 push 8 001D1009 6A 08 push 8 001D100B FF D6 call esi 001D100D 50 push eax 001D100E FF 15 00 20 1D 00 call dword ptr [__imp__HeapAlloc@12 (1D2000h)] __imp__GetProcessHeap@0: 001D2008 B9 14 E1 76 ; kernel32.dll!_GetProcessHeapStub@0 (76E114B9h) __imp__HeapAlloc@12: 001D2000 46 E0 55 77 ; ntdll.dll!_RtlAllocateHeap@12 (7755E046h)
;x64 code LPVOID lpMem = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 8); 000000013F141006 FF 15 04 10 00 00 call qword ptr [__imp_GetProcessHeap (13F142010h)] 000000013F14100C BA 08 00 00 00 mov edx,8 000000013F141011 48 8B C8 mov rcx,rax 000000013F141014 44 8B C2 mov r8d,edx 000000013F141017 FF 15 E3 0F 00 00 call qword ptr [__imp_HeapAlloc (13F142000h)] 000000013F14101D 48 8B D8 mov rbx,rax __imp_GetProcessHeap: 000000013F142010 20 1A 25 77 00 00 00 00 ; kernel32.dll!GetProcessHeapStub (0000000077251A20h) __imp_HeapAlloc: 000000013F142000 60 33 3A 77 00 00 00 00 ; ntdll.dll!RtlAllocateHeap (00000000773A3360h)
Hook Import Table的原理就是把Hook Import Table中指定API的地址替换成我们自己实现的Stub函数的地址,在Stub函数中做一些我们想要的处理和调用原始的API。预期目标是想要通过Hook Import Table来实现对API调用的跟踪,要求能够获得“哪个模块调用了哪个模块中的哪个API”,传递的参数和返回值。
整体设计思路:做一个HookApi.dll,在被加载时从一个XML文件读取要进行Hook的模块和API列表,为每一个要Hook的API生成一个Stub函数,Stub函数的功能是调用原始API并将参数和返回值输出到Log文件,最后将Import Table中的API地址替换为相应的Stub函数地址即完成Hook。
接下来详细说明各部分的设计:
一,XML列表格式
<hook logmax="1024"> <library name="HookTest.exe"> <import dll="kernel32.dll"> <api name="HeapAlloc" args="3" /> <api name="HeapFree" args="3" /> </import> <import dll="user32.dll"> <api name="DestroyWindow" args="1" /> </import> </library> <library name="gdiplus.dll"> <import dll="gdi32.dll"> <api name="Ellipse" args="5" /> <api name="Pie" args="9" /> <api name="Chord" args="9" /> <api name="Arc" args="9" /> </import> </library> </hook>
根节点“hook”的“logmax”属性指定Log文件的Max Size,以MB为单位,取值范围32~32768。“library”为要被Hook Import的调用者模块,“import”则是包含被调用API的模块,“api”的“args”属性指定API的参数个数。
二,Log Function的设计
Log Function被设计用来记录API调用、传递的参数和返回值。在生成的每个API的Stub函数中都会调用Log Function,所以为了尽可能减小对性能的影响,使用FileMapping来将Log写到文件,Log Function接收的参数不包含任何模块、函数名称字符串,而是依赖于XML列表的索引值。x86和x64版本的Log Function的prototype如下:
// x86
VOID WINAPI LogOut32(DWORD dwLibIdx, DWORD dwDllIdx, DWORD dwApiIdx, DWORD dwArgCnt, DWORD dwRetVal, LPVOID lpArgs);
// x64
VOID WINAPI LogOut64(UINT64 uLibIdxDllIdx, UINT64 uApiIdxArgCnt, UINT64 uRetVal, LPVOID lpArgs);
输出的每条Log记录的格式为(伪代码):
struct LogRecord { DWORD dwLibIdx; // 调用者模块索引 DWORD dwDllIdx; // API所在模块索引 DWORD dwApiIdx; // API索引 DWORD dwArgCnt; // 参数个数 PVOID aryArgs[dwArgCnt]; // 参数列表,是否存在取决于参数个数,在x86平台每个参数大小为4字节,x64平台为8字节。 DWORD dwRetVal; // 返回值 };
跟踪完成后,使用另一个工具“Log2Text.exe”来将Log文件转化为txt格式。FileMapping的最大文件大小从XML文件的“logmax”指定,每次映射到内存中的View大小为8MB,当检测到使用过半时向后移动4MB重新映射。使用CriticalSection做多线程同步。
三,Stub Function的汇编代码
这里的Stub Function主要是想要实现一段通用的代码,可以在运行时根据XML中的参数信息为每个要Hook的API动态的生成。经过几次修改后确定下来,思路是:x86平台,将栈上的参数按照原始顺序再次压栈,然后调用真正的API,将原始参数地址、返回值和模块、API索引等信息传递给Log Function,然后返回同时清理栈;x64平台,首先备份传参寄存器,如果栈上还有参数,按原始顺序再次压栈,调用真正API,将栈上备份参数地址、返回值和模块、API索引等信息传递给Log Function,返回。也就是说从传参与栈的角度看,Stub函数等价于一个与API prototype一致的C函数。具体代码如下:
x86 code
;------------------------------------------------
; 参数压栈,如果有的话
mov ecx,0x12345678 ; 参数个数,根据XML动态写入 cmp ecx,0 je l01_02 mov eax,ecx l01_01: push dword [esp + eax * 4] loop l01_01
;------------------------------------------------ ; 调用真正的API l01_02: call 0x12345678 ; API的相对地址,动态写入 push eax ; 备份返回值 ;------------------------------------------------ ; 调用Log Function lea ecx,[esp + 8] push ecx ; 指向参数列表的指针 push eax ; API返回值 push 0x12345678 ; ArgCnt,根据XML动态写入 push 0x12345678 ; ApiIdx,根据XML动态写入 push 0x12345678 ; DllIdx,根据XML动态写入 push 0x12345678 ; LibIdx,根据XML动态写入 call 0x12345678 ; Log Function相对地址,动态写入 ;------------------------------------------------ ; 恢复返回值,返回并清理栈 pop eax ret 0x1234 ; 栈上参数大小,根据XML动态写入
x64 code
;------------------------------------------------
; 备份传参寄存器到栈上的预留空间
push rbp mov rbp,rsp mov [rbp + 40],r9 mov [rbp + 32],r8 mov [rbp + 24],rdx mov [rbp + 16],rcx ;------------------------------------------------ ; 栈上参数压栈,如果有的话 mov rcx,0x12345678 ; 参数个数,根据XML动态写入 cmp rcx,4 jle l01_02 sub rcx,4 l01_01: push qword [rbp + rcx * 8 + 40] loop l01_01 l01_02: mov rcx,[rbp + 16] ;------------------------------------------------ ; 调用真正API sub rsp,20h mov rax,0x123456789abcdef ; API地址,动态写入 call rax mov rsp,rbp push rax ; 备份返回值 ;------------------------------------------------ ; 调用Log Function lea r9,[rbp + 16] ; 指向参数列表的指针 mov r8,rax ; API返回值 mov rdx,0x123456789abcdef ; ApiIdx | (ArgCnt << 32)得到的一个UINT64,根据XML生成并动态写入 mov rcx,0x123456789abcdef ; LibIdx | (DllIdx << 32)得到的一个UINT64,根据XML生成并动态写入 sub rsp,20h mov rax,0x123456789abcdef ; Log Function地址,动态写入 call rax ;------------------------------------------------ ; 恢复返回值并返回 mov rax,[rbp - 8] mov rsp,rbp pop rbp ret
以上汇编代码中类似于“0x12345678”只是用来占位而已,没有实际意义。其中调用Log Function的代码与上节中的prototype相对应。在实行Hook时,有专门的代码为每个Stub分配可执行虚拟内存、填写需要动态写入的值,再将Import Table中对应的Entry指向Stub函数。
这种设计要求XML中指定的参数个数必须精确。在设计Stub函数时,如果不需要跟踪返回值,可以在调用真正API之前,通过Log Function输出参数列表等信息,然后直接jmp到API地址,避免参数再次压栈,提高性能也同时避免了因为XML中错误的参数个数造成的栈破坏。
四,Log2Text转换工具
此工具的作用是根据XML列表,将Log文件转化成可以直接阅读的txt文件,为了优化性能同样使用FileMapping,txt文件最大大小同样取决于“logmax”。输出的txt文本格式如下:
HookTest.exe : kernel32.dll : HeapAlloc ( 0x005c0000, 0x00000008, 0x00000004 ) : 0x00614350 HookTest.exe : kernel32.dll : HeapFree ( 0x005c0000, 0x00000000, 0x00614350 ) : 0x00000001 HookTest.exe : kernel32.dll : HeapAlloc ( 0x005c0000, 0x00000008, 0x00000008 ) : 0x00614350 HookTest.exe : kernel32.dll : HeapFree ( 0x005c0000, 0x00000000, 0x00614350 ) : 0x00000001 HookTest.exe : kernel32.dll : HeapAlloc ( 0x005c0000, 0x00000008, 0x0000000c ) : 0x00615838 HookTest.exe : kernel32.dll : HeapFree ( 0x005c0000, 0x00000000, 0x00615838 ) : 0x00000001 HookTest.exe : kernel32.dll : HeapAlloc ( 0x005c0000, 0x00000008, 0x00000010 ) : 0x00615838 HookTest.exe : kernel32.dll : HeapFree ( 0x005c0000, 0x00000000, 0x00615838 ) : 0x00000001 HookTest.exe : kernel32.dll : HeapAlloc ( 0x005c0000, 0x00000008, 0x00000014 ) : 0x00613768 HookTest.exe : kernel32.dll : HeapFree ( 0x005c0000, 0x00000000, 0x00613768 ) : 0x00000001 HookTest.exe : kernel32.dll : HeapAlloc ( 0x005c0000, 0x00000008, 0x00000018 ) : 0x00613768 HookTest.exe : kernel32.dll : HeapFree ( 0x005c0000, 0x00000000, 0x00613768 ) : 0x00000001
依次是:“调用者模块名称 : 包含API的模块名称 : API名称 ( 参数列表 ) : 返回值”,对于void函数也会取到返回值,就是当时eax/rax的值,没有任何意义。其实也可以把函数的返回地址一起由Log文件输出,可以更精确地跟踪到模块中调用API的代码位置。
此工具目前只实现了Import Table的Hook,基本上也可以按照同样思路Hook Export Table来应对GetProcAddress或者用户自己实现类似函数。inline Hook可算是终极手法,但是想要做成一个普适型工具好像不是很可行。实现这个工具的目的,只是为了辅助我们宏观上大致定位一下我们感兴趣的位置,在下调试断点时可以更明确,更深入的跟踪分析还是需要自己去调试。
上述可执行文件已上传至我的百度云:http://pan.baidu.com/s/1nNB0y,还没有经过太多测试,有兴趣的朋友可以测试一下。感谢阅读。