本站在去年连载过《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。

// ...

/* Fuzzer stats file and plot update intervals (sec): */


#define STATS_UPDATE_SEC    1    // fuzzer_stats 文件的更新间隔
#define PLOT_UPDATE_SEC     5

/* Smoothing divisor for CPU load and exec speed stats (1 - no smoothing). */

#define AVG_SMOOTHING       16
// ...
▲ 修改 config.h 以增加 fuzzer_stats 文件更新频率

  这样,每秒收集一次信息,半个小时就能收集到 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.cnekoafl.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.cadd_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 文件的逻辑中添加代码,尽管这样显得更加优雅,但徒增编程复杂度,没有现实收益。