3. 环境搭建(下):AFL/AFL++核心Fuzzer的编译与安装,LibFuzzer的集成方式,Docker镜像封装

上一章我们把基础工具链搭好了。这一章,咱们来搞定真正的核心——Fuzzer本身。

说实话,我见过不少新手在环境搭建这一步就卡住了。不是编译报错,就是版本冲突。我自己也踩过不少坑,所以这一章我会把那些容易翻车的地方都指出来。

3.1 AFL与AFL++:从源码编译到安装

AFL(American Fuzzy Lop)是Fuzz界的经典之作。但说实话,原版AFL已经很久没更新了。我个人建议直接用AFL++,它是社区维护的增强版,修复了大量bug,还支持更多特性。

3.1.1 编译AFL++

先拉代码:

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

然后编译。这里要注意,AFL++的编译选项挺多的,我一般这样配:

make all

嗯,就这么简单?其实不是。默认编译会检测你的CPU架构,自动开启一些优化。但如果你在交叉编译环境里,就得手动指定了。

注意: 如果你是在ARM64(aarch64)环境下编译,记得先装好gcc-aarch64-linux-gnu之类的交叉编译工具链。否则make会报找不到编译器。

编译完成后,安装:

sudo make install

验证一下:

afl-fuzz --version

看到版本号就说明装好了。

3.1.2 核心组件说明

AFL++安装后会提供几个关键工具:

工具名 作用
afl-gcc / afl-clang 带插桩的编译器,用于编译被测程序
afl-fuzz 主Fuzzer,负责生成变异输入并执行
afl-showmap 显示单个输入的执行路径覆盖
afl-tmin 测试用例最小化工具
afl-cmin 语料库精简工具

我曾经在项目里遇到过一个问题:用afl-gcc编译的程序跑起来没问题,但afl-fuzz就是报错。后来发现是编译时忘了加-fno-omit-frame-pointer。这个坑我记了很久。

3.1.3 插桩模式的选择

AFL++支持多种插桩模式。我整理了一个对比表:

模式 编译命令 特点 适用场景
LLVM模式 afl-clang-fast 性能最好,支持LTO 推荐首选
GCC模式 afl-gcc-fast 兼容性好 LLVM不可用时
QEMU模式 afl-fuzz -Q 无需源码,二进制级别插桩 闭源程序、异架构
Unicorn模式 afl-fuzz -U 模拟执行,可插桩任意架构 异架构Pwn题

对于异架构Pwn题,QEMU模式和Unicorn模式是重点。后面章节会详细讲。

3.2 LibFuzzer的集成方式

LibFuzzer是LLVM项目自带的Fuzzer。它和AFL的思路不太一样——LibFuzzer是库的形式,直接链接到被测程序里。

3.2.1 基本集成步骤

假设你有一个简单的测试函数:

// test_fuzz.cc
#include <stdint.h>
#include <stddef.h>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
    // 你的测试逻辑
    if (Size > 0 && Data[0] == 'H') {
        if (Size > 1 && Data[1] == 'I') {
            if (Size > 2 && Data[2] == '!') {
                __builtin_trap();  // 模拟崩溃
            }
        }
    }
    return 0;
}

编译命令:

clang++ -fsanitize=fuzzer -o test_fuzz test_fuzz.cc

就这么简单。运行:

./test_fuzz

LibFuzzer会自动开始生成输入并测试。

小技巧: 我习惯在编译时加上-fsanitize=address,这样能同时检测内存错误。命令变成:clang++ -fsanitize=fuzzer,address -o test_fuzz test_fuzz.cc

3.2.2 与AFL++的对比

很多人问我:到底该用AFL++还是LibFuzzer?我的看法是:

  • AFL++:适合黑盒测试,不需要改源码,支持多核并行,异架构支持好
  • LibFuzzer:适合白盒测试,需要写fuzz入口函数,启动快,内存开销小

在实际项目中,我经常两个一起用。先用LibFuzzer快速跑一轮,找出明显的bug。再用AFL++做长时间深度Fuzz。

3.3 Docker镜像封装

环境搭好了,但换个机器就得重来一遍?太麻烦了。Docker就是用来解决这个问题的。

3.3.1 编写Dockerfile

我一般这样写:

FROM ubuntu:22.04

# 安装基础依赖
RUN apt-get update && apt-get install -y \
    build-essential \
    git \
    clang \
    llvm \
    lld \
    python3 \
    python3-pip \
    qemu-user \
    binfmt-support \
    && rm -rf /var/lib/apt/lists/*

# 编译安装AFL++
RUN git clone https://github.com/AFLplusplus/AFLplusplus.git /opt/AFLplusplus \
    && cd /opt/AFLplusplus \
    && make all \
    && make install

# 设置环境变量
ENV AFL_PATH=/opt/AFLplusplus
ENV PATH=$PATH:/opt/AFLplusplus

# 创建工作目录
WORKDIR /workspace

构建镜像:

docker build -t fuzz-env:latest .

3.3.2 运行容器

启动容器:

docker run -it --rm \
    -v $(pwd):/workspace \
    --cap-add=SYS_PTRACE \
    --security-opt seccomp=unconfined \
    fuzz-env:latest
重要: --cap-add=SYS_PTRACE和--security-opt seccomp=unconfined这两个参数不能少。AFL++需要ptrace系统调用来跟踪子进程,默认的Docker安全策略会阻止这个操作。我曾经因为这个折腾了一下午。

3.3.3 多架构支持

对于异架构Pwn题,我们需要在Docker里安装QEMU用户态模拟器:

# 在Dockerfile中添加
RUN apt-get install -y qemu-user binfmt-support

# 注册binfmt支持
RUN update-binfmts --enable qemu-aarch64

这样,容器就能直接运行ARM64、MIPS等架构的二进制文件了。

3.4 知识体系总览

下面这张图把本章的核心内容串起来了:

Fuzzer环境搭建知识体系 Fuzzer选择 AFL++ LibFuzzer Docker封装 源码编译安装 插桩模式选择 LLVM / GCC / QEMU / Unicorn LLVMFuzzerTestOneInput -fsanitize=fuzzer 集成AddressSanitizer Dockerfile编写 容器运行参数 QEMU多架构支持 核心原则 异架构Pwn题优先选择AFL++的QEMU/Unicorn模式,LibFuzzer适合快速白盒测试

3.5 验证环境是否可用

装好之后,我建议你跑一个简单的测试来验证:

# 创建一个测试程序
echo 'int main() { return 0; }' > test.c

# 用afl-clang-fast编译
afl-clang-fast -o test test.c

# 创建输入目录
mkdir -p input output
echo "hello" > input/seed.txt

# 启动Fuzz(只跑几秒验证)
afl-fuzz -i input -o output -t 1000 ./test

如果看到AFL的统计界面正常显示,说明环境没问题。

个人经验: 我第一次装AFL++时,跑afl-fuzz一直报"core dump pattern"错误。后来发现是没设置/proc/sys/kernel/core_pattern。执行echo core > /proc/sys/kernel/core_pattern就解决了。这个坑我印象很深。

好了,环境搭建到这里就告一段落了。工具链、Fuzzer、Docker镜像都准备好了。接下来,我们就可以真正开始Fuzz了。