本站在去年连载过《AFL 源码阅读》系列文章,其中第七篇提到了如何修改 AFL,以统计用例长度、添加变异算子、追踪 virgin_bits
变化。现在新写一篇文章,是因为在做毕业论文的过程中,发现有些值得跟读者讨论的内容在《源码阅读》第七篇中没有覆盖到。下文中将列举这些经验。
本文是写给准备修改原生 AFL 的、无经验的读者。下面将介绍如何往 AFL 中引入代码,以及如何做出一些适合用于论文的图片。
如何收集 fuzzer 状态
fuzzer 状态收集(主要是用例数量、crash 数量、变异深度、每秒执行次数等)这个问题,今年三月份曾经有读者问过。AFL 在运行过程中,会间歇性地写入工作目录下的 fuzzer_stats
文件,其内容大致如下:
start_time : 1714488467
last_update : 1714492077
fuzzer_pid : 199728
cycles_done : 9
execs_done : 2404704
execs_per_sec : 665.02
paths_total : 1774
paths_favored : 201
paths_found : 1773
paths_imported : 0
max_depth : 13
cur_path : 295
pending_favs : 0
pending_total : 829
variable_paths : 0
stability : 100.00%
bitmap_cvg : 5.84%
unique_crashes : 0
unique_hangs : 0
last_path : 1714491994
last_crash : 0
last_hang : 0
execs_since_crash : 2404704
exec_timeout : 20
afl_banner : harness
afl_version : 2.57b
target_mode : default
command_line : afl-fuzz -i corpus -o sync -d -- ./harness @@
slowest_exec_ms : 20
peak_rss_mb : 29
因此,我们可以间歇性地读取 fuzzer_stats
以获得当前 fuzzer 状态,然后把信息保存起来,或者传递给日志分析工具。代码写法如下:
from datetime import datetime
from time import sleep
import sqlite3
import traceback
import os
def work():
with sqlite3.connect('cur.db') as db:
file = '....../sync/fuzzer_stats'
with open(file) as f:
r = f.read()
db.execute('INSERT INTO record(info, ts) VALUES (?, ?)', [r, datetime.now()])
with sqlite3.connect('cur.db') as db:
db.execute('CREATE TABLE record (id INTEGER PRIMARY KEY, info TEXT, ts TIMESTAMP)')
db.commit()
while True:
try:
work()
except:
pass
sleep(1)
在 fuzzer 运行之前,先跑起来上述 Python 脚本,即可每秒收集一次信息并保存到 sqlite3 数据库。但是,我们会面临一个小问题:AFL 默认每隔一分钟才更新一次 fuzzer_stats
文件,这个频率不足以我们画出符合学术要求的图。所以,需要修改 AFL 的 config.h
,把 STATS_UPDATE_SEC
从 60 改到 1。
这样,每秒收集一次信息,半个小时就能收集到 1800 个点,足够画出不错的图了。
画图
我们上面的 Python 代码把 fuzzer 状态信息保存到了数据库中,现在可以从数据库读取这些信息并画图。代码如下:
import sqlite3
import matplotlib.pyplot as plt
db = sqlite3.connect('cur.db')
r = []
for idx, raw_info, ts in db.execute('select id, info, ts from record').fetchall():
try:
info = {}
for k, v in [x.split(':') for x in raw_info.splitlines()]:
x, y = k.strip(), v.strip()
try:
y = int(y)
except:
try:
y = float(y)
except:
pass
info[x] = y
info['idx'] = idx
info['ts'] = ts
r.append(info)
except Exception:
pass
x = [t['idx'] for t in r[:600]]
y = [t['paths_total'] for t in r[:600]]
plt.plot(x, y)
需要注意的是,如果是在统计 AFL++ 的信息,则上面代码中的 paths_total
要改成 corpus_count
。代码中使用了 600 个点,结果如下:
如何插入代码
AFL fuzzer 的几乎全部逻辑都实现在 afl-fuzz.c
中,可以选择把代码都插入到其中。但这种实现比较不雅,因为该文件有 8000 多行,容易找不到自己的代码。笔者推荐的做法是将主要的代码放在另外的文件中,在 afl-fuzz.c
引用这些 API。
举个例子。假如我们的改进叫做「nekoafl」,则可以在 afl-fuzz.c
旁边放一个 nekoafl.c
和 nekoafl.h
,在其中实现自己的主要逻辑。相应地,改写 AFL 的 Makefile:
# ...
afl-as: afl-as.c afl-as.h $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
ln -sf afl-as as
# 在此处插入 nekoafl.c
afl-fuzz: afl-fuzz.c nekoafl.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) nekoafl.c $@.c -o $@ $(LDFLAGS)
afl-showmap: afl-showmap.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
# ...
使用 make afl-fuzz
即可编译出我们修改后的 AFL。举个例子,假如我们想在用例加入队列之后,对其进行某些操作,那么我们可以改写 afl-fuzz.c
的 add_to_queue
函数:
/* Append new test case to the queue. */
static void add_to_queue(u8* fname, u32 len, u8 passed_det) {
// 我们插入的逻辑
nekoafl_log("add to queue: %s len = %u passed_det = %u\n", fname, len, passed_det);
struct queue_entry* q = ck_alloc(sizeof(struct queue_entry));
q->fname = fname;
q->len = len;
q->depth = cur_depth + 1;
q->passed_det = passed_det;
// ...
总而言之,我们尽量少改动 afl-fuzz.c
文件。这样不容易破坏 AFL 本身的逻辑,出现问题时方便调试。
插入代码如何传递统计信息
假如我们想要统计改进算法的一些状态信息,那我们就需要在代码中把这些信息传递出来。尽管共享内存、TCP 等方式是可行的,但笔者强烈建议使用文件系统传递这些信息。
实践上可以采用这样的方式:要更新信息时,则写入 /dev/shm/log
;上文的 Python 监控程序增加几行,每次收集信息时,读取一遍 /dev/shm/log
,看看现在的状态。笔者不推荐在 AFL 写入 fuzzer_stats
文件的逻辑中添加代码,尽管这样显得更加优雅,但徒增编程复杂度,没有现实收益。