使用 Pin 进行代码覆盖率测量的深入探讨
引言
在逆向工程二进制文件时,有时需要测量或了解特定执行对目标代码的覆盖程度。这可能用于模糊测试,或者比较两次执行之间的覆盖差异以定位程序处理特定功能的位置。但通常没有目标源代码,且需要快速执行。此外,没有覆盖整个代码库的输入,甚至不知道是否可能,因此无法将分析与此“理想”分析进行比较。但可以记录程序使用输入 A 和执行输入 B 时的操作,然后分析差异,从而更精确地了解哪个输入似乎具有更好的覆盖率。
这也是一个使用 Pin 的完美机会。本文将简要介绍如何使用 Pin 构建此类工具,以及如何将其用于逆向工程目的。
目录
引言
我们的 Pintool
查看结果
跟踪差异
是否可扩展?
便携式 Python 2.7.5.1
无插桩
带插桩和 JSON 报告序列化
VLC 2.0.8
无插桩
带插桩和 JSON 报告序列化
浏览器?
结论
参考文献与灵感来源
我们的 Pintool
如果您从未听说过 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。
在编码之前,我们需要讨论一下我们真正想要什么。这很简单,我们想要一个 Pintool:
尽可能高效。好吧,这是一个真正的问题;即使 Pin 比其他 DBI 框架(如 DynamoRio 或 Valgrind)更高效,它仍然有点慢。
跟踪所有执行的基本块。我们将存储每个执行的基本块地址及其指令数。
生成关于特定执行的 JSON 报告。一旦有了报告,我们就可以使用 Python 脚本做任何我们想做的事情。为此,我们将使用 Jansson:它易于使用、开源且用 C 编写。
不插桩 Windows API。我们不想浪费 CPU 时间在系统的本机库中;这是我们提高 Pintool 速度的一些小“技巧”。
我认为现在是编码的时候了:首先,让我们定义几个数据结构来存储我们需要的信息:
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;
复制代码
前两种类型将用于保存模块相关信息:模块路径、起始地址和结束地址。第三种很简单:键是基本块地址,值是指令数。
然后我们将定义我们的插桩回调:
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 函数可以轻松完成。当然,如果基本块地址在黑名单地址范围内,我们不会插入对分析函数的调用。
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 很可能会内联该函数,这对效率也很酷。
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 报告。您可能还想使用二进制序列化以获得更小的报告。
VOID save_instrumentation_infos()
{
/// basic_blocks_info section
json_t *bbls_info = json_object();
json_t *bbls_list = json_array();
json_t *bbl_info = json_object();
// unique_count field
json_object_set_new(bbls_info, "unique_count", json_integer(basic_blocks_info.size()));
// list field
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);
}
/* .. same thing for blacklisted modules, and modules .. */
/// Building the tree
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);
/// Writing the report
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,该字段保存该 stub 的地址。这样,您就不会在程序每次执行系统调用时浪费 CPU 时间。
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 报告中!)。
# idapy_color_path_from_json.py
import json
import idc
import idaapi
def color(ea, nbins, c):
'''Color 'nbins' instructions starting from ea'''
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 如何工作,您想专注于“真实”代码,而不是那种“噪音”。每次您在某处有噪音时,您必须找出一种方法来过滤那种噪音;以便只保留有趣的部分。这正是我们在生成程序的不同执行跟踪时所做的事情,并且过程每次都非常相同:
对我们来说很酷,因为通过 IDAPython 很容易实现,以下是脚本:
# idapy_color_diff_from_jsons.py https://github.com/0vercl0k/stuffz/blob/master/pin-code-coverage-measure/idapy_color_diff_from_jsons.py
import json
import idc
import idaapi
from collections import defaultdict
def color(ea, nbins, c):
'''Color 'nbins' instructions starting from ea'''
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 中的模块都被黑名单了。此外,请注意这些测试并不非常准确,我没有启动每个东西数千次,它只是在这里给您一个模糊的概念。
便携式 Python 2.7.5.1
无插桩
PS D:\> Measure-Command {start-process python.exe "-c 'quit()'" -Wait}
TotalMilliseconds : 73,1953
复制代码
带插桩和 JSON 报告序列化
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
无插桩
PS D:\> Measure-Command {start-process vlc.exe "--play-and-exit hu" -Wait}
TotalMilliseconds : 369,4677
复制代码
带插桩和 JSON 报告序列化
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 文档。说真的,还有什么?
使用它,对您有好处。
参考文献与灵感来源
如果您觉得这个主题很酷,我列出了一些很酷的阅读材料:
评论