内容简介:生成型Fuzzer通常以单一输入类型为目标,根据预定义的语法来生成输入数据,例如对于覆盖率导向的突变型Fuzzer(如然而,通过适当的处理之后,也可以将libFuzzer转换为适用于特定输入类型的语法敏感(即结构敏感)型模糊测试引擎。
生成型Fuzzer通常以单一输入类型为目标,根据预定义的语法来生成输入数据,例如 csmith (可以生成有效的C程序)和 Peach (能够生成任何类型的输入,但需要将这种类型表示为语法定义形式)。
对于覆盖率导向的突变型Fuzzer(如 libFuzzer 或 AFL )来说,其目标并不局限于单个输入类型,也不需要语法定义。因此,突变型Fuzzer通常比生成型Fuzzer更容易设置和使用。但是,由于缺少输入文法,所以对复杂输入类型进行模糊测试的时候,效率较低,因为任何经传统的突变(例如位翻转)而得到的输入,在解析的早期都会被目标API所拒绝,也就是说,生成了许多无效输入。
然而,通过适当的处理之后,也可以将libFuzzer转换为适用于特定输入类型的语法敏感(即结构敏感)型模糊测试引擎。
示例:压缩
下面,我们将通过一个简单的 示例 ,来演示如何使用libFuzzer进行结构敏感型模糊测试的大致过程。
好了,我们先来看看模糊测试的对象:它接收Zlib压缩的数据,并对其进行解压处理,如果非压缩形式的输入中的前两个字节是“F”和“U”,就会崩溃。
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { uint8_t Uncompressed[100]; size_t UncompressedLen = sizeof(Uncompressed); if (Z_OK != uncompress(Uncompressed, &UncompressedLen, Data, Size)) return 0; if (UncompressedLen < 2) return 0; if (Uncompressed[0] == 'F' && Uncompressed[1] == 'U') abort(); // Boom return 0; }
这是一个非常简单的测试目标,但是传统的通用Fuzzer(包括libFuzzer)实际上根本无法发现崩溃点。这是为什么呢?因为它们进行突变处理时,会对压缩数据进行操作,从而导致生成的所有输入对于解压缩都是无效的。
为此,我们必须借助于自定义mutator(又名libFuzzer插件)。所谓自定义mutator,其实就是具有固定签名的用户自定义函数,其执行以下操作:
·根据指定的语言语法来分析输入数据(在我们的示例中,它会对压缩数据进行解压处理)。如果解析失败,它将返回符合语法的伪输入(这里,它将返回一个压缩字节序列Hi)。
· 让输入(在本例中为未压缩的原始数据)的解析表示形式发生突变。自定义mutator可以通过函数LLVMFuzzerMutate请求libFuzzer对原始数据的某些部分进行赋值。
· 对突变后的表示形式进行序列化(在我们的示例中,就是进行压缩处理)。
extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size, size_t MaxSize, unsigned int Seed) { uint8_t Uncompressed[100]; size_t UncompressedLen = sizeof(Uncompressed); size_t CompressedLen = MaxSize; if (Z_OK != uncompress(Uncompressed, &UncompressedLen, Data, Size)) { // The data didn't uncompress. Return a dummy... } UncompressedLen = LLVMFuzzerMutate(Uncompressed, UncompressedLen, sizeof(Uncompressed)); if (Z_OK != compress(Data, &CompressedLen, Uncompressed, UncompressedLen)) return 0; return CompressedLen; }
现在,请运行该 示例 。首先,让我们在不使用自定义mutator的情况下编译该测试对象:
% clang -O -g CompressedTest.cpp -fsanitize=fuzzer -lz % ./a.out ... INFO: A corpus is not provided, starting from an empty corpus #2 INITED cov: 2 ft: 3 corp: 1/1b lim: 4 exec/s: 0 rss: 25Mb #2097152 pulse cov: 2 ft: 3 corp: 1/1b lim: 4096 exec/s: 1048576 rss: 25Mb #4194304 pulse cov: 2 ft: 3 corp: 1/1b lim: 4096 exec/s: 1048576 rss: 25Mb #8388608 pulse cov: 2 ft: 3 corp: 1/1b lim: 4096 exec/s: 1198372 rss: 26Mb #16777216 pulse cov: 2 ft: 3 corp: 1/1b lim: 4096 exec/s: 1290555 rss: 26Mb #33554432 pulse cov: 2 ft: 3 corp: 1/1b lim: 4096 exec/s: 1342177 rss: 26Mb #67108864 pulse cov: 2 ft: 3 corp: 1/1b lim: 4096 exec/s: 1398101 rss: 26Mb ...
如您所见,这里的覆盖率( cov:2)并不会增加,因为目标中没有执行新的检测代码。即便使用Zlib,在模糊测试期间提供更多的覆盖率反馈,libFuzzer也不太可能发现崩溃点。
下面,再次运行该测试目标代码,但这次使用自定义mutator:
% clang -O -g CompressedTest.cpp -fsanitize=fuzzer -lz -DCUSTOM_MUTATOR % ./a.out ... INFO: A corpus is not provided, starting from an empty corpus #2 INITED cov: 2 ft: 3 corp: 1/1b lim: 4 exec/s: 0 rss: 25Mb #512 pulse cov: 2 ft: 3 corp: 1/1b lim: 8 exec/s: 256 rss: 26Mb #713 NEW cov: 3 ft: 4 corp: 2/11b lim: 11 exec/s: 237 rss: 26Mb L: 10/10 MS: 1 Custom- #740 NEW cov: 4 ft: 5 corp: 3/20b lim: 11 exec/s: 246 rss: 26Mb L: 9/10 MS: 3 Custom-EraseBytes-Cus #1024 pulse cov: 4 ft: 5 corp: 3/20b lim: 11 exec/s: 341 rss: 26Mb #2048 pulse cov: 4 ft: 5 corp: 3/20b lim: 21 exec/s: 682 rss: 26Mb #4096 pulse cov: 4 ft: 5 corp: 3/20b lim: 43 exec/s: 1365 rss: 26Mb #4548 NEW cov: 5 ft: 6 corp: 4/30b lim: 48 exec/s: 1516 rss: 26Mb L: 10/10 MS: 6 ShuffleBytes-Custom #8192 pulse cov: 5 ft: 6 corp: 4/30b lim: 80 exec/s: 2730 rss: 26Mb #16384 pulse cov: 5 ft: 6 corp: 4/30b lim: 163 exec/s: 5461 rss: 26Mb ==157112== ERROR: libFuzzer: deadly signal... #7 0x4b024b in LLVMFuzzerTestOneInput CompressedTest.cpp:23:5
在这里,目标函数(LLVMFuzzerTestOneInput)接收的每个输入都是有效的压缩数据并成功地进行了解压缩。通过这种简单的改造,libFuzzer原来的突变处理将变得更加有效,这样就能找到崩溃点了。
示例:PNG
PNG 是一种光栅图形文件格式。PNG文件是由一系列的length-tag-value-checksum块组成的。由于以下原因,这种数据格式对于非专用的基于突变的模糊测试引擎来说是一个挑战:
每个块都有一个长度值,因此,突变处理增加块大小后,也需要修改对应的长度值。
某些块包含Zlib压缩数据,并且,同一压缩数据流中可能含有多个IDAT块。
下面是 libpng 的模糊测试目标的示例。当禁用CRC校验并提供一个全面的种子语料库时,非专用的模糊器对于这个目标来说还是比较有效的。但是,如果使用带有自定义mutator的libFuzzer的话,效果更佳。这里的示例mutator将PNG文件解析为内存中的数据结构,对其进行赋值,并将经过突变处理后的数据序列化为PNG格式。
这个自定义mutator还会进行一些其他方面的处理:随机插入一个特殊的fUZz块,模糊测试目标稍后会对其执行额外的突变处理,以提供更大的覆盖范围。
与没有自定义mutator的相同模糊测试目标相比,这里生成的Fuzzer即使从空语料库开始,也能获得更高的覆盖率;而没有使用自定义mutator的Fuzzer,即使使用了良好的种子语料库并进行更多次数的迭代,也达不到这样的覆盖率!
示例:Protocol Buffer
对于接口定义语言(Interface Definition Languages,IDL),如 Protocol Buffers (又称protobufs)、 Mojo 、 FIDL 或 Thrift 等,其输入类型都具有高度结构化特征,这些类型的语言很难用普通的突变型Fuzzer进行模糊测试。
使用libFuzzer时,可以借助自定义mutator对IDL进行结构敏感型的模糊测试。实际上,人们已经为protobufs语言实现了一个这种类型的mutator,即 libprotobuf-mutator (也称为LPM)。
下面,我们给出一些proto定义和相应的模糊测试目标的 例子 。
message Msg { optional float optional_float = 1; optional uint64 optional_uint64 = 2; optional string optional_string = 3; } DEFINE_PROTO_FUZZER(const libfuzzer_example::Msg& message) { // Emulate a bug. if (message.optional_string() == "FooBar" && message.optional_uint64() > 100 && !std::isnan(message.optional_float()) && std::fabs(message.optional_float()) > 1000 && std::fabs(message.optional_float()) < 1E10) { abort(); } }
在这里,如果该消息的3个字段中的内容为特定的值,则会发生崩溃。
注意,LPM提供了一个宏define_proto_fuzzer,用于定义直接使用protobuf消息的模糊测试目标。
下面是通过libFuzzer和LPM对基于protobuf的API进行模糊测试的一些例子:
·使用 config_fuzz_test 对Envoy的配置API进行模糊测试。
· TODO
作为中间格式的Protocol Buffer
Protobufs为结构化数据的序列化处理提供了一种非常方便的方法,而LPM则提供了一种简单的方法,可用于对protobufs进行突变处理,以实现结构敏感型的模糊测试。因此,对于使用结构化数据而非protobufs的API来说,libFuzzer+LPM乃是上上之选。
使用LPM对数据格式Foo进行模糊测试时,需要执行以下步骤:
·将Foo表示为一个protobuf消息,例如FooProto。当然,实现从Foo到protobufs的精确映射难度太大,因此,只要求FooProto描述Foo的超集的子集即可。
· 实现FooProto=>Foo转换器。
· 实现Foo=>FooProto转换器,这是可选的。如果需要使用大量的Foo输入的话,最好实现这个转换器。
下面我们将讨论这种方法的几个实例。
示例:SQLite
在Chromium中,SQLite数据库库支持许多功能,包括WebSQL,它将 SQLite 暴露给任意网站,使SQLite成为恶意网站的目标。由于SQLite使用的是高度结构化的基于文本的 SQL 语言,所以,它是结构敏感的模糊测试的理想选择。此外,SQLite对其使用的语言也提供了非常好的 描述 。
第一步,是将SQLite的语法转换为protobuf格式,以便在 Chromium源代码树 中查看。为简单起见,这里只对CREATE TABLE sql语句进行模糊测试,为此,可以定义如下所示的protobuf语法:
message SQLQueries { repeated CreateTable queries = 1; } message CreateTable { optional TempModifier temp_table = 1; required Table table = 2; required ColumnDef col_def = 3; repeated ColumnDef extra_col_defs = 4; repeated TableConstraint table_constraints = 5; required bool without_rowid = 6; } // Further definitions of TempModifier, Table, ColumnDef, and TableConstraint.
然后,编写将结构化protobufs转换为文本型SQL查询所需的C++代码(完整版本,请参阅相应的Chromium源代码树):
// Converters for TempModifier, Table, ColumnDef, and TableConstraint go here. std::string CreateTableToString(const CreateTable& ct) { std::string ret("CREATE TABLE "); if (ct.has_temp_table()) { ret += TempModifierToString(ct.temp_table()); ret += " "; } ret += TableToString(ct.table()); ret += "("; ret += ColumnDefToString(ct.col_def()); for (int i = 0; i < ct.extra_col_defs_size(); i++) { ret += ", "; ret += ColumnDefToString(ct.extra_col_defs(i)); } for (int i = 0; i < ct.table_constraints_size(); i++) { ret += ", "; ret += TableConstraintToString(ct.table_constraints(i)); } ret += ") "; if (ct.without_rowid()) ret += "WITHOUT ROWID "; return ret; } std::string SQLQueriesToString(const SQLQueries& queries) { std::string queries; for (int i = 0; i < queries.queries_size(); i++) { queries += CreateTableToString(queries.queries(i)); queries += ";\n"; } return queries; }
最后,给出模糊测试目标的相关代码:
DEFINE_BINARY_PROTO_FUZZER(const SQLQueries& sql_queries) { std::string queries = SQLQueriesToString(sql_queries); sql_fuzzer::RunSQLQueries(SQLQueriesToString(queries)); // Helper that passes our queries to sqlite library to execute }
幸运的是,libFuzzer和LPM将创建许多有趣的CREATE TABLE语句,并且,不同的数据表具有不同数量的列、表约束和其他属性。SQLQueries的这个基本定义可以进行扩展,以便可以与INSERT或SELECT等其他SQL语句一起使用,并且让这些语句从随机CREATE TABLE语句创建的表中执行选择或插入操作。如果不定义这个protobuf结构的话,Fuzzer很难得到用于创建表的合法(即不会产生解析错误的)CREATE TABLE语句——尤其是创建具有有效表约束的数据表的时候。
(未完待续)
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 通过结构化异常处理绕过CFG
- Golang 通过反射的方式调用结构体方法
- 数据结构是噩梦?想要通过面试,你必须掌握它
- 通过libFuzzer实现结构敏感型的模糊测试技术(下)
- Python cookbook(数据结构与算法)通过公共键对字典列表排序算法示例
- 无需理解数据结构也不需编程技能 Tableau如何通过数据问答再降低数据使用门槛
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
UNIX 时间戳转换
UNIX 时间戳转换
正则表达式在线测试
正则表达式在线测试