本系列文章预计分4篇介绍下hutrace以及hzytrace工具,前3篇主要介绍下hutrace基本功能以及不同应用场景使用示例,hutrace工具本身是基于drltrace开源项目开发,支持Windows+Linux平台,最后1篇会介绍hutrace的同款工具hzytrace,hzytrace是基于Pin开发完成hutrace大部分功能,但侧重点略有区别,虽然hzytrace也可以支持windows,但Windows上相应需求基本可全部由hutrace达成,且效率更高,故目前暂先完善了Linux平台下的hzytrace。利用hutrace、hzytrace在很多分析场景下甚至可以不需调试就可以完成分析目的,因为Linux上找到样例偏少,本篇基本功能介绍部分将主要以Windows为例介绍其功能,实际两个平台上功能基本一致(dynamorio本身是支持arm linux、arm android的、pin也支持macos,但我自己测试移植到对应平台还有些问题,需求比较小众,对应平台也有一些替代工具,暂也没来及继续深入研究)。
简介
hutrace工具主要基于drltrace项目开发,原项目相当于一个利用dynamorio实现的特别简易的apimonitor,而dynamorio本身机制为动态插桩,自带N种反调试技术的免疫效果,后面又根据工作过程中的各类实际需求断断续续的对hutrace进行功能完善,目前主要支持的功能列表如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15C:\Users\huhu\Desktop\bin64>hutrace.exe
ERROR: Usage error:
Usage:
-logdir //生成的trace日志存储位置,默认为hutrace程序目录
-only_from_app //只记录主程序执行情况
-trace_to_librarys //除主程序外指定记录特定模块,与only_from_app结合使用
-record_start_addr //指定记录开始地址
-record_end_addr //指定记录结束地址
-follow_children //追踪子进程
-print_ret_addr //打印API函数调用返回地址
-print_ins_info //打印所有执行基本块的汇编代码
-print_ins_reg //打印所有执行指令的代码以及寄存器、内存引用数据
-print_ins_all //不推荐,print_ins_reg增强版,解析上述寄存器等数据的引用数据并打印,打印的数据更丰富,但是数据量太大,一般需求不大。。
-print_syscall //追踪系统调用
//省略部分无关紧要的功能参数
当然功能不止如此,原项目的功能较为简单,返回地址都不能记录,而hutrace工具中除上述列表中功能外我后续添加了分线程记录、基本块转移记录、复杂参数及返回值等的打印、以及Sub函数参数打印、Linux Fork类型子进程追踪、简易补丁及利用插件完成任意地址Hook功能等等…
本篇以notepad、calc以及一些demo程序为例对hutrace的主要功能进行演示,后续文章将结合一些实际的漏洞以及样本案例演示hutrace应用于逆向分析实战。
API记录
目前很多ApiMonitor仅仅是记录调用的APi名称,能够记录API调用前后参数信息的就比较少了,当然Rohitab Batra的API Monitor工具都可以做到上述功能,功能也比较丰富,也可以做到参数结构体的解析,但是目前仅支持Windows平台,下面介绍下hutrace的API记录功能特点。
常规参数类型
hutrace工具集成了不同类型参数的输出功能,并依托hutrace.config配置文件进行设置对应API函数的参数属性,例如对WriteFile函数的描述:
1 | bool|WriteFile|HANDLE|char*|DWORD|__out DWORD*|__inout OVERLAPPED* |
以notepad为例记录记事本程序的文件写入操作,在记事本程序中输入测试字符串并保存到文件,使用hutrace程序运行并记录程序日志的命令格式如下:
1 | C:\Users\huhu\Desktop\bin64>hutrace.exe -only_from_app -print_ret_addr -- notepad 111.txt |
在当前目录会根据notepad程序运行情况生成文件名形如“huhu.notepad.exe.01896.0000.log”的文件,其中“01896”为进程PID,“0000”为线程序号,下面截取一部分trace的日志结果进行说明:
1 | ~~1464~~ KERNEL32.dll!WideCharToMultiByte N:0xa09 |
在记录的trace日志结果里,“1464”代表线程id,“N:0xa09”代表记录的API函数序号(为了区分多线程记录的日志里API函数的调用顺序)。
Hex类型参数打印
常规的参数类型打印并不能满足我们对十六进制数据的记录,下面以nc为例演示下十六进制数据参数类型的3种记录方式,首先查看第一种指定特定参数为十六进制输出数据大小的方式:
1
2
3
4
5
6
7
8
9hutrace.config文件中的配置:
int|send|DWORD|hex^ARG2|int|int
int|recv|DWORD|__out hex^ARG2|int|int
运行服务端监听:
C:\Users\huhu\Desktop\bin64>nc64.exe -lvp 1234
使用hutrace记录客户端连接:
C:\Users\huhu\Desktop\bin64>hutrace.exe -only_from_app -print_ret_addr -- nc64.exe 127.0.0.1 1234建连成功后分别在客户端和服务端分别输入测试字符串“111…”、“222…”,在生成的trace日志文件中得到对应的记录:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24~~2164~~ WS2_32.dll!send N:0x7cb
arg 0: 0x174 (type=DWORD, size=0x4)
arg 1: 0x555fd0
000000: 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 1111111111111111
000010: 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 1111111111111111
000020: 31 31 31 31 31 31 31 31 31 31 0a 1111111111.
(type=hex^ARG2, size=0x2b)
arg 2: 0x2b (type=int, size=0x4)
arg 3: 0x0 (type=int, size=0x4)
and return to module id:0, retraddr: 0x405197,offset:0x5197
and send return value: 0x2b
......
~~2164~~ WSOCK32.dll!recv N:0x1045
arg 0: 0x174 (type=DWORD, size=0x4)
arg 2: 0x2000 (type=int, size=0x4)
arg 3: 0x0 (type=int, size=0x4)
and return to module id:0, retraddr: 0x404f08,offset:0x4f08
and recv return value: 0x2b
arg 1: 0x0000000000557fe0
000000: 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 2222222222222222
000010: 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 2222222222222222
000020: 32 32 32 32 32 32 32 32 32 32 0a 00 00 00 00 00 2222222222......
000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
//缓冲区0x2000字节大小,省略部分显示。上述演示中recv函数设置按“ARG2”参数(以0为开始索引,ARG2代表recv函数第三个参数)的大小进行输出,实际接受数据可能没有这么多,下面介绍第二种以返回值方式打印:
1
2hutrace.config文件中的配置:
int|recv|DWORD|__out hex^RET|int|int对应的日志记录:
1
2
3
4
5
6
7
8
9
10~~1836~~ WSOCK32.dll!recv N:0xa5f
arg 0: 0x174 (type=DWORD, size=0x4)
arg 2: 0x2000 (type=int, size=0x4)
arg 3: 0x0 (type=int, size=0x4)
and return to module id:0, retraddr: 0x404f08,offset:0x4f08
and recv return value: 0x1d
arg 1: 0x0000000000277fe0
000000: 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 2222222222222222
000010: 32 32 32 32 32 32 32 32 32 32 32 32 0a 222222222222.
(type=__out hex^RET*, size=0x1d)在输出时hutrace对返回值进行了判断如果返回值太大则不进行输出避免影响trace进行。
第三种以固定长度方式记录:
1
2hutrace.config文件中的配置:
int|recv|DWORD|__out hex^NUM20|int|int对应的日志记录:
1
2
3
4
5
6
7
8
9
10~~1396~~ WSOCK32.dll!recv N:0x8f2
arg 0: 0x178 (type=DWORD, size=0x4)
arg 2: 0x2000 (type=int, size=0x4)
arg 3: 0x0 (type=int, size=0x4)
and return to module id:0, retraddr: 0x404f08,offset:0x4f08
and recv return value: 0x25
arg 1: 0x0000000000587fe0
000000: 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 2222222222222222
000010: 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 2222222222222222
(type=__out hex^NUM20*, size=0x20)
结构体参数打印
最后是最为麻烦的参数为结构体类型时,以WSASend、WSARecv为例:
1 | hutrace.config文件中的配置: |
这里解释一下,首先是struct_ptr{DWORD.DWORD.hex^A0},可以查看下WSASend、WSARecv函数参数的定义,第二个参数是个WSABUF结构体类型,正常结构体中第2个成员代表发送数据缓冲区,结构体第1个成员代表缓冲区数据的长度:
1 | typedef struct __WSABUF { |
而这个测试的64位程序,该结构体实际上对齐成四字节的len+四字节填充+8字节buf指针,所以在配置文件语法上将第1个成员拆开成了两个DWORD类型,buf指针指向的数据长度为第1个DWORD成员,也即是{DWORD.DWORD.hex^A0},结构体内的“hex^A0”是为了和常规参数中“^ARG2”区分,在结构体中使用“^A0”标识代表结构体内第1个成员。
后面更坑的是WSASend、WSARecv函数第3个参数代表第2个参数也即是WSABUF结构体的数量,所以在struct_ptr{…}后的“^ARG2”指示打印第3个参数的值对应的结构体数据,这也是和常规参数打印兼容的地方。注意如果是32位程序需要把参数中的第二个DWORD去掉哦~对结构体类型参数的处理过程中需要特别注意。
最终得到的trace日志结果:
1 | 客户端日志 |
记录的日志显示还不够美观,有空可能再改改优化下,WSARecv接收到的数据就不支持按返回值大小打印了,不过结构体中可以使用“^N20”这种固定数值的打印,和常规格式打印的“^NUM20”相区分。
额外说明下样例里为了体现多个WSABUF结构体数据的打印,在客户端运行参数中指定的发送的第1段数据“111…”之后,程序中又追加了一个数据内容为“222…”的结构体合并发送用来测试,不然不好构造出这样的参数数据,不过服务端接收时还是一起接收的,如果拿测试程序测试时请自动忽略服务端报的一些小错,客户端发送完数据就关闭了,服务端还会把接受的数据发回来会调用WSASend报个错,懒得改了,仅为测试复杂结构体数据的打印。
- 注意:在使用结构体或HEX类型时必须指定^NUM或者^ARG确定结构体的数目。
执行流trace
基本块汇编代码输出
默认不使用执行流trace功能时hutrace工具本身也会记录程序的基本块执行情况,分别为“I”间接call、“R”ret返回、“C”直接Call、“J”跳转指令四类。
1 | I40dacd|405170 |
下面在hutrace的运行参数上添加“-print_ins_info”参数运行记录calc进程代码执行流程:
1 | C:\Users\huhu\Desktop\bin64>hutrace.exe -only_from_app -print_ret_addr -print_ins_info -- calc |
得到如下所示的部分记录结果:
1 | 10001daad: |
这种记录方式并非是以记录所执行的每条指令为单位,而是以基本块为单位插桩记录,打印执行的每一个基本块内的所有汇编指令,优点在于已经执行过的基本块如果执行频次较多,hutrace不会重复记录其汇编代码,能够极大避免日志记录以及dynamorio插桩过程造成的效率降低影响(对于非自修改的代码也可以不使用print_ins_info参数,打印出基本块转移转移指令之后再使用IDA脚本或者其它工具将对应的汇编代码添加到日志中,对于申请内存释放代码并执行的情况也可以DUMP内存然后解析反汇编代码到日志中)。
在分析程序时使用print_ins_info功能不仅可以根据API信息了解到程序的基本功能,同时也能对一些代码执行细节进行预览及回溯,例如在分析某样本时通过查看日志可以直接定位到其调用退出进程函数前执行了检测虚拟机环境的代码,这也是常规API监视工具所做不到的。
特定Sub函数的trace
为了满足一些特殊的需求,想对程序中特定的函数参数进行记录,注意这里所说的sub函数可以简单理解成ida识别的function起始地址,函数调用约定必须是标准的cdecl或者stdcall传参,当然64位程序正常的fastcall寄存器传参顺序不影响参数的获取,以一个最简单的64位弹窗程序为例:
测试步骤如下:
1 | hutrace.config添加以下内容: |
得到的部分日志结果:
1 | ~~2024~~ test.exe!0x14000102A N:0x1 |
特定DLL、SO的trace
在分析时我们也经常遇到漏洞或者样本等将主要功能逻辑放到库中加载的情况,在hutrace中trace程序运行时可以同时指定需要额外记录执行流程的DLL或So名称:
1 | C:\Users\huhu\Desktop\bin64>hutrace.exe -only_from_app -print_ret_addr -print_ins_info -trace_to_librarys dll_test.dll -- rundll32 dll_test_x64.dll,#1 |
在使用时同时注意,windows上hutrace获取的模块名是根据其导出信息获取,有时候出现指定模块文件名记录却未记录到的情况,可以使用PE编辑工具查看模块导出表结构体中的模块名称是否与模块文件名称一致,这种情况很少,不过找测试用例时刚好找到一个qiling的这个demo程序dll_test_x64.dll是这个样子的:
故虽然文件名为“dll_test_x64.dll”,“-trace_to_librarys”参数仍旧指定为“dll_test.dll”,最终trace的日志除包含rundll32.exe主程序代码外还会记录dll_test_x64.dll的main函数及导出函数的执行:
1 | ~~1008~~ KERNELBASE.dll!LoadLibraryExW N:0x2c |
对于一些申请可读写执行属性内存块存放代码并执行的情况,这些不在任意模块中的代码常见于漏洞Shellcode或病毒样本runpe、注入等,hutrace会进行判断并将不在任意模块空间的代码执行纳入默认记录范围。
如果需要同时追踪多个加载模块的运行代码,在“-trace_to_librarys”参数中设置“1.dll!2.dll!3.dll”即可,我一般也就只追踪一个模块,需要同时记录多个模块的情况比较少见。
指定起始、结束地址
该功能主要目的是划分关注的代码范围,提高trace程序运行效率并降低生成的日志文件体积,使用也比较简单,运行hutrace时指定需要开始或结束记录的基本块头部或尾部地址(注意并非指定任意地址,为了性能考虑只对基本块头部和尾部进行判断)
1 | C:\Users\huhu\Desktop\bin64>hutrace.exe -only_from_app -print_ret_addr -print_ins_info -record_start_addr 0x40524A -record_end_addr 0x402422 -- nc64.exe |
实际记录的结果不再展示,指定的起始与结束地址必须为基本块的头部或尾部。对应IDA的信息如下:
常规情况下指定地址记录时设置的为固定值,这时一般要求程序的地址空间固定,可以使用petools等工具设置文件头属性不要求程序进行重定位:
亦可同时设置关闭ASLR(Win7关闭ASLR.bat):
1 | @echo off |
Linux关闭ASLR:
1 | sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" |
对于申请内存并拷贝代码执行情况可以考虑其它手段来固定其申请的内存地址(hutrace提供了一个演示的插件功能可以达成该目的,详见下一篇hutrace在windows系统的应用文章中对任意地址hook功能的介绍),如果不想修改PE文件(有的程序会对完整性进行校验、有时关闭ASLR仍旧存在模块地址随机的情况、还有关闭ASLR但hutrace加载导致的模块地址变化等等情况),这种情况下可以使用“模块名+偏移”方式替代固定地址方式:
1 | C:\Users\huhu\Desktop\bin64>hutrace.exe -only_from_app -print_ret_addr -print_ins_info -trace_to_librarys dll_test.dll -record_start_addr rundll32.exe+0x2d30 -record_end_addr dll_test.dll+0x1030 -- rundll32 dll_test_x64.dll,#1 |
数据流trace
寄存器及内存记录
逆向分析时经常遇到需要定位处理特定数据代码的需求,常规调试手段就不用说了,除了常规手段,“四哥”曾经发过利用Windbg TTD以及Dynamorio代码覆盖率等技术处理该需求的文章:
http://scz.617.cn:8/windows/202202101230.txt
http://scz.617.cn:8/windows/202201251528.txt
上述print_ins_info功能中指令流记录实际上使用的是基本块插桩,无法记录到每一条指令运行时的详细信息,通过指令插桩可以记录到每条指令记录的寄存器以及内存信息,但是引入的主要问题就是会大大降低程序的运行速度,尤其是处理一些图形界面程序时,可以通过逆向或者hutrace的指令流记录,再结合指定起始、结束地址功能进行每条指令运行信息的记录。
常规使用方式的命令格式如下:
1 | C:\Users\huhu\Desktop\bin64>hutrace.exe -only_from_app -print_ret_addr -print_ins_reg -- test.exe |
得到的记录:
1 | B0x140001000-0x14000101c //对应下面6条指令所在基本块范围 |
这一部分代码实现主要参考了ddr插件,做了一部分精简,hutrace的“-print_ins_all”参数实现与原插件的功能较为一致,可以自行测试,该功能打印的内容太多不仅影响运行效率,很多时候也并不需要,仅打印寄存器以及内存的值已经能够满足大部分需求了。
Demo:三分钟定位calc中0x41414141*0x666666的计算指令地址
下面介绍使用hutrace进行一个简单演示,快速定位计算器中计算指令位置(win7x64测试环境,win10系统计算器不行,参见四哥的文章中dynamorio部分):
1 | C:\Users\huhu\Desktop\bin64>hutrace.exe -only_from_app -print_ret_addr -print_ins_reg -- calc |
在计算器中切换到程序员的16进制模式,输入并计算41414141*666666,计算结束后关闭计算器程序,使用010Edit打开trace日志,文件大小大约200多兆,直接010里搜索“41414141”,大概有500条记录,可以在搜索结果里直接查找“666666”,也可以将记录拷贝至新文件再搜索,最后一条出现“41414141”的记录也即是乘法运算的指令处:
整个过程基本无脑操作。。trace操作过程大概两分钟,搜索1分钟,这种记录状态会较为严重的影响运行效率,最耗时也就是trace的时间,尤其是图形界面程序,会有卡顿的现象,但是总的来说已经比基于调试器的trace效率有一个很大的提升了,我自己简单测试下TTD的速度比hutrace的速度还是快很多的,其它优点也超级多,不过一旦hutrace记录完成,hutrace形成的记录结果的更为直观,清晰可搜,当然这里只是最简单的示例。
曹操:“关将军真乃神人也!”,关羽:“何足道哉?吾弟张翼德,于万军之中,取敌将首级,如杀鸡取卵,探囊取物耳”(恶搞)
其它功能
还有一些小功能,有的比较零碎简单介绍下,有的稍微麻烦点在后面的文章中进行实例介绍。
简易Patch
有时想对程序进行运行时的patch,可以在hutrace.config文件中设置如下:
1 | patch|0x1400010AB|0x14000114D*9090|0x140001224*9090|0x1400020A8*61616141414141616161616161 |
其中0x1400010AB代表代码执行到该位置时进行patch,此处可设置为任意地址,后面列举需要进行补丁的地址以及数据,依次对0x14000114D、0x140001224等三个地址进行数据的修改,需要设置的补丁数据为16进制字符串形式。
以scyllahide的调试状态检测程序ScyllaTest_x64.exe为例,虽然hutrace可以躲避其调试状态检测功能,但是不支持ScyllaTest_x64程序的AllocConsole功能,故可以对其退出代码进行patch:
1 | patch|140001CC0|0x140001CDD*9090 |
虽然后续会报错AllocConsole失败,但是程序继续运行不会退出,trace得到后续代码的执行情况,同时可验证hutrace对常见反调试技术的绕过效果:
1 | C:\Users\huhu\Desktop\bin64>hutrace.exe -only_from_app -print_ret_addr -print_ins_info -- ScyllaTest_x64.exe |
该功能仅作为简易的补丁操作,需实现复杂需求的话可以编写对应的Hook插件,在后面的文章中会进行详细的介绍。
隐藏指定函数记录
在trace图形界面程序时经常有一些不需关注的函数的调用,有时记录的频次太多也可能会影响hutrace的运行,可以在hutrace.config文件中设置忽略该部分函数的记录(已经默认添加)。
1 | hide|KiUserCallbackDispatcher |
内存dump功能
在hutrace.config文件中设置需要dump的时机以及需要dump的内存地址以及内存大小。
1 | dump|0x14000101c|0x140000000|0x5000 |
意为代码执行到0x14000101c指令处时对内存地址0x140000000开始的0x5000大小字节的数据进行dump,和日志文件生成在同一目录,选择dump时机如0x14000101c时可以指定任意地址哦。
系统调用Syscall记录
Linux下常用strace记录系统调用,相应需求也比较常见,而Windows上记录Syscall记录的需求比较小众,而且不同版本下的系统调用号差异较大,同时还引入一些小问题,将分别在后续两篇中Window、linux应用篇进行介绍。
任意地址Hook之插件编写
略微麻烦点,同样放入第2、3篇实际应用篇分别进行Win和linux系统下插件的介绍,俩个系统略有差异。
总结
hutrace的功能已经能够覆盖较多的分析场景了,无论是漏洞分析、样本分析、软件破解等等,最简单的功能就是掌握API参数及返回结果信息,同时使用print_ins_info、print_ins_reg功能可记录全部执行流程情况,进行代码或数据的追踪回溯,关键还是在性能上trace效率、trace日志文件大小以及相应的日志检索的效率,目前测试的图形界面程序虽然功能略有卡顿,都还是可以正常运行的(print_ins_reg功能最卡顿些,而且如果应用于程序的全流程跟踪或者如果遇到一些密码算法代码等会可能会形成庞大的日志文件,检索也不方便,有需求的话建议结合IDA以及指定起始、结束地址进行trace,过滤无关代码)。
开发hutrace的目的只是为了提升分析效率,掌握程序执行流程,有时不需要高深的调试技巧就可以实现敏感代码的定位及回溯,说实话也少了一些调试的乐趣,整体的运行流程可以直接在010edit上检索,用高大上的词来说就是降维打击,把程序的执行流程二维化,但是调试技巧还是基础,有的分析场景下可能还是调试或者IDA怼更直观、速度更快点,能结合使用效果更佳,可开发些IDA插件对记录的地址进行符合信息标注处理、图形化展示等等。
github地址:https://github.com/huhu0706/hutrace