使用Pin进行代码覆盖率测量的思考与实践

本文详细介绍了如何使用Intel的Pin工具进行动态二进制插桩,测量代码覆盖率,生成JSON报告,并通过IDAPython脚本可视化执行路径,适用于逆向工程和模糊测试场景。

使用Pin进行代码覆盖率测量的思考与实践

引言

在逆向工程二进制文件时,有时需要测量或了解特定执行对目标代码的覆盖程度。这可能用于模糊测试,例如当你拥有大量输入(文件、网络流量等)并希望仅使用子集实现相同覆盖时。或者,你可能不关心具体测量值,而只关注两次执行之间的覆盖差异,以定位程序处理特定功能的位置。

但这并非易事,通常你没有目标的源代码,且希望过程快速。另外,你没有覆盖整个代码库的输入,甚至不知道是否可能;因此无法将分析与此“理想”情况比较。简而言之,你无法告诉用户“此输入覆盖了二进制文件的10%”。但你可以记录程序使用输入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速度的小“技巧”之一。

我认为现在是编码的时候了:首先,定义几个数据结构来存储我们需要的信息:

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
14
15
16
17
18
19
20
21
22
23
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
15
16
17
18
19
20
21
22
23
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决定在哪里放置其对分析函数handle_basic_block的JITed调用;我们还使用快速链接(基本上意味着函数将使用__fastcall调用约定调用)。

分析函数也非常简单,我们只需要将基本块地址存储在全局变量中。该方法没有任何分支,这意味着Pin很可能会内联该函数,这对效率也很酷。

1
2
3
4
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
21
22
23
24
25
26
27
28
29
30
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内在函数读取字段TEB32.WOW32Reserved,该字段保存该存根的地址。这样,你不会在程序每次执行系统调用时浪费CPU时间。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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
35
# 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如何工作,你想专注于“真实”代码而不是那种“噪音”。每次你在某处有噪音时,你必须找出一种方法来过滤那种噪音;以便只保留有趣的部分。这正是我们在生成程序的不同执行跟踪时所做的事情,过程每次几乎相同:

  • 你启动应用程序,然后退出
  • 你启动应用程序,做某事,然后退出
  • 你从第二次跟踪中移除第一次运行中执行的基本块;以便只保留“做某事”的部分。这样你过滤由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
51
52
53
54
55
56
57
58
# 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

无插桩

1
2
3
PS D:\> Measure-Command {start-process python.exe "-c 'quit()'" -Wait}

TotalMilliseconds : 73,1953

带插桩和JSON报告序列化

1
2
3
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
3
PS D:\> Measure-Command {start-process vlc.exe "--play-and-exit hu" -Wait}

TotalMilliseconds : 369,4677

带插桩和JSON报告序列化

1
2
3
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的参考!)
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计