Featured image of post libAFL速通Fuzzing101 (1)

libAFL速通Fuzzing101 (1)

本系列是学习《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 环境就只需一句话:

1
2
docker pull aflplusplus/aflplusplus
docker run -ti -v ./Fuzzing101:/fuzzing101 aflplusplus/aflplusplus

Execise-1 xpdf

练习一主要是熟悉基本流程。使用AFL主要有以下步骤:

  • 目标编译插桩。如:
1
2
3
4
export LLVM_CONFIG=llvm-config-15
CC=afl-clang-fast CXX=afl-clang-fast++ ./configure --prefix=./install
make
make install
  • 语料库准备
  • 执行afl-fuzz

Let’s see 如何用Rust完成上述步骤。

Rust 基础之cargo

在 Rust 中,可以用rustc编译单个文件,更常见的是使用包管理器cargo

Cargo.toml

运行cargo new创建一个 package ,其中必有Cargo.toml,描述 package 如何构建。

1
2
3
4
5
6
7
8
9
# execise-1/Cargo.toml
[package]
name = "exercise-1"
version = "0.1.0"
edition = "2021"
build = "build.rs"

[dependencies]
libafl = "0.10.1"

本项目首先规定了构建脚本为build.rs,然后引入了 libafl 依赖。 运行cargo build时,Cargo 会自动处理依赖。(包括 crates.io 下载安装,我觉得这就是现代语言的一种自信)

build.rs

构建脚本是为了方便 Rust 项目与第三方工具的集成。比如上文中目标编译的几条指令,就可以用 Rust std 库中的Command类来执行。

1
2
3
4
5
6
// make clean; 
Command::new("make")
    .arg("clean")
    .current_dir(xpdf_dir.clone())
    .status()
    .expect("Couldn't clean xpdf directory");

使用构建脚本还有许多好处,可以利用 Cargo 采取更灵活的构建策略。构建脚本的输出可以被 Cargo 解释,只需打印以cargo: 开头的指令。如下面两行输出向 Cargo 表明仅在这两个文件发生改动时执行构建脚本。

1
2
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=src/main.rs");

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

1
2
3
4
5
6
7
8
    let monitor = SimpleMonitor::new(|s| println!("{s}"));
    let mut mgr = SimpleEventManager::new(monitor);
    let scheduler = IndexesLenTimeMinimizerScheduler::new(QueueScheduler::new());
    let mutator = StdScheduledMutator::new(havoc_mutations());
    let mut stages = tuple_list!(StdMutationalStage::new(mutator));
    -------------8<-------------
    let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
    fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?;

可以看到 StdFuzzer 串联起了全部组件。mgrschedulermutatorstages 都是使用库自带的类,固省略之,下面详述复杂些的组件。

组件:State

1
2
3
4
5
6
7
    let mut state = StdState::new(
        StdRand::with_seed(current_nanos()),
        input_corpus,
        timeouts_corpus,
        &mut feedback,
        &mut objective,
    )?;

查阅文档可知StdState的成员含义,这些成员就是Fuzzer的全部状态了。一个可能的疑惑是为什么会有feedbackobjective两种 Feedback,让我们继续向上检阅代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pub fn new<F, O>(
    rand: R,
    corpus: C,
    solutions: SC,
    feedback: &mut F,
    objective: &mut O
) -> Result<Self, Error>
where
    F: Feedback<Self>,
    O: Feedback<Self>,

Rust 基础之所有权

所有权,是 Rust 特有的设计,在无GC的前提下实现内存安全与高性能。基本规则:

  1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

基于这些原则,当一个值被另一个变量使用时,会发生所有权的转移,先前的变量便不能访问该值。

1
2
3
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 报错!
1
2
3
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}, world!", s1); // 不报错
1
2
3
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // 不报错
  • 转移(move) 和 拷贝(copy)

let s2 = s1;在 Rust 中被称为变量绑定,代表s1被移动到了s2。

如果想要访问相同值,需要对变量进行拷贝。在上面的例子中,int类型没有拷贝也不报错,这是因为基本类型的大小是已知的,且分配在栈上,对他的拷贝比较简单,故 Rust 自动实现了拷贝。而String是复杂类型,必须显式的拷贝。

  • 引用(reference) 和 借用(borrowing)

