d的模糊测试

原文
ldc使用LLVMlibFuzzer.使用-fsanitize=fuzzer编译代码,可指导模糊测试的控制流检测,并与驱动模糊测试的libFuzzer库链接(与Clang相同).-fsanitize=fuzzer可从LDC1.4.0获得,而不是在窗口上.示例使用了LDC1.6.0.

模糊和模糊库

模糊测试,是用随机生成的输入多次测试程序(部分)来查找错误的技术.模糊测试后,一般最终会得到,测试程序大部分执行路径的大量输入文件集合,及程序崩溃的输入文件集合(最好没有).

模糊测试的关键要素是"多"和"随机"."随机"即输入是在不一定知道程序一般接收的输入类型时生成的.会导致许多垃圾测试,因此必须运行许多此类测试(如,每秒超过500次测试).

为了指导生成伪随机输入,可检测代码,以便模糊器可检测新输入是否生成了以前未见过的执行跟踪,假设不同状态转换比带不同值的相同执行跟踪更有趣(即错误).这叫覆盖引导模糊测试.模糊测试应该是快速的,因此控制流的指令和检测是近似的,精度和速度间的良好平衡很重要.

要使用libFuzzer,必须编译时加上LLVM的代码覆盖率检测,且需要与libFuzzer运行时库链接.

libFuzzer库实现main并驱动模糊测试,发送测试输入到用户必须定义的LLVMFuzzerTestOneInput函数.LDCClang一样,有个专门模糊测试的命令行开关:-fsanitize=fuzzer.该开关设置-fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp并链接libFuzzer运行时库.

在文档及教程有更多libFuzzer信息.

运行时检查

通过让编译器插入额外的运行时检查来检查错误行为,可大大提高模糊测试的有效性.如,如果没有边界检查,数组重载可能(偶然)对你的程序不是致命的.

D内置的安全功能之一是边界检查数组访问.许多人使用-release-boundscheck=off禁止这些边界检查以获得额外的性能.

但是,我相信在模糊测试时,在允许边界检查时,会发现更多漏洞.内置边界检查也可用.ptr属性访问数组来绕过:

array.ptr[i]而不是一般的数组[i].使用.ptr访问的一个原因是在本地禁止数组边界检查,但为程序的非性能临界区允许边界检查.但是,使用.ptr会永久禁止内置边界检查,并且(不修改代码,则)无法重新允许该检查以模糊测试.

简单示例

看看带bug的小模糊目标:

// File: example.d

bool FuzzMe(const(ubyte[]) data)
{
    return (data.length >= 3) &&
       data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' &&
       data[3] == 'Z';//`<-`访问可能越界
}

extern (C) int LLVMFuzzerTestOneInput(const(ubyte*) data, size_t size)
{
    FuzzMe(data[0 .. size]);
    return 0;
}

-fsanitize=fuzzer编译,并运行程序(大部分输出已剪切).

> ldc2 -fsanitize=fuzzer -g ../example.d -of=example
> ./example
...
==18381== ERROR: libFuzzer: deadly signal
SUMMARY: libFuzzer: deadly signal
MS: 3 EraseBytes-ChangeBit-EraseBytes-; base unit: 3e67ddbe5fee7aa7241d0db7cb27b2199c34091b
0x46,0x55,0x5a,
FUZ
artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60
...

模糊器已找到崩溃的输入.导致崩溃的输入字节(0x46,0x55,0x5a,FUZASCII编码)已写入文件crash-0eb8e4ed029b774d80f2b66408203801cb982a60.可用此文件重现崩溃.

调试器中加载示例,看看是什么导致了崩溃.

