最近遇到 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: 同步完毕

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

  1. AFL++ 会检查 out 目录下的文件夹(排除 AFL++ 本身的 default 文件夹)
  2. 如果文件夹中没有 queue 子目录,转 1
  3. 如果文件夹中有 queue,则遍历 queue 下的 corpus
  4. 如果 corpus 是处理过的(根据文件名 id 判断),转 4
  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 的源码就明白了,源码面前,了无秘密。