AFL++ 安装与使用完整指南

适用平台: macOS / Linux 版本: AFL++ 4.35c 用途: 文件格式模糊测试


目录

  1. 什么是AFL++
  2. 环境准备
  3. 安装步骤
  4. 基础使用
  5. 进阶:自定义变异器
  6. 实战:生成图片测试样本
  7. 常见问题

什么是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++ ✅ 基本的模糊测试流程 ✅ 编写自定义变异器 ✅ 针对文件格式的测试策略 ✅ 分析和处理测试结果

下一步建议

  1. 选择一个目标程序进行实战测试
  2. 学习编写更复杂的变异器
  3. 探索AFL++的高级特性(QEMU模式、持久模式等)
  4. 参与开源安全项目
点赞