软件安全实验 SEEDlabs 模糊测试技术实验(基于AFL)

Task1:初探AFL

Task 1.1:安装 AFL

我们从源码进行构建,克隆最新版本的源码。

1
git clone https://github.com/google/AFL.git

随后进入 AFL文件夹,在文件夹下打开命令行输入:

1
2
make
sudo make install #安装到系统目录

安装完成后,AFL安装在/usr/local/bin目录下,ls查看文件判断是否安装成功

1
ls /usr/local/bin/afl*

image-20251227185000976

Task1.2:初步尝试

首先准备一段待测试的代码test.c,此代码中模拟了4个漏洞(有些漏洞并非真实安全问题,仅用 作演示)。

image-20251227221740055

首先,使用afl-gcc编译待测试的test.c文件

实质上,afl-xxx系列命令是对编译器如gcc、g++或clang的包装,其使用的参数选项与这些编译 器相同。

对于C++源代码,应使用afl-g++进行编译。 接下来,创建两个目录fuzz_in和fuzz_out,分别用于存放模糊测试的输入和输出。在fuzz_in目 录中至少需要准备一个测试用例。

执行以下命令进行模糊测试

1
2
3
afl-gcc -g -o test test.c
echo 'aaa' > ./fuzz_in/case
afl-fuzz -i ./fuzz_in -o ./fuzz_out ./test

fuzz_out 目录记录了模糊测试的运行结果。通过分析此目录,我们可以识别出导致程序崩溃(crash) 的具体测试用例(case)。

crashes 子目录包含了触发程序崩溃的测试用例,hangs子目录包含了导致程序超时的测试用例,而 queue 子目录存储了所有探索到的不同执行路径的测试用例。如果获得了所需的崩溃信息或长时间无法获 取新的有趣输出,应使用Ctrl-C命令退出模糊测试过程。

例如,可使用xxd命令查看触发的具体崩溃实例,分析test.c可知crashcase3是触发了第三个漏 洞,即输入长度过长导致栈溢出,程序将异常退出。

image-20251227223020108

image-20251228012303027

任务:请使用 xxd查看你触发crash的cases,并判断分别对应 test.c中哪种漏洞,并解释漏洞产生 的原因。注意,你的crashcases应覆盖所有4个漏洞。

特定字符串触发异常 (Logic Flaw - “FAS” + Length 6)

image-20251228012822564

现象:分析 AFL 生成的崩溃样本 id:000007,通过 xxd 命令查看其二进制内容。样本内容以十六进制 46 41 53 开头(即 ASCII 码 “FAS”),且文件总长度为 6 字节。
成因:对应代码 test.c 第 14-20 行。函数 vuln_test 检查输入字符串:若首字符为 ‘F’ 且长度为 6,随后继续检查第二字符是否为 ‘A’、第三字符是否为 ‘S’。一旦匹配字符串 “FAS…”,程序调用 raise(SIGSEGV) 导致崩溃。

栈缓冲区溢出 (Stack Buffer Overflow)

image-20251228012850576

现象:AFL 生成了大量长文件样本(例如 id:000002),其长度(如 26698 字节)远超缓冲区大小。
成因:对应代码 test.c 第 34 行 gets(buff)。变量 buff 定义为 char buff[80],但 gets() 函数未对输入长度进行限制。当输入长度超过 80 字节时,多余的数据覆盖了栈上的关键信息(如返回地址),导致函数返回时跳转到非法地址,引发段错误。

格式化字符串漏洞 (Format String Vulnerability)

image-20251228012953020

现象:通过输入包含格式化字符(如 %s%s%s)的样本 manual_vuln4_fmt,程序在未发生栈溢出的情况下崩溃。
成因:对应代码 test.c 第 35 行 printf(buff)。程序直接将用户输入作为 printf 的格式化字符串参数,而未提供后续参数。当输入包含 %s、%n 等格式说明符时,printf 尝试从栈中读取非法内存地址或向非法地址写入数据,导致访问违规(Access Violation)并崩溃。

特定条件触发异常 (Logic Flaw - ‘C’ + Length 30)

image-20251228013045149

现象:通过构造首字符为 ‘C’ 且总长度为 30 字节的输入样本 manual_vuln1_c_30,程序运行后发生段错误(Segmentation Fault)。
成因:代码 test.c 第 10-12 行存在逻辑判断 if(str[0] == ‘C’ && len == 30)。当输入满足此条件时,程序主动调用 raise(SIGSEGV) 触发崩溃。AFL 在测试中倾向于生成长字符串触发栈溢出,因此该精确长度路径较难命中,通过手动验证确认了漏洞存在。

