使用Pin进行代码覆盖率测量的思考
引言
在逆向工程二进制文件时,有时需要测量或了解特定执行对目标代码的覆盖情况。这可能用于模糊测试,例如当你拥有大量输入(文件、网络流量等)并希望仅用子集实现相同覆盖率时。或者,你可能只关注两次执行间的覆盖率差异,以定位程序处理特定功能的位置。
但这并非易事:通常你没有目标的源代码,且希望过程快速。另外,你没有覆盖整个代码库的输入,甚至不知道是否可能,因此无法将分析与此“理想情况”比较。简而言之,你无法告诉用户“此输入覆盖了二进制文件的10%”。但你可以记录程序在输入A和输入B下的行为,并分析差异,从而更精确地了解哪个输入具有更好的覆盖率。
这也是使用Pin的完美机会。本文将简要介绍如何使用Pin构建此类工具,及其在逆向工程中的应用。
目录
- 引言
- 我们的Pintool
- 查看结果
- 跟踪差异
- 可扩展性测试
- Portable Python 2.7.5.1
- VLC 2.0.8
- 浏览器?
- 结论
- 参考文献与灵感来源
如果你从未听说过Intel的DBI框架Pin,以下链接可供参考。了解其工作原理对正确使用Pin至关重要:
- Pin 2.12用户指南
- Pin简介 - Aamer Jaleel
我的设置:在Windows 7 x64上使用Pin 2.12、VC2010,并构建x86 Pintools(与Wow64兼容良好)。如果希望在Pin工具包目录外轻松构建Pintool,我编写了一个方便的Python脚本:setup_pintool_project.py。
在编码前,需明确需求:
- 尽可能高效:Pin虽比其他DBI框架(如DynamoRio或Valgrind)高效,但仍较慢。
- 跟踪所有执行的基本块:存储每个基本块的地址及其指令数。
- 生成关于特定执行的JSON报告:之后可用Python脚本自由处理。使用Jansson库(易用、开源、C编写)。
- 不插桩Windows API:避免浪费CPU时间在系统原生库中,这是提高Pintool速度的“技巧”之一。
现在开始编码。首先定义几个数据结构存储所需信息:
1
2
3
|
typedef std::map<std::string, std::pair<ADDRINT, ADDRINT> > MODULE_BLACKLIST_T;
typedef MODULE_BLACKLIST_T MODULE_LIST_T;
typedef std::map<ADDRINT, UINT32> BASIC_BLOCKS_INFO_T;
|
前两个类型用于存储模块信息:模块路径、起始地址和结束地址。第三个类型简单:键是基本块地址,值是指令数。
然后定义插桩回调:
- 一个用于在模块加载时存储其基址/结束地址,通过IMG_AddInstrumentationFunction设置。
- 一个用于跟踪,通过TRACE_AddInstrumentationFunction设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
VOID image_instrumentation(IMG img, VOID * v) {
ADDRINT module_low_limit = IMG_LowAddress(img), module_high_limit = IMG_HighAddress(img);
if(IMG_IsMainExecutable(img)) return;
const std::string image_path = IMG_Name(img);
std::pair<std::string, std::pair<ADDRINT, ADDRINT> > module_info = std::make_pair(
image_path,
std::make_pair(module_low_limit, module_high_limit)
);
module_list.insert(module_info);
module_counter++;
if(is_module_should_be_blacklisted(image_path))
modules_blacklisted.insert(module_info);
}
|
由于Pin没有BBL_AddInstrumentationFunction,我们必须插桩跟踪,迭代获取基本块。使用TRACE_BblHead、BBL_Valid和BBL_Next函数轻松实现。如果基本块地址在黑名单地址范围内,则不插入分析函数调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
VOID trace_instrumentation(TRACE trace, VOID *v) {
for(BBL bbl = TRACE_BblHead(trace); BBL_Valid(bbl); bbl = BBL_Next(bbl)) {
if(is_address_in_blacklisted_modules(BBL_Address(bbl))) continue;
BBL_InsertCall(
bbl,
IPOINT_ANYWHERE,
(AFUNPTR)handle_basic_block,
IARG_FAST_ANALYSIS_CALL,
IARG_UINT32, BBL_NumIns(bbl),
IARG_ADDRINT, BBL_Address(bbl),
IARG_END
);
}
}
|
出于效率考虑,让Pin决定何处放置JITed调用到分析函数handle_basic_block;并使用快速链接(即函数通过__fastcall调用约定调用)。
分析函数也很简单,只需将基本块地址存储在全局变量中。该方法无任何分支,意味着Pin很可能内联该函数,这对效率也有利。
1
2
3
|
VOID PIN_FAST_ANALYSIS_CALL handle_basic_block(UINT32 number_instruction_in_bb, ADDRINT address_bb) {
basic_blocks_info[address_bb] = number_instruction_in_bb;
}
|
最后,在进程结束前,使用jansson将数据序列化为简单的JSON报告。也可使用二进制序列化以获得更小的报告。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
VOID save_instrumentation_infos() {
json_t *bbls_info = json_object();
json_t *bbls_list = json_array();
json_t *bbl_info = json_object();
json_object_set_new(bbls_info, "unique_count", json_integer(basic_blocks_info.size()));
json_object_set_new(bbls_info, "list", bbls_list);
for(BASIC_BLOCKS_INFO_T::const_iterator it = basic_blocks_info.begin(); it != basic_blocks_info.end(); ++it) {
bbl_info = json_object();
json_object_set_new(bbl_info, "address", json_integer(it->first));
json_object_set_new(bbl_info, "nbins", json_integer(it->second));
json_array_append_new(bbls_list, bbl_info);
}
json_t *root = json_object();
json_object_set_new(root, "basic_blocks_info", bbls_info);
json_object_set_new(root, "blacklisted_modules", blacklisted_modules);
json_object_set_new(root, "modules", modules);
FILE* f = fopen(KnobOutputPath.Value().c_str(), "w");
json_dumpf(root, f, JSON_COMPACT | JSON_ENSURE_ASCII);
fclose(f);
}
|
如果在x64 Windows系统上插桩x86进程,应直接黑名单Windows保存SystemCallStub的区域(即“JMP FAR”)。使用__readfsdword intrinsic读取TEB32.WOW32Reserved字段,该字段持有该存根地址。这样,程序每次执行系统调用时都不会浪费CPU时间。
1
2
3
4
5
6
7
|
ADDRINT wow64stub = __readfsdword(0xC0);
modules_blacklisted.insert(
std::make_pair(
std::string("wow64stub"),
std::make_pair(wow64stub, wow64stub)
)
);
|
完整Pintool源代码在此:pin-code-coverage-measure.cpp。
查看结果
拥有JSON报告显示程序执行的基本块固然好,但对人类可读性不佳。可使用IDAPython脚本解析报告,并着色所有执行的指令。这样能更好地查看程序的执行路径。
着色指令使用函数:idaapi.set_item_color和idaapi.del_item_color(如需重置颜色)。也可使用idc.GetItemSize了解指令大小,从而迭代特定数量的指令(记得,我们在JSON报告中存储了此信息!)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
# idapy_color_path_from_json.py
import json
import idc
import idaapi
def color(ea, nbins, c):
colors = defaultdict(int, {
'black' : 0x000000,
'red' : 0x0000FF,
'blue' : 0xFF0000,
'green' : 0x00FF00
}
)
for _ in range(nbins):
idaapi.del_item_color(ea)
idaapi.set_item_color(ea, colors[c])
ea += idc.ItemSize(ea)
def main():
f = open(idc.AskFile(0, '*.json', 'Where is the JSON report you want to load ?'), 'r')
c = idc.AskStr('black', 'Which color do you want ?').lower()
report = json.load(f)
for i in report['basic_blocks_info']['list']:
print '%x' % i['address'],
try:
color(i['address'], i['nbins'], c)
print 'ok'
except Exception, e:
print 'fail: %s' % str(e)
print 'done'
return 1
if __name__ == '__main__':
main()
|
以下是启动“ping google.fr”生成的示例,黑色节点清晰显示ping实用程序到达的节点:
(图片描述:执行路径可视化)
你甚至可以开始生成具有不同选项的多个跟踪,以查看每个参数在程序中的处理和分析位置。
跟踪差异
如前所述,查看程序的执行路径很方便。但思考后,查看两次不同执行间的差异可能更方便。可用于定位程序的特定功能:如许可证检查、选项检查等。
现在,运行另一个跟踪,例如“ping -n 10 google.fr”。以下是两次执行的跟踪及两者间的差异(前一次和新一次):
(图片描述:执行差异对比)
你可以清晰识别使用“-n 10”参数的基本块和函数。
如果更仔细地观察,你能快速找出字符串转换为整数的位置:
(图片描述:字符串转换位置)
许多软件围绕非常烦人的GUI构建(至少对逆向者而言):通常生成大型二进制文件,或附带大量外部模块(如Qt运行时库)。但你不关心GUI的工作原理,希望专注于“真实”代码而非“噪音”。每当有噪音时,必须找出过滤方法;仅保留有趣部分。这正是生成程序不同执行跟踪时所做的工作,过程每次基本相同:
- 启动应用程序并退出
- 启动应用程序,执行某些操作后退出
- 从第二次跟踪中移除第一次运行中执行的基本块;仅保留执行“某些操作”的部分。这样过滤GUI引入的噪音,专注于有趣部分。
这对我们来说很酷,因为通过IDAPython很容易实现,以下是脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
# idapy_color_diff_from_jsons.py
import json
import idc
import idaapi
from collections import defaultdict
def color(ea, nbins, c):
colors = defaultdict(int, {
'black' : 0x000000,
'red' : 0x0000FF,
'blue' : 0xFF0000,
'green' : 0x00FF00
}
)
for _ in range(nbins):
idaapi.del_item_color(ea)
idaapi.set_item_color(ea, colors[c])
ea += idc.ItemSize(ea)
def main():
f = open(idc.AskFile(0, '*.json', 'Where is the first JSON report you want to load ?'), 'r')
report = json.load(f)
l1 = report['basic_blocks_info']['list']
f = open(idc.AskFile(0, '*.json', 'Where is the second JSON report you want to load ?'), 'r')
report = json.load(f)
l2 = report['basic_blocks_info']['list']
c = idc.AskStr('black', 'Which color do you want ?').lower()
addresses_l1 = set(r['address'] for r in l1)
addresses_l2 = set(r['address'] for r in l2)
dic_l2 = dict((k['address'], k['nbins']) for k in l2)
diff = addresses_l2 - addresses_l1
print '%d bbls in the first execution' % len(addresses_l1)
print '%d bbls in the second execution' % len(addresses_l2)
print 'Differences between the two executions: %d bbls' % len(diff)
assert(len(addresses_l1) < len(addresses_l2))
funcs = defaultdict(list)
for i in diff:
try:
color(i, dic_l2[i], c)
funcs[get_func(i).startEA].append(i)
except Exception, e:
print 'fail %s' % str(e)
print 'A total of %d different sub:' % len(funcs)
for s in funcs.keys():
print '%x' % s
print 'done'
return 1
if __name__ == '__main__':
main()
|
顺便提醒,我们仅讨论确定性程序(相同输入总是执行相同路径)。如果相同输入每次不给出完全相同的输出,则程序非确定性。
另外,不要忘记ASLR,因为如果你想比较两个不同时间执行的基本块地址,相信我,你希望二进制文件加载到相同的基地址。然而,如果你想快速修补简单文件,我编写了一个有时很方便的Python脚本:remove_aslr_bin.py;否则,启动Windows XP虚拟机是简单解决方案。
可扩展性测试
这些测试在我的Windows 7 x64笔记本电脑上进行,使用Wow64进程(4GB RAM,i7 Q720 @ 1.6GHz)。黑名单了所有位于C:\Windows的模块。注意,这些测试并不非常准确,我没有每项运行千次,仅为你提供大致概念。
Portable Python 2.7.5.1
无插桩
1
2
|
PS D:\> Measure-Command {start-process python.exe "-c 'quit()'" -Wait}
TotalMilliseconds : 73,1953
|
带插桩和JSON报告序列化
1
2
|
PS D:\> Measure-Command {start-process pin.exe "-t pin-code-coverage-measure.dll -o test.json -- python.exe -c 'quit()'" -Wait}
TotalMilliseconds : 13122,4683
|
VLC 2.0.8
无插桩
1
2
|
PS D:\> Measure-Command {start-process vlc.exe "--play-and-exit hu" -Wait}
TotalMilliseconds : 369,4677
|
带插桩和JSON报告序列化
1
2
|
PS D:\> Measure-Command {start-process pin.exe "-t pin-code-coverage-measure.dll -o test.json -- D:\vlc.exe --play-and-exit hu" -Wait}
TotalMilliseconds : 60109,204
|
为优化过程,你可能想黑名单一些VLC插件(有大量插件!),否则你的插桩VLC比正常慢160倍(我甚至未尝试在解码x264视频时启动插桩)。
浏览器?
你不想在这里看到开销。
结论
如果你希望将此类工具用于模糊测试,我强烈建议你编写一个小程序,以目标相同的方式使用目标库。这样,你有一个更小、更简单的二进制文件进行插桩,因此插桩过程将高效得多。在此特定情况下,我确实相信你可以在大量输入(数千)上启动此Pintool,以挑选更好覆盖目标的输入。另一方面,如果你直接在大型软件(如浏览器)上这样做:它将无法扩展,因为你会花时间插桩GUI或不关心的内容。
Pin是一个真正强大且易用的工具。C++ API非常容易使用,适用于Linux、OSX、Android for x86(甚至在重要目标上支持X86_64),还有doxygen文档。说真的,还有什么?
使用它,对你有好处。
参考文献与灵感来源
如果你觉得此主题很酷,我列出了一些很酷的阅读材料:
- Coverage analyzer:你会发现使用Pin真的更容易
- Code-coverage-analysis-tool:很酷,但似乎在例程级别插桩;我们希望在基本级别拥有信息
- 安全专业人士的二进制插桩
- MyNav,一个Python插件
- zynamics BinNavi视频
- 差分切片:识别安全应用的因果执行差异(感谢j04n的参考!)