由 subprocess.PIPE 引发的血案
一、案发现场
事情是酱滴:
我写了个 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_fuzzers
在 afl-fuzz.c
的main
函数中有四处调用,编号 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 | self.fuzzer_popen = sp.Popen( |
我又去查了一下 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 | self.fuzzer_popen = sp.Popen( |
去他娘的 stdout 和 stderr,改完后 AFL++ 同步都正常了~~