Task2: 基于AFL++测试Xpdf

AFL++ 是在原始AFL框架基础上演进的覆盖引导模糊测试工具,对性能和可用性进行了多项优化, 尤其在大型项目模糊测试场景下往往具有更好的执行效率。尽管如此,AFL++在整体架构、使用方式和 核心工作流程上与AFL基本保持一致。在本任务中,我们将使用AFL++对大型开源PDF处理组件Xpdf 进行模糊测试实践。

AFL++ 安装

本实验采用自源码编译的方式安装最新版本的AFL++2。在此之前,需要安装若干构建依赖包

1
2
3
4
5
6
7
8
9
sudo apt-get update
sudo apt-get install automake autoconf build-essential llvm clang lldb lld meson gcc-$(gcc-dumpversion)-plugin-d
sudo apt-get install -y automake autoconf build-essential llvm clang lldb lld meson gcc-$(gcc -dumpversion)-plugin-dev libglib2.0-dev ninja-build

cd $HOME
git clone https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
make all
sudo make install

系统提示LLVM版本过旧 如果在执行AFL++相关命令时收到“LLVM版本过旧”的提示,需要安装更新版 本的LLVM/Clang。以下示例以安装LLVM15为例:

1
2
3
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" -- 15

sudo apt install clang-15 lld-15 llvm-15-dev

构建测试环境

首先,为本次Xpdf模糊测试创建单独工作目录:

1
2
cd $HOME
mkdir fuzzing_xpdf && cd fuzzing_xpdf

下载并解压Xpdf3.02版本源码:

1
2
wget --no-check-certificate https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -xvzf xpdf-3.02.tar.gz

使用系统默认编译器对Xpdf进行常规构建与安装(用于功能验证)

1
2
3
4
cd xpdf-3.02
./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install

随后,准备若干PDF示例文件用于测试:

1
2
3
cd $HOME/fuzzing_xpdf
mkdir pdf_examples && cd pdf_examples
wget --no-check-certificate https://github.com/mozilla/pdf.js-sample-files/raw/master/helloworld.pdf

可以通过如下命令调用pdfinfo对示例文件进行解析,检查Xpdf是否安装成功并可正常工作:

1
2
cd $HOME/fuzzing_xpdf
./install/bin/pdfinfo -box -meta ./pdf_examples/helloworld.pdf

若能看到正确的PDF元数据输出,则说明Xpdf已成功构建和安装。

image-20251228022354889

执行模糊测试

在使用AFL++重新构建前,先清理已有的安装目录和编译产物:

1
2
3
rm -r $HOME/fuzzing_xpdf/install
cd $HOME/fuzzing_xpdf/xpdf-3.02/
make clean

与afl-gcc 类似,afl-clang-fast 是对 clang 的封装,通过LLVMPass对目标程序进行插桩,从 而在性能与插桩灵活性方面具备优势(原版AFL也支持LLVMMode,有兴趣可进一步自行探索)。 使用afl-clang-fast 和afl-clang-fast++ 作为编译器,对Xpdf进行重新配置与构建:

1
2
3
4
CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++ ./configure --prefix="$HOME/fuzzing_xpdf/install/"
# 编译安装
make
make install

在编译输出中可以看到来自AFL++的插桩提示,表明目标程序已被插入覆盖率收集代码。随后,可 以使用如下命令启动模糊测试:

1
2
3
4
# 创建输出目录
mkdir -p $HOME/fuzzing_xpdf/out

$HOME/AFLplusplus/afl-fuzz -i $HOME/fuzzing_xpdf/pdf_examples/ -o $HOME/fuzzing_xpdf/out/ -s 123 -- $HOME/fuzzing_xpdf/install/bin/pdftotext @@ $HOME/fuzzing_xpdf/output

参数含义如下:

•-i:指定初始输入(种子)文件目录。

•-o:指定AFL++输出目录,用于保存变异样本、崩溃用例等。

•-s:设置静态随机种子。AFL默认采用非确定性测试流程,使用固定种子(如-s 123)有助于复现 实验结果。

• @@:命令行参数占位符,AFL++会在运行时将其替换为当前输入样本的文件名。

image-20251228221358910

复现Crash

本实验中触发的崩溃对应的已知安全问题被分配为CVE-2019-13288。请阅读相关公开信息,接下来我们将使用gdb分析导致程序崩溃的具体原因。

