用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,还没有经过太多测试,有兴趣的朋友可以测试一下。感谢阅读。

一个Win32API Trace Tool的设计与实现的更多相关文章

  1. Java基础-继承-编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight。小车类Car是Vehicle的子类,其中包含的属性有载人数 loader。卡车类Truck是Car类的子类,其中包含的属性有载重量payload。每个 类都有构造方法和输出相关数据的方法。最后,写一个测试类来测试这些类的功 能。

    #29.编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight.小车类Car是Vehicle的子类,其中包含的属性有载人数 loader.卡车类T ...

  2. Axure RP一个专业的快速原型设计工具

    Axure RP是一个专业的快速原型设计工具.Axure(发音:Ack-sure),代表美国Axure公司:RP则是Rapid Prototyping(快速原型)的缩写. Axure简要介绍 Axur ...

  3. 框架源码系列四:手写Spring-配置(为什么要提供配置的方法、选择什么样的配置方式、配置方式的工作过程是怎样的、分步骤一个一个的去分析和设计)

    一.为什么要提供配置的方法 经过前面的手写Spring IOC.手写Spring DI.手写Spring AOP,我们知道要创建一个bean对象,需要用户先定义好bean,然后注册到bean工厂才能创 ...

  4. Axure RP一个专业的高速原型设计工具

    Axure RP是一个专业的高速原型设计工具.Axure(发音:Ack-sure),代表美国Axure公司.RP则是Rapid Prototyping(高速原型)的缩写. Axure简要介绍 Axur ...

  5. Skywalking-02:如何写一个Skywalking trace插件

    如何写一个Skywalking trace插件 javaagent 原理 美团技术团队-Java 动态调试技术原理及实践 类图 实现 ConsumeMessageConcurrentlyInstrum ...

  6. K8 系统中省市县数据表的设计可以反映出什么? 通过一个基础业务表的设计品味软件系统的整体架构

    1:没有严谨的Id思想,不变化的Id思想,看不见的Id的思想. 2:数据不严谨,没有上下级关系,没有树形结构,ParentId 的思想. 3:表之间的关系都是弱关联,基础数据一修改业务数据就容易乱套. ...

  7. 资产信息之收集资产代码流程,API的一个认证,数据库表的设计

    收集资产代码流程 1.起初我们些的代码是面条式的一堆的逻辑判断.   后来通过了不断的优化升级实现了一个3种方案都支持的CMDB系统,我们用哪种方案只需要在配置文件里修改一下设置就行了.   同时我们 ...

  8. ios开发:一个音乐播放器的设计与实现

    github地址:https://github.com/wzpziyi1/MusicPlauer 这个Demo,关于歌曲播放的主要功能都实现了的.下一曲.上一曲,暂停,根据歌曲的播放进度动态滚动歌词, ...

  9. 【大型web架构】一个大型web系统架构设计和技术选型的讨论摘录

    1.数据库压力问题 所有的压力最终都会反映到数据库方面,一定要对数据库有一个整体的规划. 可以按照业务.区域等等特性对数据库进行配置,可以考虑分库.使用rac.分区.分表等等策略,确保数据库能正常的进 ...

随机推荐

  1. angular 4 实现的tab栏切换

    管理系统 tab 切换页,是一种常见的需求,大概如下: 点击左边菜单,右边显示相应的选项卡,然后不同的选项卡面可以同时编辑,切换时信息不掉失! 用php或.net,java的开发技术,大概是切换显示, ...

  2. 如何让vim像IDE一样一键放大缩小字号?

    原创,转载请注明出处 在其他IDE中,比如codeblocks,按住ctrl,然后滑动鼠标滚轮就可以实现字体的放大缩小. 在强大的vim中code怎么能缺少这种功能?! 在vim插件库中查询一番,发现 ...

  3. mysql5.7在windows不能启动的方法及查看数据库大小命令

    1.将mysql目录下的my-default.ini改为my.ini 2.cmd进入mysql的bin目录下 3.执行mysqld --initialize进行初始化(如果mysql目录下已经存在da ...

  4. LINUX 笔记-DU 和 DF

    du(disk usage)搜索文件统计文件大小 1.显示指定文件所占空间 du file1 file2 2.显示指定目录占的空间 du dir 3.只显示总和大小 du -s 4.以方便格式显示 d ...

  5. linux命令行下svn常用命令

    linux命令行下svn常用命令 1. 将文件checkout到本地目录 1 #path是服务器上的目录 2 svn checkout path 3 4 #示例 5 svn checkout svn: ...

  6. LeetCode 495. Teemo Attacking (提莫攻击)

    In LOL world, there is a hero called Teemo and his attacking can make his enemy Ashe be in poisoned ...

  7. LeetCode 461. Hamming Distance (汉明距离)

    The Hamming distance between two integers is the number of positions at which the corresponding bits ...

  8. Vue源码后记-更多options参数(1)

    我是这样计划的,写完这个还写一篇数据变动时,VNode是如何更新的,顺便初探一下diff算法. 至于vue-router.vuex等插件源码,容我缓一波好吧,vue看的有点伤. 其实在之前讲其余内置指 ...

  9. CSS3新增伪类汇总

    :root 选择文档的根元素,等同于 html 元素 :empty 选择没有子元素的元素 :target 选取当前活动的目标元素 :not(selector) 选择除 selector 元素意外的元素 ...

  10. app.get is not a function解决方案

    在express4.x中app.js被申明为一个模块,而不是一个主程序入口,在文件的最后暴露出了这个模块,如下所示 app.js module.exports = app; 但是我们在routes目录 ...