一个Win32API Trace Tool的设计与实现

时间:2022-10-19 06:38:21

用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, );
001D1001 8B 1D mov esi,dword ptr [__imp__GetProcessHeap@0 (1D2008h)]
001D1007 6A push
001D1009 6A push
001D100B FF D6 call esi
001D100D push eax
001D100E FF 1D call dword ptr [__imp__HeapAlloc@12 (1D2000h)] __imp__GetProcessHeap@0:
001D2008 B9 E1 76 ; kernel32.dll!_GetProcessHeapStub@0 (76E114B9h) __imp__HeapAlloc@12:
001D2000 E0 77 ; ntdll.dll!_RtlAllocateHeap@12 (7755E046h)

;x64 code
LPVOID lpMem = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, );
000000013F141006 FF call qword ptr [__imp_GetProcessHeap (13F142010h)]
000000013F14100C BA mov edx,
000000013F141011 8B C8 mov rcx,rax
000000013F141014 8B C2 mov r8d,edx
000000013F141017 FF E3 0F call qword ptr [__imp_HeapAlloc (13F142000h)]
000000013F14101D 8B D8 mov rbx,rax __imp_GetProcessHeap:
000000013F142010 1A 00 ; kernel32.dll!GetProcessHeapStub (0000000077251A20h) __imp_HeapAlloc:
000000013F142000 3A 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,
je l01_02
mov eax,ecx
l01_01: push dword [esp + eax * ]
loop l01_01
;------------------------------------------------
; 调用真正的API l01_02: call 0x12345678 ; API的相对地址,动态写入
push eax ; 备份返回值 ;------------------------------------------------
; 调用Log Function lea ecx,[esp + ]
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 + ],r9
mov [rbp + ],r8
mov [rbp + ],rdx
mov [rbp + ],rcx ;------------------------------------------------
; 栈上参数压栈,如果有的话 mov rcx,0x12345678 ; 参数个数,根据XML动态写入
cmp rcx,
jle l01_02
sub rcx,
l01_01: push qword [rbp + rcx * + ]
loop l01_01 l01_02: mov rcx,[rbp + ] ;------------------------------------------------
; 调用真正API sub rsp,20h
mov rax,0x123456789abcdef ; API地址,动态写入
call rax mov rsp,rbp
push rax ; 备份返回值 ;------------------------------------------------
; 调用Log Function lea r9,[rbp + ] ; 指向参数列表的指针
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 - ] 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,还没有经过太多测试,有兴趣的朋友可以测试一下。感谢阅读。