写点什么

使用 Pin 进行代码覆盖率测量的深入探讨

作者:qife122
  • 2025-08-23
    福建
  • 本文字数:6270 字

    阅读完需:约 21 分钟

使用 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;
复制代码


前两种类型将用于保存模块相关信息:模块路径、起始地址和结束地址。第三种很简单:键是基本块地址,值是指令数。


然后我们将定义我们的插桩回调:


  • 一个用于在模块加载时存储其基址/结束地址,一个用于跟踪。您可以使用 IMG_AddInstrumentationFunction 和 TRACE_AddInstrumentationFunction 设置回调。


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.pyimport jsonimport idcimport 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 如何工作,您想专注于“真实”代码,而不是那种“噪音”。每次您在某处有噪音时,您必须找出一种方法来过滤那种噪音;以便只保留有趣的部分。这正是我们在生成程序的不同执行跟踪时所做的事情,并且过程每次都非常相同:


  • 您启动应用程序,然后退出

  • 您启动应用程序,执行某些操作,然后退出

  • 您从第二次跟踪中移除第一次运行中执行的基本块;以便只保留执行“做某事”的部分。这样,您过滤了 GUI 引起的噪音,只专注于有趣的部分。


对我们来说很酷,因为通过 IDAPython 很容易实现,以下是脚本:


# idapy_color_diff_from_jsons.py https://github.com/0vercl0k/stuffz/blob/master/pin-code-coverage-measure/idapy_color_diff_from_jsons.pyimport jsonimport idcimport idaapifrom 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 文档。说真的,还有什么?


使用它,对您有好处。

参考文献与灵感来源

如果您觉得这个主题很酷,我列出了一些很酷的阅读材料:


  • 覆盖率分析器:您将看到使用 Pin 真的更容易

  • 代码覆盖率分析工具:很酷,但似乎在例程级别进行插桩;我们想要在基本级别拥有信息

  • 安全专业人员的二进制插桩

  • MyNav,一个 Python 插件

  • zynamics BinNavi 视频

  • 差分切片:识别安全应用的因果执行差异(感谢 j04n 的参考!)更多精彩内容 请关注我的个人公众号 公众号(办公 AI 智能小助手)公众号二维码

  • 办公AI智能小助手
用户头像

qife122

关注

还未添加个人签名 2021-05-19 加入

还未添加个人简介

评论

发布
暂无评论
使用Pin进行代码覆盖率测量的深入探讨_逆向工程_qife122_InfoQ写作社区