适用平台: macOS / Linux 版本: AFL++ 4.35c 用途: 文件格式模糊测试
目录
什么是AFL++
AFL++ 是 American Fuzzy Lop (AFL) 的增强版本,是一个强大的模糊测试工具。
核心特性
- 覆盖率引导: 基于代码路径覆盖率的智能变异
- 多种变异策略: 位翻转、算术操作、字典替换等
- 并行测试: 支持多核并行提升效率
- 自定义变异器: 可编写Python/C插件
适用场景
- 文件格式解析测试(图片、音频、视频等)
- 协议实现测试
- 命令行工具测试
- API接口测试
环境准备
macOS 环境要求
# 检查系统版本
sw_vers
# 安装Xcode命令行工具
xcode-select --install
# 安装Homebrew包管理器(如果未安装)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Linux 环境要求
# Ubuntu/Debian
sudo apt update
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 wget clang llvm llvm-dev
# CentOS/RHEL
sudo yum groupinstall "Development Tools"
sudo yum install -y python3-devel automake cmake git flex bison glib2-devel pixman-devel python3-setuptools cargo gtk3-devel wget clang llvm llvm-devel
安装步骤
步骤1: 下载源码
# 创建工作目录
mkdir -p ~/security-tools
cd ~/security-tools
# 克隆AFL++仓库
git clone https://github.com/AFLplusplus/AFLplusplus.git
cd AFLplusplus
# 查看当前版本
git describe --tags
步骤2: 编译安装
# 清理之前的编译(如果有)
make clean
# 编译核心组件
make distrib
# 安装到系统路径
sudo make install
# 验证安装
afl-fuzz --version
步骤3: 编译自定义变异器支持
AFL++ 支持使用 Python 编写自定义变异器:
# 编译Python扩展支持
cd custom_mutators
make
# 验证Python绑定
python3 -c "import custom_mutator; print('OK')"
步骤4: 配置环境
# 添加到PATH(可选)
echo 'export PATH=$PATH:$HOME/security-tools/AFLplusplus' >> ~/.zshrc
source ~/.zshrc
# 设置核心转储权限(Linux需要)
echo core | sudo tee /proc/sys/kernel/core_pattern
基础使用
AFL++ 工作流程
[种子输入] → [变异器] → [目标程序] → [覆盖率监控] → [发现崩溃]
↑ ↓
└──────────── 保存有趣路径 ←─────────────────────┘
示例1: 测试简单程序
# 创建测试程序
cat > test_program.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc < 2) {
printf("Usage: %s <input_file>\n", argv[1]);
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (!fp) return 1;
char buffer[10];
if (fread(buffer, 1, 10, fp) == 10) {
if (buffer[0] == 'A' && buffer[1] == 'F' && buffer[2] == 'L') {
// 触发崩溃
char *ptr = NULL;
*ptr = 'X';
}
}
fclose(fp);
return 0;
}
EOF
# 编译(使用afl-gcc插桩)
afl-gcc -o test_program test_program.c
# 创建种子输入
mkdir -p seeds
echo "Hello World" > seeds/test1.txt
echo "AFL Test" > seeds/test2.txt
# 运行模糊测试
afl-fuzz -i seeds -o findings -m none -- ./test_program @@
参数说明
| 参数 | 说明 |
|---|---|
-i <dir> |
种子输入目录 |
-o <dir> |
输出目录 |
-m none |
不限制内存 |
@@ |
输入文件占位符 |
-t <ms> |
超时时间(毫秒) |
-d |
跳过确定性变异 |
进阶:自定义变异器
为什么需要自定义变异器?
默认变异器对特定文件格式(如JPEG、MP4)效率较低。自定义变异器可以:
- 保持文件格式有效性
- 针对特定字段变异
- 提高测试覆盖率
Python自定义变异器模板
#!/usr/bin/env python3
"""
AFL++ 自定义变异器示例
用于生成特定格式的测试样本
"""
import random
import struct
class CustomMutator:
def __init__(self, seed):
"""初始化变异器"""
random.seed(seed)
self.seed_data = seed
def mutate(self, input_data):
"""
变异输入数据
参数:
input_data: bytes - 原始输入
返回:
bytes - 变异后的数据
"""
# 选择变异策略
strategy = random.choice([
self._bit_flip,
self._byte_flip,
self._arith_mutation,
self._insert_bytes,
self._delete_bytes,
])
return strategy(input_data)
def _bit_flip(self, data):
"""位翻转"""
data = bytearray(data)
pos = random.randint(0, len(data) - 1)
bit = random.randint(0, 7)
data[pos] ^= (1 << bit)
return bytes(data)
def _byte_flip(self, data):
"""字节翻转"""
data = bytearray(data)
pos = random.randint(0, len(data) - 1)
data[pos] = random.randint(0, 255)
return bytes(data)
def _arith_mutation(self, data):
"""算术变异"""
data = bytearray(data)
pos = random.randint(0, len(data) - 4)
delta = random.randint(-100, 100)
# 读取32位整数
value = struct.unpack('<I', data[pos:pos+4])[0]
value = (value + delta) & 0xFFFFFFFF
# 写回
data[pos:pos+4] = struct.pack('<I', value)
return bytes(data)
def _insert_bytes(self, data):
"""插入随机字节"""
data = bytearray(data)
pos = random.randint(0, len(data))
insert_len = random.randint(1, 10)
insert_data = bytes([random.randint(0, 255) for _ in range(insert_len)])
return bytes(data[:pos] + insert_data + data[pos:])
def _delete_bytes(self, data):
"""删除随机字节"""
if len(data) < 2:
return data
data = bytearray(data)
pos = random.randint(0, len(data) - 1)
delete_len = random.randint(1, min(10, len(data) - pos))
return bytes(data[:pos] + data[pos + delete_len:])
# AFL++ 入口函数
def init(seed):
"""初始化"""
global mutator
mutator = CustomMutator(seed)
return mutator
def fuzz(buf, add_buf, max_size):
"""
AFL++ 调用的变异函数
参数:
buf: bytes - 当前输入
add_buf: bytes - 额外输入(可选)
max_size: int - 最大输出大小
返回:
bytes - 变异后的数据
"""
global mutator
return mutator.mutate(buf)
使用自定义变异器
# 保存为 custom_mutator.py
# 运行AFL++并加载变异器
afl-fuzz -i seeds -o findings -m none \
-- python3 ./custom_mutator.py @@
实战:生成图片测试样本
场景描述
对图片查看APP进行模糊测试,需要生成畸形的JPEG/PNG文件。
方案1: 基于种子的变异
# 创建种子目录
mkdir -p seeds/images
# 准备正常图片作为种子
cp normal_image.jpg seeds/images/
cp normal_image.png seeds/images/
# 运行AFL++(需要编译目标程序)
# 这里假设有一个图片解析程序
afl-fuzz -i seeds/images -o findings -m none -- ./image_parser @@
方案2: Python脚本生成
创建专门的图片POC生成器:
#!/usr/bin/env python3
"""
JPEG格式POC生成器
用于测试图片解析器的安全性
"""
import os
import struct
import random
OUTPUT_DIR = 'jpeg_pocs'
os.makedirs(OUTPUT_DIR, exist_ok=True)
def generate_jpeg_poc(index, attack_type):
"""
生成JPEG畸形文件
JPEG结构:
SOI (FF D8) - 开始标记
APPn (FF En) - 应用标记
DQT (FF DB) - 量化表
SOF (FF C0) - 帧开始
DHT (FF C4) - 哈夫曼表
SOS (FF DA) - 扫描开始
EOI (FF D9) - 结束标记
"""
data = bytearray()
# SOI - 开始标记
data += b'\xFF\xD8'
if attack_type == 'overflow_size':
# APP0标记,恶意大小字段
data += b'\xFF\xE0' # APP0
data += struct.pack('>H', 0xFFFF) # 最大大小
data += b'JFIF\x00'
data += b'\x01\x01' # 版本
data += b'\x00' # 宽高比单位
data += struct.pack('>H', 0xFFFF) # X密度溢出
data += struct.pack('>H', 0xFFFF) # Y密度溢出
data += b'\x00\x00' # 缩略图大小
# 填充数据
data += bytes([random.randint(0, 255) for _ in range(500)])
elif attack_type == 'invalid_marker':
# 无效标记序列
for _ in range(100):
data += bytes([0xFF, random.randint(0, 255)])
elif attack_type == 'truncated':
# 截断文件
data += b'\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00'
# 故意不添加EOI
elif attack_type == 'nested_markers':
# 嵌套标记
for i in range(50):
data += b'\xFF\xE0'
data += struct.pack('>H', random.randint(10, 1000))
data += bytes([random.randint(0, 255) for _ in range(100)])
# 添加EOI(某些攻击可能省略)
if attack_type != 'truncated':
data += b'\xFF\xD9'
# 保存文件
filename = f'{OUTPUT_DIR}/jpeg_{attack_type}_{index:05d}.jpg'
with open(filename, 'wb') as f:
f.write(data)
return filename
def generate_png_poc(index, attack_type):
"""
生成PNG畸形文件
PNG结构:
签名 (8字节)
IHDR (头块)
IDAT (数据块)
IEND (结束块)
"""
data = bytearray()
# PNG签名
data += b'\x89PNG\r\n\x1a\n'
if attack_type == 'ihdr_overflow':
# IHDR块,恶意尺寸
chunk_type = b'IHDR'
chunk_data = bytearray()
chunk_data += struct.pack('>I', 0xFFFFFFFF) # 宽度溢出
chunk_data += struct.pack('>I', 0xFFFFFFFF) # 高度溢出
chunk_data += struct.pack('B', 8) # 位深度
chunk_data += struct.pack('B', 2) # 颜色类型
chunk_data += struct.pack('B', 0) # 压缩方法
chunk_data += struct.pack('B', 0) # 滤波方法
chunk_data += struct.pack('B', 0) # 隔行扫描
# 构建完整块
data += struct.pack('>I', len(chunk_data))
data += chunk_type
data += chunk_data
# CRC(简化处理)
data += struct.pack('>I', 0xDEADBEEF)
elif attack_type == 'chunk_size_overflow':
# 块大小溢出
data += struct.pack('>I', 0xFFFFFFFF) # 最大块大小
data += b'tEXt'
data += b'Comment' + b'\x00' + b'A' * 1000
data += struct.pack('>I', 0xDEADBEEF)
elif attack_type == 'invalid_chunk':
# 无效块类型
for i in range(20):
chunk_type = bytes([random.randint(65, 90) for _ in range(4)])
chunk_data = bytes([random.randint(0, 255) for _ in range(50)])
data += struct.pack('>I', len(chunk_data))
data += chunk_type
data += chunk_data
data += struct.pack('>I', 0xDEADBEEF)
# IEND块
data += struct.pack('>I', 0)
data += b'IEND'
data += struct.pack('>I', 0xAE426082)
filename = f'{OUTPUT_DIR}/png_{attack_type}_{index:05d}.png'
with open(filename, 'wb') as f:
f.write(data)
return filename
def main():
print('=== JPEG/PNG POC生成器 ===\n')
jpeg_attacks = ['overflow_size', 'invalid_marker', 'truncated', 'nested_markers']
png_attacks = ['ihdr_overflow', 'chunk_size_overflow', 'invalid_chunk']
print('[*] 生成JPEG POC...')
for attack in jpeg_attacks:
for i in range(100):
generate_jpeg_poc(i, attack)
print(f' 生成了 {len(jpeg_attacks) * 100} 个JPEG文件')
print('[*] 生成PNG POC...')
for attack in png_attacks:
for i in range(100):
generate_png_poc(i, attack)
print(f' 生成了 {len(png_attacks) * 100} 个PNG文件')
print(f'\n[+] 完成!文件保存在 {OUTPUT_DIR}/')
if __name__ == '__main__':
main()
方案3: 集成AFL++变异
结合AFL++和自定义生成:
#!/usr/bin/env python3
"""
AFL++ + 自定义JPEG变异器
"""
import random
import struct
class JPEGMutator:
def __init__(self):
self.jpeg_markers = {
'SOI': b'\xFF\xD8', # 开始
'EOI': b'\xFF\xD9', # 结束
'APP0': b'\xFF\xE0', # JFIF
'APP1': b'\xFF\xE1', # EXIF
'DQT': b'\xFF\xDB', # 量化表
'SOF': b'\xFF\xC0', # 帧开始
'DHT': b'\xFF\xC4', # 哈夫曼表
'SOS': b'\xFF\xDA', # 扫描开始
}
def generate_base_jpeg(self):
"""生成基础JPEG结构"""
data = bytearray()
# SOI
data += self.jpeg_markers['SOI']
# APP0 (JFIF)
app0 = bytearray()
app0 += b'JFIF\x00'
app0 += b'\x01\x01' # 版本
app0 += b'\x00' # 宽高比单位
app0 += struct.pack('>H', 1) # X密度
app0 += struct.pack('>H', 1) # Y密度
app0 += b'\x00\x00' # 无缩略图
data += self.jpeg_markers['APP0']
data += struct.pack('>H', len(app0) + 2)
data += app0
# DQT (简化)
data += self.jpeg_markers['DQT']
data += struct.pack('>H', 67)
data += b'\x00' # 精度
data += bytes([random.randint(1, 255) for _ in range(64)])
# SOF0
data += self.jpeg_markers['SOF']
data += struct.pack('>H', 17) # 长度
data += b'\x08' # 精度
data += struct.pack('>H', random.randint(100, 1000)) # 高度
data += struct.pack('>H', random.randint(100, 1000)) # 宽度
data += b'\x03' # 组件数
data += bytes([1, 0x11, 0, 2, 0x11, 1, 3, 0x11, 1])
# DHT (简化)
data += self.jpeg_markers['DHT']
data += struct.pack('>H', 31)
data += b'\x00' # DC表
data += bytes([random.randint(0, 255) for _ in range(28)])
# SOS
data += self.jpeg_markers['SOS']
data += struct.pack('>H', 12)
data += b'\x03' # 组件数
data += bytes([1, 0, 2, 0x11, 3, 0x11])
data += b'\x00\x3F\x00' # 谱选择
# 图像数据(简化)
data += bytes([random.randint(0, 254) for _ in range(1000)])
# EOI
data += self.jpeg_markers['EOI']
return bytes(data)
def mutate(self, jpeg_data):
"""变异JPEG数据"""
data = bytearray(jpeg_data)
# 随机选择变异策略
strategy = random.choice([
self._mutate_size_field,
self._mutate_dimension,
self._insert_marker,
self._corrupt_data,
])
return strategy(data)
def _mutate_size_field(self, data):
"""变异大小字段"""
# 找到所有长度字段并随机变异一个
i = 0
size_fields = []
while i < len(data) - 3:
if data[i] == 0xFF and data[i+1] not in [0x00, 0xD8, 0xD9]:
# 找到标记,读取长度
if i + 3 < len(data):
size = struct.unpack('>H', data[i+2:i+4])[0]
size_fields.append(i + 2)
i += size + 2
else:
break
else:
i += 1
if size_fields:
pos = random.choice(size_fields)
new_size = random.choice([0, 1, 0xFFFF, random.randint(0, 0xFFFF)])
data[pos:pos+2] = struct.pack('>H', new_size)
return bytes(data)
def _mutate_dimension(self, data):
"""变异图像尺寸"""
# 找到SOF标记
for i in range(len(data) - 10):
if data[i:i+2] == self.jpeg_markers['SOF']:
# SOF结构: marker, length, precision, height, width, ...
height_pos = i + 5
width_pos = i + 7
# 变异高度或宽度
if random.random() > 0.5:
new_val = random.choice([0, 1, 0xFFFF, 0xFFFFFFFF])
data[height_pos:height_pos+2] = struct.pack('>H', new_val & 0xFFFF)
else:
new_val = random.choice([0, 1, 0xFFFF, 0xFFFFFFFF])
data[width_pos:width_pos+2] = struct.pack('>H', new_val & 0xFFFF)
break
return bytes(data)
def _insert_marker(self, data):
"""插入随机标记"""
pos = random.randint(2, len(data) - 2)
marker = random.choice([
b'\xFF\xE0', b'\xFF\xE1', b'\xFF\xE2',
b'\xFF\xFE', # 注释
])
new_segment = marker
new_segment += struct.pack('>H', random.randint(10, 100))
new_segment += bytes([random.randint(0, 255) for _ in range(50)])
return bytes(data[:pos] + new_segment + data[pos:])
def _corrupt_data(self, data):
"""损坏图像数据"""
# 随机修改非标记区域
for _ in range(random.randint(1, 10)):
pos = random.randint(0, len(data) - 1)
# 避免修改关键标记
if data[pos] == 0xFF:
continue
data[pos] = random.randint(0, 255)
return bytes(data)
# AFL++ 接口
mutator = None
def init(seed):
global mutator
mutator = JPEGMutator()
return mutator
def fuzz(buf, add_buf, max_size):
global mutator
return mutator.mutate(buf)
运行完整测试流程
# 1. 创建项目结构
mkdir -p image_fuzzing/{seeds,findings,scripts}
# 2. 准备种子文件
cp normal_image.jpg image_fuzzing/seeds/
cp normal_image.png image_fuzzing/seeds/
# 3. 保存变异器脚本
# 将上面的JPEGMutator保存为 image_fuzzing/scripts/jpeg_mutator.py
# 4. 编译目标程序(以ImageMagick为例)
wget https://github.com/ImageMagick/ImageMagick/archive/refs/tags/7.1.0.tar.gz
tar xzf 7.1.0.tar.gz
cd ImageMagick-7.1.0
# 使用AFL++编译
CC=afl-gcc CXX=afl-g++ ./configure --prefix=/tmp/im
make -j$(nproc)
make install
# 5. 运行测试
cd ../image_fuzzing
afl-fuzz -i seeds -o findings -m none \
-- /tmp/im/bin/identify @@
# 6. 使用自定义变异器(高级)
export PYTHONPATH=$PYTHONPATH:$(pwd)/scripts
afl-fuzz -i seeds -o findings -m none \
-- python3 -c "
import sys
sys.path.insert(0, 'scripts')
from jpeg_mutator import init, fuzz
# AFL++会自动调用
" @@
常见问题
Q1: 编译时出现 “afl-as not found”
# 确保PATH包含AFL++目录
export PATH=$PATH:$(pwd)
# 或重新安装
sudo make install
Q2: “Oops, the program crashed with signal 11”
这是正常的!AFL++会捕获崩溃并保存触发崩溃的输入:
# 查看崩溃样本
ls findings/default/crashes/
# 分析崩溃
gdb ./target_program findings/default/crashes/id:000001,sig:11,src:...
Q3: 测试速度太慢
# 使用并行模式
afl-fuzz -i seeds -o findings -M fuzzer01 -- ./target @@ &
afl-fuzz -i seeds -o findings -S fuzzer02 -- ./target @@ &
afl-fuzz -i seeds -o findings -S fuzzer03 -- ./target @@
# 查看状态
afl-whatsup findings
Q4: 内存不足
# 限制内存使用
afl-fuzz -i seeds -o findings -m 500 -- ./target @@
# 或使用持久模式(需要修改目标程序)
Q5: 如何分析结果
# 查看统计信息
afl-stat findings
# 查看执行路径
afl-showmap -o map.txt -- ./target @@
# 最小化崩溃样本
afl-tmin -i findings/default/crashes/id:000001 -o minimized -- ./target @@
# 生成测试用例
afl-cmin -i seeds -o minimized_seeds -- ./target @@
进阶学习资源
官方文档
推荐书籍
- 《模糊测试:强制漏洞发现》- Michael Sutton
- 《The Fuzzing Book》- 在线免费书籍
- 《灰帽黑客:正义黑客的道德规范》
实战项目
- OSS-Fuzz: 开源软件持续模糊测试
- FuzzBench: 模糊测试器评估平台
- Syzkaller: 内核模糊测试
总结
AFL++ 是一个强大且灵活的模糊测试框架,通过本教程你学会了:
✅ 安装和配置 AFL++ ✅ 基本的模糊测试流程 ✅ 编写自定义变异器 ✅ 针对文件格式的测试策略 ✅ 分析和处理测试结果
下一步建议:
- 选择一个目标程序进行实战测试
- 学习编写更复杂的变异器
- 探索AFL++的高级特性(QEMU模式、持久模式等)
- 参与开源安全项目