> lldb example
(lldb) target create "example"
Current executable set to 'example' (x86_64).
(lldb) run crash-0eb8e4ed029b774d80f2b66408203801cb982a60
...
/Users/johan/github.io/johanengelen.github.io/code/fuzz/run_example/example: Running 1 inputs 1 time(s) each.
Running: crash-0eb8e4ed029b774d80f2b66408203801cb982a60
Process 21839 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x000000010017073d example`gc_malloc + 13
...
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
  * frame #0: 0x000000010017073d example`gc_malloc + 13
    frame #1: 0x000000010015daaa example`onRangeError + 26
    frame #2: 0x000000010015e5f8 example`_d_arraybounds + 8
    frame #3: 0x00000001000010af example`_D7example6FuzzMeFxAhZb(data=(length = 3, ptr = "FUZ")) at example.d:5
    frame #4: 0x0000000100001100 example`LLVMFuzzerTestOneInput(data="FUZ", size=3) at example.d:12
...

崩溃回溯展示了崩溃过程:在_D7example6FuzzMeFxAhZb内部(示例.FuzzMe),边界检查代码_d_arraybounds调用,发现索引越界并调用调用gc_malloconRangeError,然后程序崩溃,因为没有初化运行时,gc_malloc崩溃!

完整非常简单的示例

必须手动初化druntime,因为libFuzzer实现了main,而程序中没有"Dmain".注意,多次调用fuzz目标,但只应初化一次druntime.此外,必须设置一个try-catch子句来抓并报告测试中所有未抓的异常.完整简单示例如下:

// File: example2.d

bool FuzzMe(const(ubyte[]) data)
{
    return (data.length >= 3) &&
           data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' &&
           data[3] == 'Z';//`<-`访问可能越界
}

int LLVMFuzzerTestOneInput(const(ubyte[]) data)
{
    FuzzMe(data);
    return 0;
}

//不必编写的样板:
extern (C) void _d_print_throwable(Throwable t);
extern (C) int LLVMFuzzerTestOneInput(const(ubyte*) data, size_t size) {
//必须初化D运行时,但只能初化一次.
    static bool init = false;
    if (!init) {
        import core.runtime : rt_init;
        rt_init();
        init = true;
    }
    try {
        return LLVMFuzzerTestOneInput(data[0 .. size]);
    }
    catch (Throwable t) {
        _d_print_throwable(t);
        import ldc.intrinsics : llvm_trap;
        llvm_trap();
    }
    assert(0);
}

这样,就做好了,传递"FUZ"崩溃测试用例时,输出如下:

> ldc2 -fsanitize=fuzzer -g ../example2.d -of=example2
> ./example2 crash-0eb8e4ed029b774d80f2b66408203801cb982a60
...
INFO: Seed: 3516036321
INFO: Loaded 1 modules (24 guards): [0x1096b7950, 0x1096b79b0),
./example2: Running 1 inputs 1 time(s) each.
Running: crash-0eb8e4ed029b774d80f2b66408203801cb982a60
core.exception.RangeError@../example2.d(7): Range violation
----------------
0   example2                            0x000000010957e9b9 object.Throwable.TraceInfo core.runtime.defaultTraceHandler(void*) + 137
1   example2                            0x000000010959218d _d_throw_exception + 77
2   example2                            0x000000010957ca6e onRangeError + 158
3   example2                            0x000000010957d537 _d_arraybounds + 7
4   example2                            0x000000010941fe8e bool example2.FuzzMe(const(ubyte[])) + 734
5   example2                            0x000000010941fedf int example2.LLVMFuzzerTestOneInput(const(ubyte[])) + 63
6   example2                            0x000000010941ff8f LLVMFuzzerTestOneInput + 159
7   example2                            0x000000010942baf0 _ZN6fuzzer6Fuzzer15ExecuteCallbackEPKhm + 336
8   example2                            0x0000000109420781 _ZN6fuzzer10RunOneTestEPNS_6FuzzerEPKcm + 257
9   example2                            0x0000000109424b59 _ZN6fuzzer12FuzzerDriverEPiPPPcPFiPKhmE + 6105
10  example2                            0x0000000109431fd2 main + 34
==27357== ERROR: libFuzzer: deadly signal

为了模糊化你自己的项目,建议从example2.d复制样板代码.
看看如何模糊测试实际代码.我使用熟悉的测试用例:D编译器.

