最近遇到 AFL++ 同步 interesting corpus 失败的问题,不得不嗑一嗑 AFL++ 的源码了,该来的躲不掉,虽然 C 代码让我有点头大。

一、同步机制

常规情况下 AFL++ 是以单实例运行的。三个臭皮匠顶个诸葛亮,多开几个 AFL++ 实例一起跑是可行的,AFL++ 和其他的 Fuzz 工具(LibFuzzer、HonggFuzz)一起跑也是可以的。

通过afl-fuzz -h可以看到 AFL++ 提供了相关的命令参数:

Other stuff:

 -M/-S id - distributed mode (-M sets -Z and disables trimming)

      see docs/fuzzing_in_depth.md#c-using-multiple-cores

      for effective recommendations for parallel fuzzing.

 -F path - sync to a foreign fuzzer queue directory (requires -M, can

      be specified up to 32 times)

简而言之,AFL++ 同步相关的几个参数如下:

  • -M:指定 AFL++ 主实例。主实例会从 -S 指定的从实例、-F 指定的其他 Fuzz 中同步有趣的测试用例。主实例只有一个,负责管理所有的测试用例。
  • -S:指定 AFL++ 从实例。从实例产生的有趣测试用例将被同步到 -M 指定的主实例中,从实例也会从 -M 指定的主实例中同步有趣的测试用例。
  • -F:指定其他 Fuzz 实例的 corpus 目录。由其他 Fuzz 产生的有趣的测试用例,将会被同步到 -M 指定的主实例中。

可以看到主实例位于多实例运行模式的核心地位,它负责汇总和分发 AFL++ 的测试用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  +---------+       +---------+
| Fuzzer 1| | Fuzzer 2|
| (F) | | (F) |
+----+----+ +----+----+
| |
v v
+------+-----------------+------+
| Master (M) |
+-----+---------------+---------+
^ | ^ |
| v | v
+---------+ +---------+
| Slave 1 | | Slave 2 |
| (S) | | (S) |
+---------+ +---------+

实际使用时,执行命令参考如下:

1
2
3
afl-fuzz -i inputs -o outputs -M master -F /path/to/foreign_fuzzer1/queue -F /path/to/foreign_fuzzer2/queue -- ./target
afl-fuzz -i inputs -o outputs -S slave1 -- ./target
afl-fuzz -i inputs -o outputs -S slave2 -- ./target

二、源码分析

AFL++ 的源码入口为afl-fuzz.cmain函数。AFL++ 从其他 fuzzer 中同步 corpus,是通过sync_fuzzers函数来实现的。

main函数中,可以看到对sync_fuzzers函数的调用。一路跟踪下来,代码调用关系参考如下时序图:

sequenceDiagram
participant main
participant sync_fuzzers
participant read_foreign_testcases
participant update_sync_time
participant scandir
participant fuzz_run_target
participant save_if_interesting
participant has_new_bits
participant describe_op
participant ck_write
participant add_to_queue

main->>sync_fuzzers: 开始同步
loop 遍历AFL++的out目录中的子目录d_name
    sync_fuzzers->>update_sync_time: 更新同步时间
    sync_fuzzers->>scandir: 读取d_name/queue目录中的corpus列表
    loop 遍历新增的corpus(通过corpus的id号来识别)
        sync_fuzzers->>fuzz_run_target: 用corpus作为输入运行一次target
        fuzz_run_target-->>sync_fuzzers: 返回target运行结果
        sync_fuzzers->>save_if_interesting: 检查并保存corpus
        save_if_interesting->>has_new_bits: 检查corpus是否为interesting
        has_new_bits-->>save_if_interesting: 返回检查结果
        alt 如果corpus是interesting
            save_if_interesting->>describe_op: 为corpus创建文件名
            describe_op-->>save_if_interesting: 返回文件名
            save_if_interesting-->>ck_write: 将interesting的corpus保存到AFL++的queue目录中
            save_if_interesting->>add_to_queue: 将interesting的corpus添加到AFL++的队列中
        end
    end
end
alt 如果指定了其他Fuzz的corpus目录
    sync_fuzzers->>read_foreign_testcases: 开始同步其他Fuzz的corpus
    loop 遍历其他Fuzz的corpus目录
        read_foreign_testcases->>scandir: 读取其他Fuzz的corpus列表
        loop 遍历其他Fuzz的corpus
            read_foreign_testcases->>fuzz_run_target: 用corpus作为输入运行一次target
            fuzz_run_target-->>read_foreign_testcases: 返回target运行结果
            read_foreign_testcases->>save_if_interesting: 检查corpus是否为interesting
            save_if_interesting->>has_new_bits: 检查corpus是否为interesting
            has_new_bits-->>save_if_interesting: 返回检查结果
            alt 如果corpus是interesting
                save_if_interesting->>describe_op: 为corpus创建文件名
                describe_op-->>save_if_interesting: 返回文件名
                save_if_interesting-->>ck_write: 将interesting的corpus保存到AFL++的queue目录中
                save_if_interesting->>add_to_queue: 将interesting的corpus添加到AFL++的队列中
            end
        end
    end
    read_foreign_testcases-->>sync_fuzzers: 同步其他Fuzz的corpus结束
end
sync_fuzzers-->>main: 同步完毕</pre>

简而言之,主实例同步从实例过程是这样的:

  1. AFL++ 会检查 out 目录下的文件夹(排除 AFL++ 本身的 default 文件夹)
  2. 如果文件夹中没有 queue 子目录,转 1
  3. 如果文件夹中有 queue,则遍历 queue 下的 corpus
  4. 如果 corpus 是处理过的(根据文件名 id 判断),转 3
  5. 如果 corpus 是没有处理过的,那就用这个 corpus 作为输入,执行一次 target
  6. 分析 target 执行结果,判断 corpus 是否为 interesting,如果不是 interesting,转 3
  7. 如果 corpus 是 interesting,则将 corpus 保存为 out/default/queue 目录下的文件,并将 corpus 添加到 AFL++ 的内存队列中,再转 3

主实例同步其他 Fuzz 的过程与之类似,只是每次同步时,并不会根据 corpus 文件名的 id 编号过滤已经分析过的 corpus,因此从外部 Fuzz 同步 corpus 的性能较差。

三、实践应用

如果某一款工具能生成 corpus,但是希望以从实例的方式被主实例同步,需要保证这款工具产生的 corpus 的命名符合以下要求:

  1. id:开头,然后是高位补 0 的六位数字
  2. 第一个 corpus 的 id 从 0 开始,随后的 corpus 依次以 1 递增

其原因读一遍sync_fuzzers的源码就明白了,源码面前,了无秘密。