首先,需要重新编译Xpdf,引入调试信息以获得完整的符号和栈回溯信息:

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 清理旧的编译文件
rm -r $HOME/fuzzing_xpdf/install
cd $HOME/fuzzing_xpdf/xpdf-3.02
make clean

# 2. 配置编译参数:加入 -g (调试信息) 和 -O0 (关闭优化)
# 这一步非常关键,不做的话 GDB 看到的都是乱码
CFLAGS="-g -O0" CXXFLAGS="-g -O0" ./configure --prefix="$HOME/fuzzing_xpdf/install/"

# 3. 重新编译并安装
make
make install

然后,使用gdb对触发崩溃的输入样本进行调试(将替换为实际Crash文件名)

1
gdb --args $HOME/fuzzing_xpdf/install/bin/pdftotext $HOME/fuzzing_xpdf/out/default/crashes/<your_filename> $HOME/fuzzing_xpdf/outpu

在gdb中依次执行:

1
2
(gdb) run
(gdb) bt

run 启动程序,崩溃发生后通过bt查看栈回溯信息,一般可以看到因段错误(SegmentationFault) 导致的异常退出。在实验报告中,请结合回溯信息说明程序崩溃的大致位置与原因。

进一步分析可以发现,在解析PDF过程中,多次调用了parser.cc中的相关函数。初步怀疑问题与 getObj 和 fetch 等函数的调用逻辑有关。为验证该假设,可以在Parser::getObj()处设置断点,并重 新运行程序:

任务:请依据上述步骤,结合gdb进行Crash的复现与分析。在实验报告中给出调试过程的关键截图, 描述你的观察结果,并解释该漏洞产生的根本原因(例如错误的边界检查、异常输入未被正确处理等)。

image-20251228231000011

image-20251228230951634

image-20251228231037349

image-20251228231005383

image-20251228231020374