示例

Lexer类接口要求源码缓冲以null结尾.libFuzzer提供的数据不是以null结尾的,因此必须复制输入数据到自己的缓冲并附加一个null终止符,然后再传递它给Lexer.必须动态分配自己的缓冲,应用malloc而不是GC.

这是因为要用ASan来抓涉及该缓冲的错误,而目前LDCASan实现无法抓GC'内存的缓冲过度读取.

最后要注意,必须确保模糊目标正在做编译器认为有用的事情.即,代码必须有外部可观察的效果,否则会被优化器完全消除.通过简单地加所有令牌的值到总和全局变量中来完成.

// File fuzzlexer.d
import ddmd.tokens;
import ddmd.lexer;
import core.stdc.stdlib : malloc, free;
import core.stdc.string : memcpy;

// Add the boilerplate from example2.d.
//加`示例2.d`中的样板.
TOK sum;
//累积令牌值的总和以避免消除死码

int LLVMFuzzerTestOneInput(const(ubyte[]) data)
{
    if (data.length < 1)
        return 0;

//总是以`无效`值终止`输入`数据,使用`malloc`而不是`GC`
    ubyte* data_nullterminated = cast(ubyte*) malloc(data.length + 1);
    scope(exit) free(data_nullterminated);
    memcpy(data_nullterminated, data.ptr, data.length);
    data_nullterminated[data.length] = 0;

    scope lexer = new Lexer("test", cast(char*) data_nullterminated,
        0, data.length, false, false);

    do {
        sum += lexer.token.value;
    }
    while (lexer.nextToken != TOKeof);

    return 0;
}

编译器命令行很长,因为它也必须包含dmd词法解析器文件,但这里是为了完整性.为了获得最大调试信息,我添加了-disable-fp-elim-link-debuglib,但这并不是真正需要的.

ldc2 -fsanitize=fuzzer,address -disable-fp-elim fuzzlexer.d -of=fuzzlexer -g -O3 -link-debuglib -Jdmd -Jdmd/src/ddmd -Idmd/src dmd/src/ddmd/root/array.d dmd/src/ddmd/root/ctfloat.d dmd/src/ddmd/root/file.d dmd/src/ddmd/root/filename.d dmd/src/ddmd/root/hash.d dmd/src/ddmd/root/outbuffer.d dmd/src/ddmd/root/port.d dmd/src/ddmd/root/rmem.d dmd/src/ddmd/root/rootobject.d dmd/src/ddmd/root/stringtable.d dmd/src/ddmd/console.d  dmd/src/ddmd/entity.d  dmd/src/ddmd/errors.d  dmd/src/ddmd/globals.d  dmd/src/ddmd/id.d  dmd/src/ddmd/identifier.d  dmd/src/ddmd/lexer.d  dmd/src/ddmd/tokens.d  dmd/src/ddmd/utf.d

期望是,在代码深处会有一个罕见特例错误,且模糊器必须运行数小时,来覆盖越来越多的词法解析器,也许最终才找到错误.
多次运行模糊器,有时在15秒后仍没有发现错误,但一般在一秒钟内找到错误;该过程是()随机的,因此这是意料之中的.

看看输出.运行带-close_fd_mask=3模糊目标以关闭stdout,并在词法解析器报告奇怪的无效输入时使它静音,这些不是错误!

> ./fuzzlexer -close_fd_mask=3
...
#18 NEW    cov: 272 ft: 348 corp: 11/6819b exec/s: 0 rss: 34Mb L: 3 MS: 1 InsertByte-
=================================================================
==13181==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000f14 at pc 0x000103318050 bp 0x7ffeec901250 sp 0x7ffeec901248
READ of size 1 at 0x602000000f14 thread T0
    #0 0x10331804f in _D4ddmd5lexer5Lexer4scanMFPS4ddmd6tokens5TokenZv lexer.d:1047
    #1 0x10338760b in _D9fuzzlexer22LLVMFuzzerTestOneInputFxAhZi lexer.d:234
    #2 0x1033878ba in LLVMFuzzerTestOneInput fuzzlexer.d:78
