25、Fuzzing基础:什么是Fuzzing、AFL基础使用、Crash分析

说到漏洞挖掘,很多人第一反应是盯着代码一行行看。说实话,我早年也这么干过,盯着反汇编看到眼睛发酸。后来接触了Fuzzing,才明白什么叫「让机器替你打工」。Fuzzing说白了就是——往程序里灌各种奇奇怪怪的输入,看它会不会炸。

你想想看,一个程序处理正常输入时当然没问题。但你要是给它一个超长的字符串、一个负数、或者一个精心构造的畸形文件呢?很多隐藏的bug,就是这么被炸出来的。

什么是Fuzzing

Fuzzing(模糊测试)是一种自动化测试技术。核心思路很简单:生成大量随机或半随机的数据,作为输入喂给目标程序,然后监控程序是否崩溃、挂起或出现异常行为。

我刚开始做逆向时,总觉得Fuzzing是「瞎猫碰死耗子」。直到有一次,我用AFL对一个图像解析库跑了三天,炸出了7个crash,其中两个还是高危漏洞。嗯,从那以后我再也不敢小看它了。

Fuzzing的分类大致如下:

类型 特点 适用场景
黑盒Fuzzing 不知道内部结构,纯随机生成输入 协议、文件格式测试
白盒Fuzzing 基于源码分析,生成覆盖率高 有源码的项目
灰盒Fuzzing 结合插桩和覆盖率反馈 二进制程序、闭源软件

我个人最常用的是灰盒Fuzzing。为什么呢?因为它效率高。黑盒像无头苍蝇,白盒又太依赖源码。灰盒通过插桩获取覆盖率信息,能智能地引导测试数据往「没走过」的代码路径去。

核心要点:Fuzzing不是乱撞,而是有策略地探索程序的每一条代码路径。覆盖率越高,找到漏洞的概率越大。

AFL基础使用

AFL(American Fuzzy Lop)是目前最流行的灰盒Fuzzer之一。我当年第一次用AFL时,被它的简洁震惊了——一个命令行就能跑起来。

先说说AFL的工作原理。它通过编译时插桩,在目标程序的每个基本块插入探针。运行时,AFL会监控哪些代码路径被执行了。如果某个输入触发了新的路径,AFL就会保留这个输入,并基于它进行变异。

安装AFL很简单:

# 下载源码
wget http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz
tar -xzf afl-latest.tgz
cd afl-2.52b
make
sudo make install

使用AFL的基本流程分三步:

  1. 编译目标程序:用afl-gcc或afl-clang替换原来的编译器
  2. 准备初始种子:给AFL一些合法的输入样本
  3. 启动Fuzzing:运行afl-fuzz开始测试

举个例子,假设我们要测试一个图片解析器:

# 第一步:插桩编译
afl-gcc -o target_program target.c -lm

# 第二步:创建种子目录
mkdir -p input_dir output_dir
# 放一个合法的图片进去
cp test.png input_dir/

# 第三步:启动Fuzzing
afl-fuzz -i input_dir -o output_dir ./target_program @@

这里的@@是个占位符,AFL会用生成的测试文件路径替换它。

个人经验:种子文件的质量直接影响Fuzzing效率。我习惯准备3-5个不同大小的合法样本,而不是只放一个。这样AFL能更快地探索到不同的代码路径。

跑起来之后,你会看到一个实时更新的界面。里面有几个关键指标:

  • cycles done:已经完成的测试轮数
  • total paths:发现的代码路径数
  • unique crashes:唯一的崩溃数
  • unique hangs:唯一的挂起数

我曾经遇到过一个情况:跑了12小时,unique crashes一直是0。当时差点放弃了。后来检查发现,是种子文件选错了——我放了一个空文件进去,导致AFL一直在无效路径上打转。换了种子后,半小时就炸出了第一个crash。

避坑指南:我曾经因为忘记设置echo core > /proc/sys/kernel/core_pattern,导致AFL无法捕获crash信息。跑了两天,一个crash都没记录到。嗯,这种低级错误犯一次就够了。

Crash分析

炸出crash只是第一步。真正的重头戏是分析这些crash——判断它是不是真正的漏洞,能不能被利用。

AFL会把crash文件保存在output_dir/crashes/目录下。每个文件都有一个唯一的ID。你可以用目标程序重新运行这些文件,看看会不会复现崩溃。

# 复现crash
./target_program output_dir/crashes/id:000000,sig:11,src:000001,op:flip1,pos:4

如果程序崩溃了,下一步就是用调试器分析。我个人习惯用GDB:

gdb ./target_program
(gdb) run < crash_file
# 程序崩溃后
(gdb) bt  # 查看调用栈
(gdb) info registers  # 查看寄存器状态
(gdb) x/10i $eip  # 查看崩溃位置的指令

分析crash时,我重点关注几个方面:

  • 崩溃类型:是段错误(SIGSEGV)还是非法指令(SIGILL)?
  • 崩溃位置:在哪个函数、哪条指令上崩的?
  • 可控性:崩溃时,寄存器或内存中的数据是否被输入控制?

举个例子,有一次我分析一个crash:

Program received signal SIGSEGV, Segmentation fault.
0x0804a1b2 in parse_header (data=0xbffff2a0, size=0x41414141) at parser.c:42
42      memcpy(buf, data, size);

看到size=0x41414141了吗?0x41就是大写字母A的ASCII码。这说明输入数据直接控制了拷贝的大小,典型的缓冲区溢出漏洞。

判断标准:如果崩溃时EIP或数据寄存器能被输入控制,那这个漏洞大概率是可利用的。如果只是空指针解引用,通常只能造成拒绝服务。

分析完crash后,我会做三件事:

  1. 最小化输入:用AFL自带的afl-tmin工具,把crash文件缩小到最小复现样本
  2. 确认漏洞类型:是溢出、越界、还是释放后使用?
  3. 编写POC:写一个简单的Python脚本,能稳定触发漏洞

说实话,crash分析是最考验功力的环节。Fuzzing本身是体力活,但分析crash需要你对底层机制有深入理解。我建议初学者先从简单的crash入手——比如那些明显是缓冲区溢出的,慢慢积累经验。

我的习惯:每次分析完crash,我都会在笔记里记录三样东西:崩溃的输入特征、触发的代码路径、以及修复建议。时间长了,这就是一份宝贵的漏洞模式库。

最后说一句:Fuzzing不是万能的。它找不到逻辑漏洞,也发现不了需要特定时序触发的bug。但如果你要挖内存破坏类的漏洞,Fuzzing绝对是最趁手的工具之一。

Fuzzing基础知识体系 Fuzzing 基础 什么是Fuzzing 黑盒Fuzzing 白盒Fuzzing 灰盒Fuzzing AFL基础使用 插桩编译 准备种子文件 启动afl-fuzz Crash分析 复现崩溃 GDB调试分析 判断可利用性 核心目标:发现并分析程序中的隐藏漏洞

公众号:蓝海资料掘金营,微信deep3321