用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. nodejs+mongoose操作mongodb副本集实例

    继上一篇设置mongodb副本集之后,开始使用nodejs访问mongodb副本集: 1:创建项目     express 项目名称 2:npm install mongoose    安装mongo ...

  2. 使用OLAMISDK实现一个语音输入数字进行24点计算的iOS程序

    前言 在目前的软件应用中,输入方式还是以文字输入方式为主,但是语音输入的方式目前应用的越来越广泛.这是一个利用 Olami SDK 编写的一个24点iOS程序,是通过语音进行输入. Olami SDK ...

  3. Android的快速开发框架 afinal

    afinal 框架学习: http://www.oschina.net/p/afinal

  4. Xamarin Forms 进度条控件

    本文翻译:http://xamlnative.com/2016/04/14/xamarin-forms-a-simple-circular-progress-control/ 里面都是胡说的,如果看不 ...

  5. [bzoj2131]免费的馅饼 树状数组优化dp

    2131: 免费的馅饼 Time Limit: 10 Sec  Memory Limit: 259 MB[Submit][Status][Discuss] Description Input 第一行是 ...

  6. jquery入门知识点总结(转)

    一.jquery的加载方法 $(document).ready(function(){js代码}); $(function(){js代码});(一般使用这个); 注意点1:使用jquery必须先导入函 ...

  7. 使用python实现计算器功能

    学习python过程中的作业.实现了+.-.×./.及幂运算,支持括号优先级. 代码为python3.5 import re def formatEquation(string): string = ...

  8. 压缩感知重构算法之子空间追踪(SP)

    SP的提出时间比CoSaMP提出时间稍晚一些,但和压缩采样匹配追踪(CoSaMP)的方法几乎是一样的.SP与CoSaMP主要区别在于“In each iteration, in the SP algo ...

  9. Java:什么是面向对象?

    1.首先我们去区分对象的属性和方法. 一).什么是对象? 比如:所有的东西都可以叫做对象.而对象就是提供给我们研究对象,这就是对象. 二).怎么区分"属性"和"方法&qu ...

  10. IdentityServer4 实现 OAuth 2.0(密码模式 - HTTP Post 方式)

    之前写了一篇文章:<IdentityServer4 实现 OpenID Connect 和 OAuth 2.0> 上面这篇文章虽然详细,但都是点到为止的介绍,并没有实际应用的示例,所以,后 ...