原文
ldc
使用LLVM
的libFuzzer
.使用-fsanitize=fuzzer
编译代码
,可指导模糊测试
的控制流检测,并与驱动
模糊测试的libFuzzer
库链接(与Clang
相同).-fsanitize=fuzzer
可从LDC1.4.0
获得,而不是在窗口
上.示例使用了LDC1.6.0
.
模糊测试
,是用随机
生成的输入
多次测试
程序(部分)来查找
错误的技术.模糊
测试后,一般最终会得到,测试
程序大部分执行路径的大量
输入文件集合,及程序崩溃的输入文件
集合(最好
没有).
模糊
测试的关键要素是"多"和"随机"
."随机"
即输入是在不一定知道程序一般接收的输入类型
时生成的.会导致许多垃圾测试
,因此必须运行
许多此类测试(如,每秒超过500
次测试).
为了指导生成伪随机
输入,可检测
代码,以便模糊器
可检测新输入
是否生成了以前未见过的执行跟踪
,假设不同状态
转换比带不同值
的相同执行跟踪更有趣
(即错误).这叫覆盖
引导模糊测试
.模糊测试应该是快速
的,因此控制流的指令和检测
是近似的,精度和速度
间的良好
平衡很重要.
要使用libFuzzer
,必须编译时加上LLVM
的代码覆盖率检测,且需要与libFuzzer
运行时库链接.
libFuzzer
库实现main
并驱动模糊测试,发送测试输入
到用户必须定义的LLVMFuzzerTestOneInput
函数.LDC
和Clang
一样,有个专门
模糊测试的命令行开关:-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,FUZ
的ASCII
编码)已写入文件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_malloc
的onRangeError
,然后程序崩溃,因为没有初化运行时,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
来抓涉及该缓冲的错误,而目前LDC
的ASan
实现无法抓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
终止符,会终止词法分析串令牌
,但不会终止
整个词法,然后读取
下个符(超出空
终止符,即超出缓冲
).
看看另一个需要处理可能未经检查
用户数据的代码片:Phobos
的std.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
名警卫非常低:Lexer
有7567
名警卫.std.xml
代码不太可能完全
没有分支.事实上,忘记了一些事情:标准库
是在禁止检测覆盖率
时编译的.
std.xml.check
和std.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
仓库中的字典,因为字典文件的语法在libFuzzer
和AFL
之间共享.字典中的片段:
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
插件.