一、案发现场

事情是酱滴:

我写了个 python 脚本,在脚本中通过 subprocess 启动了 AFL++ 的子进程,对 binary 进行测试。

然后我还在这个 python 脚本中,使用其他的工具生成针对 binary 有效的 corpus。

参考前文AFL++ 同步机制,正常情况下 AFL++ 会将其他工具生成的 corpus(已确认能覆盖新的代码分支),同步到 AFL++ 的 queue 目录下。

但是诡异的事情出现了:AFL++ 没有同步!

二、破案过程

1. 还原现场
这个 AFL++ 不同步的情况不是必现的,非必现的问题真 TM 难调。

我换了好几个机器,在 host 和 docker 上分别进行了多轮实验,前前后后跑了一两周的时间,排除了环境问题。虽然有的环境概率高,有的环境概率低,但只要跑的次数够多,都会出现 AFL++ 不同步的问题。

2. 埋 log
在反复跑实验的过程中,也有在 AFL++ 源码中埋 log。

我在 sync_fuzzers 函数体内,以及调用之处加了很多 log。sync_fuzzersafl-fuzz.cmain函数中有四处调用,编号 1~4 表示在代码中的先后顺序,实际调用流程大概如下:

flowchart TD
    A[main] --> B[stop_fuzzing]
    B --> C[sync_fuzzers 4]
    A --> D[while loop]
    D --> E[sync_fuzzers 1]
    E --> F[do-while loop]
    F --> G[sync_fuzzers 2]
    F --> H[sync_fuzzers 3]
    H --> D
    G --> D

通过 log 发现,AFL++ 在同步 corpus 时,主要使用过 sync_fuzzers 1 来完成的。而在未同步的场景中,log 会在 do-while loop 里面中断,后续再也没有打印出sync_fuzzers 1

3. gdb 追踪
当 log 也印不出来的时候,gdb 看起来是唯一能用的工具了。

于是我找了个复现概率高的机器,在启动 python 脚本后,用 gdb attach 脚本所启动的 AFL++ 子进程,并在 sync_fuzzers 的 1~4 处调用都打了断点,接着让 AFL++ 继续跑。

神奇的事情出现了,AFL++ 正常同步的时候,sync_fuzzers 1总是会命中。然而当跑一阵后,sync_fuzzers 1就再也到不了了。此时 ctrl+c 暂停程序,查看调用栈如下:

其中 afl-fuzz-one.c 的 420 行的代码如下,只是调用了 flush 库函数而已:

我将此行注释掉,重新 gdb 一次,结果发现会在另一个 flush 处卡住。

在库函数面前,我们要质疑的是自己的代码。

三、罪魁祸首

我回头检查了自己的 Python 脚本,启动 AFL++ 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
self.fuzzer_popen = sp.Popen(
cmd,
shell=False,
stdout=sp.PIPE,
stderr=sp.PIPE,
env=env,
preexec_fn=os.setsid,
)
time.sleep(5)
if self.fuzzer_popen.poll() is not None:
raise Exception(f'start fuzz process failed with error: {self.fuzzer_popen.stderr.read().decode("utf-8")}')
else:
logger.info(f'start fuzz process whose pid is {self.fuzzer_popen.pid}')

我又去查了一下 subprocess 的官方文档,赫然写着 注意事项

Note This will deadlock when using stdout=PIPE or stderr=PIPE and the child process generates enough output to a pipe such that it blocks waiting for the OS pipe buffer to accept more data. Use Popen.communicate() when using pipes to avoid that.

破案了破案了,我用了 PIPE 来接受 AFL++ 的 stdout 和 stderr,但是我没有通过 communicate 来读取 PIPE,导致PIPE 中积压了大量的数据,阻碍了 AFL++ 继续flush

我被自己蠢哭了,也被 subprocess 坑惨了,修改后的代码如下:

1
2
3
4
5
6
7
8
self.fuzzer_popen = sp.Popen(
cmd,
shell=False,
stdout=sp.DEVNULL,
stderr=sp.DEVNULL,
env=env,
preexec_fn=os.setsid,
)

去他娘的 stdout 和 stderr,改完后 AFL++ 同步都正常了~~