我的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
编译安装,这里有三个选项:
仅需要基础功能,仅核心组件和LLVM/GCC插桩,源码级fuzzing
make all sudo make install仅二进制fuzzing支持(QEMU/Frida等)
make binary-only sudo make insatll完整版本(包含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,你可以看到这样一个“图“

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 阶段变异策略 自动循环切换:
| 顺序 | 策略 | 操作方式 | 你的位置 |
|---|---|---|---|
| 1 | bit flips | 逐位翻转 1/1, 2/1, 4/1 | 已完成 |
| 2 | byte flips | 逐字节翻转 | 已完成 |
| 3 | arithmetics | 加减 ±1, ±2, ±4 | 已完成 |
| 4 | known ints | 插入 0, -1, MAX, 特殊值 | 已完成 |
| 5 | dictionary | 插入字典单词 | 已完成 |
| 6 | havoc | 大破坏:随机组合多种变异 | ✅ 当前 |
| 7 | splice | 拼接两个样本 | 待执行 |
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流程:
- AFL 从
in/seed.txt开始(内容为 “test”) - 不断变异输入( bit flip, byte flip, arithmetic, havoc 等)
- 监控覆盖率,发现新路径的输入保留下来
- 最终变异出 “AFL++”,触发了
__builtin_trap() - 保存崩溃文件到
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 以为程序卡死/超时 │
└─────────────────────────────────────────────────────────┘
所以可以大胆的禁用,这玩意对我们没啥用(