将值在变量之间传来传去确实比较麻烦。 Rust实现了2种引用:

  • &: 不可变引用,允许使用值但不获取所有权。可以有多个。
  • &mut: 可变引用。仅能存在一个。
  • 可变引用与不可变引用不能同时存在。

获取变量的引用,就称为借用。显然,在离开作用域后所有权将被归还。引用的作用域从创建一直持续到它最后一次使用,而变量的作用域从创建持续到某一个花括号}

对引用有效性的检查是 Rust 解决数据竞争,悬垂指针等安全问题的重要机制。

在本项目中,需要feedbackobjective随着state更新而不断积累,因此使用可变引用。而其他成员和state相关,故直接将所有权移交给state

参考文档:https://course.rs/basic/ownership/ownership.html

组件:Observer & Feedback

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    let time_observer = TimeObserver::new("time");
    
    let edges_observer = unsafe { HitcountsMapObserver::new(StdMapObserver::new("shared_mem", shmem_buf)) };

    let mut feedback = feedback_or!(
        MaxMapFeedback::tracking(&edges_observer, true, false),
        TimeFeedback::with_observer(&time_observer)
    );
    let mut objective =
        feedback_and_fast!(
            TimeoutFeedback::new(), 
            MaxMapFeedback::new(&edges_observer)
            );

如定义所言,Observer 仅提供信息,Feedback 将观察到的信息进行判断。 在本例中,首先定义的两个Observer分别提供了代码运行时间和便覆盖率的信息。 然后分布对他们进行了不同的反馈判断。feedback_orfeedback_and_fast是逻辑判断宏。因此可以解读他们的逻辑:

  • feedback的条件是覆盖了新的分支 或 “运行时间反馈”。故反馈的是有趣的样例(interesting testcase)
    • 实际上,“运行时间反馈”永远不会真的反馈,需要配合其他反馈使用。这里仅作示例。
  • objective的条件是覆盖了新的分支 且 运行超时。故反馈的是能触发无限递归漏洞的样例,即我们想要的结果(solution)

组件:Executor

1
2
3
4
5
6
7
    let fork_server = ForkserverExecutor::builder()
        .program("./xpdf/install/bin/pdftotext")
        .parse_afl_cmdline(["@@"])
        .coverage_map_size(MAP_SIZE)
        .build(tuple_list!(time_observer, edges_observer))?;
    let timeout = Duration::from_secs(5);
    let mut executor = TimeoutForkserverExecutor::new(fork_server, timeout)?;

我们的 Executor 还是采用经典的 forkserver 架构,构建 Executor 时,首先加入了两个 Observer ,然后指定了超时时间,有助于增加fuzz吞吐量。

跑🏃‍

总之借助 LibAFL 的框架,写一个 Fuzzer 还是很清晰的。(或许若干年后的 Fuzzing 学习者就不用啃AFL的1000行源码了😭)

最后跑出来一个死循环。👴宣布实验一完成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[AFL++ 798a8ebfa8a4] /fuzzing101/exercise-1 # ./xpdf/install/bin/pdftotext ./timeouts/c497979e26a808e9
Error: PDF file is damaged - attempting to reconstruct xref table...
Error (2459): Dictionary key must be a name object
Error (2465): Illegal character '>'
Error (2468): Dictionary key must be a name object
Error (2471): Dictionary key must be a name object
Error (2486): Dictionary key must be a name object
Error (2488): Illegal character <2f> in hex string
Error (2489): Illegal character <78> in hex string
Error (2490): Illegal character <6d> in hex string
Error (2491): Illegal character <70> in hex string
Error (2492): Illegal character <3a> in hex string
Error (2493): Illegal character <4d> in hex string
Error (2494): Illegal character <6f> in hex string
Error (2496): Illegal character <69> in hex string
Error (2498): Illegal character <79> in hex string
Error (2501): Illegal character <74> in hex string
Error (2503): Dictionary key must be a name object
-------------8<-------------
Built with Hugo
Theme Stack designed by Jimmy