...
0x602000000f14 is located 0 bytes to the right of 4-byte region [0x602000000f10,0x602000000f14)
allocated by thread T0 here:
    #0 0x1051ce5fc in wrap_malloc (libldc_rt.asan_osx_dynamic.dylib:x86_64h+0x575fc)
    #1 0x1033873f8 in _D9fuzzlexer22LLVMFuzzerTestOneInputFxAhZi fuzzlexer.d:42
    #2 0x1033878ba in LLVMFuzzerTestOneInput fuzzlexer.d:78
...
SUMMARY: libFuzzer: deadly signal
MS: 3 InsertByte-EraseBytes-InsertByte-; base unit: a59478b0623bc537405253419b0b0d5c9430effa
0xbe,0x60,0xae,
\xbe`\xae
artifact_prefix='./';测试单元写入 ./crash-7490356393f3fad31d4f0d3f5bbb6069342a02ed

ASan发现当输入为[0xbe,0x60,0xae]时,词法解析器内部有堆缓冲溢出.崩溃位置报告为词法分析.d:1047,这是继续语句.因为LDC开关调试信息中有错误,崩溃位置报告错误,实际崩溃行是开关条件求值.

过度读取内存,是因为带未配对串引号错误.缓冲的第一个null终止符,会终止词法分析串令牌,但不会终止整个词法,然后读取下个符(超出终止符,即超出缓冲).

看看另一个需要处理可能未经检查用户数据的代码片:Phobosstd.xml库.

Fuzzing std.xml
// File fuzzxml.d
static import std.xml;

import ldc.attributes : optStrategy;
@optStrategy("none")
int LLVMFuzzerTestOneInput(const(ubyte[]) data)
{
    if (data.length < 1)
        return 0;

    try {
        // 注意:最后元素不一定是`"null"`.
        string str = cast(string)data;
//检查格式是否良好(抛)
        std.xml.check(str);
        // 造`DOM`树
        scope auto doc = new std.xml.Document(str);
    }
    catch (Exception e) {
    //(与'错误'相反),`抛异常`不是错误.
    }

    return 0;
}

此时,我用@(ldc.attributes.optStrategy("none"))来禁止函数中的死码消除(注意,未使用doc,因此编译器优化器可能会删除它).

允许模糊器ASan编译,然后运行模糊器.输出为:

> ldc2 -fsanitize=fuzzer,address -disable-fp-elim ../fuzzxml.d -of=fuzzxml -O3 -g -link-debuglib
> ./fuzzxml
INFO: Seed: 1352319858
INFO: Loaded 1 modules (24 guards): [0x10f59d690, 0x10f59d6f0),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#0  READ units: 1
#2  INITED cov: 15 ft: 8 corp: 1/1b exec/s: 0 rss: 32Mb
#16384  pulse  cov: 15 ft: 8 corp: 1/1b exec/s: 8192 rss: 58Mb
#32768  pulse  cov: 15 ft: 8 corp: 1/1b exec/s: 8192 rss: 64Mb
#65536  pulse  cov: 15 ft: 8 corp: 1/1b exec/s: 7281 rss: 75Mb
#131072 pulse  cov: 15 ft: 8 corp: 1/1b exec/s: 6898 rss: 99Mb

进度缓慢(语料库不增长),输出显示已加载1个模块(24个警卫);嗯,只有24个警卫,"警卫"跟踪执行流程并引导模糊器的覆盖警卫.

24名警卫非常低:Lexer7567名警卫.std.xml代码不太可能完全没有分支.事实上,忘记了一些事情:标准库是在禁止检测覆盖率时编译的.
std.xml.checkstd.xml.Document的二进制代码在标准库库中.因此,必须构建允许了检测的标准库.

幸好,LDC附带了使使用自定义命令行标志重建标准库容易的ldc-build-runtime工具.
因此,用-fsanitize=fuzzer,address构建运行时;如前,需要为标准库指定asan_blacklist.txt.现在,链接加了检测的标准库时,得到以下模糊输出:

> ldc-build-runtime --dFlags='-fsanitize=address;-fsanitize-blacklist=asan_blacklist.txt' BUILD_SHARED_LIBS=OFF
...
Runtime libraries built successfully into: ./ldc-build-runtime.tmp
> ldc2 -fsanitize=fuzzer,address -disable-fp-elim ../fuzzxml.d -of=fuzzxml -O3 -g -link-debuglib -L-Lldc-build-runtime.tmp/lib
> ./fuzzxml
...
INFO: Loaded 1 modules (48686 guards): [0x101cafa20, 0x101cdf2d8),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#0  READ units: 1
#2  INITED cov: 1313 ft: 469 corp: 1/1b exec/s: 0 rss: 35Mb
#3  NEW    cov: 1313 ft: 470 corp: 2/2b exec/s: 0 rss: 35Mb L: 1 MS: 1 ShuffleBytes-
#4  NEW    cov: 1432 ft: 935 corp: 3/3b exec/s: 0 rss: 35Mb L: 1 MS: 2 ShuffleBytes-ChangeBinInt-
#5  NEW    cov: 1436 ft: 952 corp: 4/4099b exec/s: 0 rss: 35Mb L: 4096 MS: 3 ShuffleBytes-ChangeBinInt-CrossOver-
#8  NEW    cov: 1437 ft: 970 corp: 5/4101b exec/s: 0 rss: 35Mb L: 2 MS: 1 CrossOver-
...

太好了,现在有更多模糊器可用来指导模糊测试警卫装置.此外,还在(使用)标准库中,获得了允许ASan的错误检测,因此可在std.xml中检测更多种类的错误.很抱歉让你失望了,但我在模糊测试时没有发现std.xml中的错误!

语料库

在运行时,模糊器创建测试输入的"语料库".每个语料库项都是测试输入,可在程序中产生不同的执行流.因此,语料库形成了自动生成的测试用例集合,有较高总代码覆盖率.
与其每次运行测试时都从头开始重启模糊器,不如使用上一次运行收集的语料库启动模糊器.
我让fuzzxml程序运行了6.5分钟,结果产生了一个包含383个项的语料库.最后项如下:

<mmwx5lwwwwwwwwwwwwwwwx5lwwwwwwww-5lwwwwwwwwwwwwwwwx5lwwwwwwww-->
wwwwwowwwwwwwwwwwwwwwwx2lwwwwwwww-->
wwwwwowwwwwwwwx5lwwwwwwww-5lwwwwwwwwwwwwwwwx5lwwwwwwww-->
wwwwwowwwwwwwwwwwwwwwww=ww&#wwwo

可见模糊器"发现"XML期望的早期阶段.但是可帮助模糊器更快地发现XML.

模糊更深的层

随机数据模糊测试只能让你到此为止.要到达程序的更深层次,输入需要超越有效性检查.可用字典文件来生成包含正在测试的程序的常见关键字的输入.
XML模糊目标,可用AFLgit仓库中的字典,因为字典文件的语法在libFuzzerAFL之间共享.字典中的片段:

tag_open=""
tag_open_close=""
tag_open_exclamation="
tag_open_q="< "
tag_sq2_close="]]>"
tag_xml_q="< xml >"

有了该字典,让模糊器再次运行6.5分钟(fuzzxml corpus -dict=xml.dict).此时,得到一个包含752个项的语料库;最后几个项(非顺序):

<a version="1" versioon=""/>
<a versiof="1" versioon=""/>
<a version="1" version="1"/>

改进思路

使用AFL模糊测试应该已可用-fsanitize-coverage=trace-pc-guard,但我还没有测试过它.为了更快地使用AFL模糊测试,可向LDC添加类似Clang的插件功能,以便人们可试用AFL插件.

你可能感兴趣的:(dlang,d,d,模糊测试)