本系列是学习《Fuzzing101 with LibAFL》系列博客(后文统称:原博客)的笔记分享,在学习介绍 LibAFL 用法的同时总结 Rust 知识点。
前置知识: fuzz基本概念、AFL基本使用
本篇要点:
- LibAFL
- 9大组件
- Forkserver模式:简单组装
- Rust
- Cargo 基本使用
- 变量所有权:
- 转移(move) 和 拷贝(copy)
- 引用(reference) 和 借用(borrowing)
是不是搞fuzz的都在搞rust
Fuzzing101是大名鼎鼎的 Fuzzing 入门教程。👴之前搞过一点,故这次整点花活。看到有个叫epi052的大佬用 LibAFL 做 Fuzzing101,于是👴也跟着学一波rust。
LibAFL是用 Rust 写的 Fuzzing 框架,主要贡献在于给出了一套 Fuzzer 的标准化定义,以此试图改善当今 Fuzzing 研究界成果倍出但互不兼容,经常重复造轮子的现象。
LibAFL 隶属于 AFL++ 项目组,故需要先配置 AFL++ 。而 AFL++ 的官方 Docker 镜像就包含了 Rust 环境,所以使用 Docker 配 LibAFL 环境就只需一句话:
|
|
Execise-1 xpdf
练习一主要是熟悉基本流程。使用AFL主要有以下步骤:
- 目标编译插桩。如:
|
|
- 语料库准备
- 执行afl-fuzz
Let’s see 如何用Rust完成上述步骤。
Rust 基础之cargo
在 Rust 中,可以用rustc
编译单个文件,更常见的是使用包管理器cargo
。
Cargo.toml
运行cargo new
创建一个 package ,其中必有Cargo.toml
,描述 package 如何构建。
|
|
本项目首先规定了构建脚本为build.rs
,然后引入了 libafl 依赖。
运行cargo build
时,Cargo 会自动处理依赖。(包括 crates.io 下载安装,我觉得这就是现代语言的一种自信)
build.rs
构建脚本是为了方便 Rust 项目与第三方工具的集成。比如上文中目标编译的几条指令,就可以用 Rust std 库中的Command类来执行。
|
|
使用构建脚本还有许多好处,可以利用 Cargo 采取更灵活的构建策略。构建脚本的输出可以被 Cargo 解释,只需打印以cargo:
开头的指令。如下面两行输出向 Cargo 表明仅在这两个文件发生改动时执行构建脚本。
|
|
LibAFL 组装
上述还是甜点,下面进入正菜环节。 LibAFL 将 Fuzzer 定义为9个组件,分别是:
摘自这篇博客。
- Input:程序的输入。
- 重点是格式,最常见的就是 byte array,也有AST等。
- Corpus:输入和其附属元数据的存储。
- 存储有位于内存和位于硬盘两种,后者更广泛。输入也可分为有助于进化的 interesting testcase 和最终触发 crash 的 solution。
- Scheduler:从 corpus 中选取 testcase 的调度策略。
- 最朴素的即先进先出或随机选择,也可引入优先级算法。
- Stage:定义对 testcase 进行的操作(action)。
- 往往会进行多阶段的操作。如 AFL 中的 random havoc stage。
- Observer:提供一次执行目标程序的信息。
- 常用的 coverage map 就是一种 observer。
- Executor:用 fuzzer 的输入来执行目标程序。
- 不同 fuzzer 在这方面区别很大。
- Feedback:将程序执行的结果分类以决定是否将其加入 corpus。
- feedback 通常处理一个或多个 observer 报告的信息来判断 execution 是否 “interesting”,是否是满足条件的 solution,比如可观测的 crash。
- Mutator:从一个或多个输入生成新的 testcase。
- 通常是最常改动的,不同 mutator 可以组合,往往还和特定的输入类型绑定。
- Generator:凭空产生新的输入。
- 有随机生成的,也有 Nautilus 这种基于语法的。
除此之外,在 LibAFL 实现中还有若干重要组件:
API文档
请 参阅
- State: 包含了运行时的所有元数据,包括 Corpus、RNG 等。
- Bolts: 工具库,实现了诸如共享内存的支持。
- Monitor: 向用户打印log之类。
- Events: 组件之间通信
- Fuzzer: 顶层组件,把一切组织起来
因原博客的讲解很详细,且提供了完整代码。故本文试图切换视角,从自顶向下的角度拆解代码:
组件:Fuzzer
|
|
可以看到 StdFuzzer
串联起了全部组件。mgr
、scheduler
、mutator
、stages
都是使用库自带的类,固省略之,下面详述复杂些的组件。
组件:State
|
|
查阅文档可知StdState
的成员含义,这些成员就是Fuzzer的全部状态了。一个可能的疑惑是为什么会有feedback
、objective
两种 Feedback,让我们继续向上检阅代码。
|
|
Rust 基础之所有权
所有权,是 Rust 特有的设计,在无GC的前提下实现内存安全与高性能。基本规则:
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
基于这些原则,当一个值被另一个变量使用时,会发生所有权的转移,先前的变量便不能访问该值。
|
|
|
|
|
|
- 转移(move) 和 拷贝(copy)
let s2 = s1;
在 Rust 中被称为变量绑定,代表s1被移动到了s2。
如果想要访问相同值,需要对变量进行拷贝。在上面的例子中,int类型没有拷贝也不报错,这是因为基本类型的大小是已知的,且分配在栈上,对他的拷贝比较简单,故 Rust 自动实现了拷贝。而String是复杂类型,必须显式的拷贝。
- 引用(reference) 和 借用(borrowing)
将值在变量之间传来传去确实比较麻烦。 Rust实现了2种引用:
&
: 不可变引用,允许使用值但不获取所有权。可以有多个。&mut
: 可变引用。仅能存在一个。- 可变引用与不可变引用不能同时存在。
获取变量的引用,就称为借用。显然,在离开作用域后所有权将被归还。引用的作用域从创建一直持续到它最后一次使用,而变量的作用域从创建持续到某一个花括号}
。
对引用有效性的检查是 Rust 解决数据竞争,悬垂指针等安全问题的重要机制。
在本项目中,需要feedback
和objective
随着state
更新而不断积累,因此使用可变引用。而其他成员和state
相关,故直接将所有权移交给state
。
参考文档:https://course.rs/basic/ownership/ownership.html
组件:Observer & Feedback
|
|
如定义所言,Observer 仅提供信息,Feedback 将观察到的信息进行判断。
在本例中,首先定义的两个Observer分别提供了代码运行时间和便覆盖率的信息。
然后分布对他们进行了不同的反馈判断。feedback_or
和feedback_and_fast
是逻辑判断宏。因此可以解读他们的逻辑:
feedback
的条件是覆盖了新的分支 或 “运行时间反馈”。故反馈的是有趣的样例(interesting testcase)- 实际上,“运行时间反馈”永远不会真的反馈,需要配合其他反馈使用。这里仅作示例。
objective
的条件是覆盖了新的分支 且 运行超时。故反馈的是能触发无限递归漏洞的样例,即我们想要的结果(solution)
组件:Executor
|
|
我们的 Executor 还是采用经典的 forkserver 架构,构建 Executor 时,首先加入了两个 Observer ,然后指定了超时时间,有助于增加fuzz吞吐量。
跑🏃
总之借助 LibAFL 的框架,写一个 Fuzzer 还是很清晰的。(或许若干年后的 Fuzzing 学习者就不用啃AFL的1000行源码了😭)
最后跑出来一个死循环。👴宣布实验一完成。
|
|