崩溃位置:EmbedStream::getChar() (Frame #0),由 Gfx::opBeginImage (Frame #1) 调用。

成因分析

这是一个**内存访问违规(Memory Corruption / Segmentation Fault)**漏洞。

通过 GDB 调试发现,程序在处理 PDF 中的图像对象(opBeginImage)时崩溃。结合命令行输出的大量 Error: Dictionary key… 提示,说明 AFL++ 生成的畸形 PDF 文件破坏了对象的字典结构。当程序试图通过 EmbedStream::getChar 读取图像数据流时,由于结构损坏(可能是空指针引用或越界读取),访问了非法的内存地址,导致操作系统抛出 SIGSEGV 信号终止程序。这证实了 Xpdf 在解析畸形 PDF 对象时存在输入验证缺陷。

Task3: 使用QEMU模式执行模糊测试(无程序源码)

在Task1和Task2中,我们主要针对具有源代码的程序开展模糊测试。而在真实应用场景中,测试目 标往往仅以二进制形式提供,无法获得源码。为支持此类“黑盒”目标,AFL++提供了QEMU模式,使得在 缺乏源码的情况下仍然可以进行覆盖引导的模糊测试。启用QEMU模式非常简单,只需在原有afl-fuzz 命令行基础上增加-Q参数。

QEMU模式是AFL++的高级特性之一。它基于QEMU(QuickEMUlator,官方网站:https://www.qemu.org/)这一全系统模拟器,在运行时对目标程序进行动态二进制翻译与插桩:即由QEMU逐块读取 目标程序的机器指令,将其翻译为宿主平台等价的指令序列,并在翻译过程中自动插入额外的监控代码, 用于记录执行路径和代码覆盖情况,而无需重新编译或修改原始可执行文件。通过这种方式,即便只有二 进制文件,也能实现覆盖引导的模糊测试。

安装QEMU模式(在AFLplusplus目录下)

首先,在AFLplusplus源码目录中为QEMU模式安装所需依赖并构建支持模块:

1
2
3
4
sudo apt-get install libglib2.0-dev ninja-build
cd $HOME/AFLplusplus/qemu_mode
./build_qemu_support.sh
# 这一步可能耗时较长

若安装过程中出现”cannot find”类错误,说明系统缺失某些必需软件包,请根据错误提示使用 sudo apt install 安装缺失依赖。

完成构建后,返回上级目录并重新安装AFL++,使QEMU相关组件正确注册到系统路径中:

1
2
cd ..
sudo make install

随后可以检查afl-qemu-trace是否已安装到/usr/local/bin等路径下:

1
ls /usr/local/bin/afl*

image-20251228235825197

使用QEMU模式执行模糊测试

根据任务提供的环境,我们使用已编译并安装好的exif程序4作为模糊测试目标,输入样本来自 exif-samples 文件夹中的JPEG文件。

由于exif依赖特定版本的libc,需要先更新系统中的libc库。可以通过添加新的软件源来获取合 适版本的libc:

1
2
3
4
5
# 1. 添加源
sudo vi /etc/apt/sources.list
# 在末尾添加:
deb http://th.archive.ubuntu.com/ubuntu jammy main
# 保存退出 (:wq)

保存后,更新软件源并安装libc6:

1
2
3
# 2. 更新并安装 libc6
sudo apt update
sudo apt install libc6

系统提示Meson版本过旧 如果AFL++在构建或使用过程中提示Meson版本过旧,可以通过卸载发行 版自带的旧版本并使用PyPI安装新版本来解决,示例命令如下:

1
2
3
4
sudo apt update
sudo apt install python3-pip
sudo apt remove meson
pip3 install meson

然后配置编译环境,使AFL++及目标程序优先使用新安装的LLVM/Clang工具链(示例仍以LLVM 15 为例):

1
2
3
4
5
export CC=/usr/bin/clang-15
export CXX=/usr/bin/clang++-15
export PKG_CONFIG=/usr/bin/pkg-config # 指定 pkg-config 路径,供构建系统查询依赖
export AR=/usr/bin/ar
export NM=/usr/bin/nm

依赖准备完成后,将提供的exif程序移动到下面路径,即可使用QEMU模式启动模糊测试:

1
$HOME/AFLplusplus/afl-fuzz -Q -i $HOME/qemu_fuzz/exif-samples/jpg/ -o $HOME/qemu_fuzz/out -s 123 -- $HOME/qemu_fuzz/install/bin/exif @@

其中,-Q表示启用QEMU模式,其他参数含义与前一任务类似。需要注意的是,QEMU模式通过动 态二进制翻译实现插桩,通常比基于源码的插桩模式更慢,因此模糊测试需要更长时间,请耐心等待。

image-20251229194246356

分析得到的Crash

当AFL++在QEMU模式下发现崩溃样本后,可以使用GDB对相应输入进行调试,分析程序异常的根 本原因。例如:

1
2
gdb --args $TARGET_BIN <crash_file_path>
gdb --args $HOME/qemu_fuzz/install/bin/exif "$HOME/qemu_fuzz/out/default/crashes/id:000000,sig:06,src:000469,time:1155795,execs:210356,op:quick,pos:665,val:+2"

任务:请结合网上公开信息研究exif中已知的CVE-2009-3895或CVE-2012-2836漏洞。参考Task2中 的分析流程,利用afl-fuzz-Q获得崩溃样本,并通过gdb进行调试,找出至少一个漏洞的触发路径与 成因。在实验报告中详细说明该漏洞是如何产生的,以及恶意输入如何导致程序崩溃或异常行为(需配合 调试截图说明)

image-20251229194431166

image-20251229194601198

image-20251229194605301

image-20251229194608189

调试现象

程序运行后中止并抛出 SIGABRT 信号。
GDB 输出显示错误信息 realloc(): invalid next size。这表明 glibc 的内存分配器在执行 realloc 操作时,检测到当前堆块(Chunk)的元数据(如头部的大小字段)已被覆盖或损坏,从而触发了安全机制主动终止程序。

漏洞定位

通过 bt(栈回溯)发现,崩溃发生在 exif_entry_fix 函数(exif-entry.c:253)调用 exif_entry_realloc 的过程中。

成因分析 (Root Cause)

这是一个典型的堆溢出导致的堆破坏 (Heap Corruption triggered by Heap Overflow)

根据调用栈分析,libexif 在处理畸形的 EXIF 标签时,调用了 exif_entry_fix 试图修正条目大小。由于输入文件中的标签长度字段被恶意篡改(例如伪造了一个巨大的长度),导致程序在之前的数据写入操作中越界,覆盖了堆内存中相邻块的头部信息(Metadata)。
当程序随后调用 realloc 试图调整内存大小时,分配器检查发现堆块的 next size 字段非法(invalid next size),从而判定堆结构已损坏并终止运行。这与 CVE-2009-3895(整数溢出导致堆溢出)的特征完全一致。


软件安全实验 SEEDlabs 模糊测试技术实验(基于AFL)
http://example.com/2026/test40/
作者
sangnigege
发布于
2026年4月15日
许可协议