我的AFL++fuzzing入门之路(1)

00 前言

最近学校上的事渐渐变少,遂想着找份实习充实一下自己,学习更多先进的前沿技术。

但是真正开始写简历时便越发惶恐,觉得能写的实在太少了,

思来想去再学点东西来扩充一下自己的技术栈,翻着网页发现收藏夹里的fuzz测试介绍。

而fuzz模糊测试是我在去年就见到过的,但是当时也只是匆匆一瞥,知道fuzz是一种自动输入寻找崩溃点的技术。

现在有空了,多篇介绍AFL的文章看下来发现这个功能很强大,而且和二进制方向有一定的互补,

所以就开始尝试学习了。

由于笔者也刚在这个领域入门,所以文章可能会有写不够准确甚至谬误,欢迎各位大佬斧正。

01 AFL++Fuzzing介绍

AFL++(American Fuzzy Lop Plus Plus)是目前最流行和最先进的模糊测试(Fuzzing)框架之一,是经典AFL(American Fuzzy Lop)的社区维护增强版。

它是由Google安全工程师Michał Zalewski开发的一款开源fuzzing测试工具。

  • 其可以高效地对二进制程序进行fuzzing,挖掘可能存在的内存安全漏洞,如_栈溢出、堆溢出、UAF、double free_等。
  • 由于需要在相关代码处插桩,因此AFL主要用于对开源软件进行测试。
  • 配合QEMU等工具,也可对闭源二进制代码进行fuzzing,但执行效率会受到影响。

工作原理:通过对源码进行重新编译时进行插桩(简称编译时插桩)的方式利用自动产生测试用例来探索二进制程序内部新的执行路径。AFL也支持直接对没有源码的二进制程序进行测试,但需要QEMU的支持。

02 环境搭建

这里有两种选择,可以使用docker容器,也可以直接安装到本机

先附上docker容器搭建的方法:

docker pull aflplusplus/aflplusplus:latest
docker run -ti -v /path/to/your/code:/src aflplusplus/aflplusplus

为了能够详细的学习(?,笔者决定直接安装到Ubuntu24.04虚拟机里。

先更新系统和软件包:

sudo apt update && sudo apt upgrade -y

安装依赖(没错,就是这么多),有LLVM/Clang、Python、Rust等:

sudo apt install -y build-essential python3-dev automake cmake git \
    flex bison libglib2.0-dev libpixman-1-dev python3-setuptools \
    cargo libgtk-3-dev lld llvm llvm-dev clang \
    gcc-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-plugin-dev \
    libstdc++-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-dev \
    ninja-build cpio libcapstone-dev wget curl python3-pip

找个好地方,克隆源码

git clone https://github.com/AFLplusplus/AFLplusplus.git
cd AFLplusplus

编译安装,这里有三个选项:

  1. 仅需要基础功能,仅核心组件和LLVM/GCC插桩,源码级fuzzing

    make all
    sudo make install
    
  2. 仅二进制fuzzing支持(QEMU/Frida等)

    make binary-only
    sudo make insatll
    
  3. 完整版本(包含QEMU、Frida、Unicorn等二进制fuzzing模式)

    make distrib
    sudo make install
    

安装完成后可以输入以下命令来检测是否安装成功:

# 检查版本
afl-fuzz --version

# 检查编译器
afl-cc --version
afl-clang-fast --version

# 查看帮助
afl-fuzz --help | head -20

03 测试用例

如果AFL++Fuzzing正确的安装好了,那么可以试试来做第一个fuzzing测试,

创建一个测试目录来放案例和输入输出

mkdir ~/afl_test && cd ~/afl_test

第一步,写一段明显有漏洞的C语言代码用于测试:

cat > test.c << 'EOF'
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char buf[100];
    FILE *f = fopen(argv[1], "r");
    if (!f) return 1;
    fread(buf, 1, sizeof(buf), f);
    fclose(f);
    
    // 漏洞触发点:输入 "AFL++" 会崩溃
    if (buf[0] == 'A' && buf[1] == 'F' && buf[2] == 'L')
        if (buf[3] == '+')
            if (buf[4] == '+')
                __builtin_trap();  // 触发崩溃(SIGTRAP)
    return 0;
}
EOF

第二步,使用afl的编译器进行插桩编译

# 使用 afl-cc 编译,自动插入覆盖率探针
afl-cc -o test test.c

# 验证是否插桩成功
ls -la test

第三步,准备模糊测试的输入输出文件

mkdir -p in out
echo "test" > in/seed.txt

第四步,开始fuzzing

# 启动 fuzzing
afl-fuzz -i in -o out -- ./test @@

# 参数说明:
# -i in      : 输入目录(种子文件)
# -o out     : 输出目录(结果保存)
# --          : 分隔符
# ./test      : 目标程序
# @@          : 占位符,AFL 会自动替换为生成的测试文件

诶,这里如果也是Ubuntu系统可能就会碰到和我一样的问题了(别的系统我也不造啊

❯ afl-fuzz -i in -o out -- ./test @@
afl-fuzz++4.36a based on afl by Michal Zalewski and a large online community
[+] AFL++ is maintained by Marc "van Hauser" Heuse, Dominik Maier, Andrea Fioraldi and Heiko "hexcoder" Eißfeldt
[+] AFL++ is open source, get it at https://github.com/AFLplusplus/AFLplusplus
[+] NOTE: AFL++ >= v3 has changed defaults and behaviours - see README.md
[+] No -M/-S set, autoconfiguring for "-S default"
[*] Getting to work...
[+] Using exploration-based constant power schedule (EXPLORE)
[+] Enabled testcache with 50 MB
[+] Generating fuzz data with a length of min=1 max=1048576
[+] FrameShift status: enabled (10% overhead configured)
[*] Checking core_pattern...

[-] Your system is configured to send core dump notifications to an
    external utility. This will cause issues: there will be an extended delay
    between stumbling upon a crash and having this information relayed to the
    fuzzer via the standard waitpid() API.
    If you're just experimenting, set 'AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1'.

    To avoid having crashes misinterpreted as timeouts, please 
    temporarily modify /proc/sys/kernel/core_pattern, like so:

    echo core | sudo tee /proc/sys/kernel/core_pattern

[-] PROGRAM ABORT : Pipe at the beginning of 'core_pattern'
         Location : check_crash_handling(), src/afl-fuzz-init.c:2653

查了一下问问ai,说是 Ubuntu 的 apport 服务占用了 core dump 处理,

跟着它提示的命令敲就行了

# 临时修复(立即生效,重启后失效)
echo core | sudo tee /proc/sys/kernel/core_pattern

或者临时设置一下环境

# 使用环境变量忽略警告(不推荐用于正式测试)
AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 afl-fuzz -i in -o out -- ./test @@

如果想一劳永逸呢,就按下面这样

# 禁用 apport 服务
sudo systemctl disable apport.service
sudo service apport stop

# 永久设置 core_pattern
echo "kernel.core_pattern = core" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

这里给个底部跳转:什么是apport,为什么会冲突以及可以禁用吗

好,处理完之后接着fuzzing,你可以看到这样一个“图“

image-20260309003159937

04 分析fuzzing结果

第六步:查看崩溃结果

按 Ctrl+C 停止 fuzzing后可以看到上面的图

我们来详细(?)看看这个图中各项的意义:

1st.总体结果区

首先是右上角的总体结果区:

cycles done : 68        ← 已完成 68 轮完整循环
corpus count : 9        ← 语料库有 9 个有效样本
saved crashes : 1       ← 发现 1 个独特崩溃!
saved hangs : 0         ← 无卡死

关键:saved crashes : 1 表示 AFL 成功找到了触发 __builtin_trap() 的输入,触发了一次崩溃

2nd.运行时间区

然后是左上角的运行时间区:

run time : 0 days, 0 hrs, 3 min, 54 sec           ← 整个fuzzing的运行时间:3分54秒
last new find : 0 days, 0 hrs, 3 min, 51 sec      ← 上一个新发现的时间:3分51秒
last saved crash : 0 days, 0 hrs, 3 min, 51 sec   ← 上一个崩溃发现时间:3分51秒
last saved hang : none seen yet                   ← 还么有发现卡死

这个不是特别值得看

3rd.循环进度区

左上第二区,循环进度区

├─ cycle progress ─────────────────────┬
│  now processing : 7*97 (77.8%)       ← 当前处理第 7 个样本,第 97 次变异,完成 77.8%
│  runs timed out : 0 (0.00%)          ← 超时次数 0

也没啥可以注意的

4th.覆盖率映射区

右上第二区,覆盖率映射区

├─ map coverage┴──────────────────────┤
│    map density : 38.46% / 69.23%    ← 当前覆盖率 38.46%,历史最高 69.23%
│ count coverage : 17.00 bits/tuple   ← 边碰撞计数精度

这个,有点重要,不过在后头有详细的

5th.阶段进度区

左侧第三区,阶段进度区

├─ stage progress ─────────────────────┼
│  now trying : havoc                   ← 当前使用的变异策略
│ stage execs : 229/400 (57.25%)        ← 本阶段已执行 229 次,目标 400 次,完成 57.25%
│ total execs : 254k                    ← 累计总执行次数
│  exec speed : 1050/sec                ← 每秒执行速度

重点在于变异策略这一个属性

AFL 的 5 阶段变异策略 自动循环切换:

顺序策略操作方式你的位置
1bit flips逐位翻转 1/1, 2/1, 4/1已完成
2byte flips逐字节翻转已完成
3arithmetics加减 ±1, ±2, ±4已完成
4known ints插入 0, -1, MAX, 特殊值已完成
5dictionary插入字典单词已完成
6havoc大破坏:随机组合多种变异✅ 当前
7splice拼接两个样本待执行

havoc 特点:

  • 最"暴力"的策略
  • 随机选择多种变异操作组合
  • 通常最容易发现新崩溃

6th.深度发现分析区

右侧第三区,深度发现分析区

├─ findings in depth ─────────────────┤
│ favored items : 3 (33.33%)          ← 3 个"优质"样本(占总数 33%)
│  new edges on : 9 (100.00%)         ← 9 个样本都触发了新边(100%)
│ total crashes : 135 (1 saved)       ← 共触发 135 次崩溃,保存 1 个独特崩溃
│  total tmouts : 0 (0 saved)         ← 无超时

one.什么是优质样本?

AFL 从语料库中挑选"最有价值"的样本优先变异
标准:体积小、执行快、发现新边多

two.为什么叫独特崩溃?

因为afl为了避免存 135 个相同的 "AFL++" 文件,节约磁盘空间
135 次崩溃 → 哈希崩溃签名 → 发现都是同一个漏洞 → 只存第 1 个触发文件

7th.策略产出区

左下角,策略产出区

bit flips : 0/0, 0/0, 0/0        ← 3种位翻转策略:新发现/尝试次数
byte flips : 0/0, 0/0, 0/0       ← 3种字节翻转策略:新发现/尝试次数
arithmetics : 0/0, 0/0, 0/0      ← 3种算术变异策略:新发现/尝试次数
known ints : 0/0, 0/0, 0/0       ← 3种已知整数策略:新发现/尝试次数
dictionary : 0/0, 0/0, 0/0, 0/0  ← 4种字典策略:新发现/尝试次数
havoc/splice : 9/253k, 0/0       ← havoc发现9个新路径/253k次尝试, splice无
py/custom/rq : unused...         ← Python变异器/自定义/Redqueen:未使用
trim/eff : 39.44%/11, n/a        ← 样本精简效率39.44%/11次尝试

8th.语料库几何结构

右下角,语料库几何结构

levels : 6          ← 最大变异深度6层
pending : 0         ← 待处理样本0(全部处理完)
pend fav : 0        ← 优质样本待处理0
own finds : 8       ← AFL自己发现的样本8个
imported : 0        ← 外部导入的样本0
stability : 100.00% ← 执行稳定性100%

end.查看策略

1看右上:有崩溃吗?有几个?
2看右下:稳定吗?100%?
3看左中:速度快吗?>500?
有问题再展开,没问题继续跑

05查看崩溃结果

第七步:理解发生了什么

# 查看发现的崩溃
ls out/default/crashes/

id:000000,sig:04,src:000007,time:2715,execs:2919,op:havoc,rep:1  README.txt

# 查看触发崩溃的具体输入
xxd out/default/crashes/id:000000*

00000000: 4146 4c2b 2b2b 2b2b                      AFL+++++

# 手动验证崩溃
./test out/default/crashes/id:000000*
# 应该显示:Trace/breakpoint trap (core dumped)

[1]    289291 illegal hardware instruction  ./test out/default/crashes/id:000000*

崩溃文件名解析:

id:000000,sig:04,src:000007,time:2715,execs:2919,op:havoc,rep:1
│      │      │      │       │      │       │      │
│      │      │      │       │      │       │      └─ 重复次数
│      │      │      │       │      │       └─ 变异策略:havoc
│      │      │      │       │      └─ 第2919次执行时发现
│      │      │      │       └─ 2715毫秒(2.7秒)时发现
│      │      │      └─ 源自第7号样本
│      │      └─ 信号04:SIGILL(非法指令)
│      └─ 崩溃编号:000000
└─ 唯一ID

崩溃内容解析:

xxd: 4146 4c2b 2b2b 2b2b  =  AFL+++++  (9字节)
        │  │  └── +++++ (havoc随机添加的冗余+)
        │  └─ ++ (触发条件)
        └─ AFL (触发条件)

触发逻辑:

if (buf[0]=='A' && buf[1]=='F' && buf[2]=='L')  // ✓ 匹配 "AFL"
    if (buf[3]=='+')                             // ✓ 匹配 "+"
        if (buf[4]=='+')                         // ✓ 匹配 "+" 
            __builtin_trap();                    // 💥 崩溃!

后面多余的 ++++ 是 havoc 策略随机添加的,不影响触发。

验证成功标志:

[1] 289291 illegal hardware instruction  ./test out/default/crashes/id:000000*
     │                              │
     └─ 进程ID                      └─ 错误类型:非法硬件指令 = SIGILL = __builtin_trap()

06fuzzing流程

综上所述,我们可以看到在一个简单的测试用例下的fuzzing流程:

  1. AFL 从 in/seed.txt 开始(内容为 “test”)
  2. 不断变异输入( bit flip, byte flip, arithmetic, havoc 等)
  3. 监控覆盖率,发现新路径的输入保留下来
  4. 最终变异出 “AFL++”,触发了 __builtin_trap()
  5. 保存崩溃文件到 out/default/crashes/

什么是apport

Apport 是 Ubuntu 的错误自动收集系统,它主要负责自动收集崩溃信息(程序崩溃时生成 .crash 报告)、上传 Canonical(发送给 Ubuntu 开发者分析)以及用户友好提示(图形界面弹出"系统错误"对话框)。

为什么会有冲突?

核心问题:core_pattern 管道机制

Linux 通过 /proc/sys/kernel/core_pattern 控制 core dump 处理方式:

# Ubuntu 默认(被 apport 占用)
cat /proc/sys/kernel/core_pattern
# 输出:|/usr/share/apport/apport %p %s %c %d %P %E
#      ↑ 管道符号表示交给外部程序处理

而AFL的工作流程是:

程序崩溃
    ↓
内核生成 core dump
    ↓
通过管道传给 apport
    ↓
apport 处理(可能几秒~几十秒)
    ↓
最后才通知父进程
    ↓
AFL 以为程序是 timeout,不是 crash!

FL 的 forkserver 需要毫秒级感知崩溃,apport 的延迟会导致:

  • 崩溃被误判为超时
  • 崩溃文件丢失或重复
  • fuzzing 效率严重下降

图例应该是:

┌─────────────────────────────────────────────────────────┐
│  正常配置 (echo core)                                    │
│  程序 ──崩溃──→ 生成 core 文件 ──→ AFL 立即感知 (waitpid)  │
│                              延迟: ~1ms                  │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│  Apport 配置 (Ubuntu 默认)                               │
│  程序 ──崩溃──→ 管道 → apport 处理 → 生成 .crash 报告      │
│                              延迟: ~100ms~数秒          │
│                              → AFL 以为程序卡死/超时      │
└─────────────────────────────────────────────────────────┘

所以可以大胆的禁用,这玩意对我们没啥用(