浅尝AFL++
入门一下fuzzing
简单安装配置
这里先不用源码编译的方式, 直接pull 一个镜像来尝尝鲜
docker pull aflplusplus/aflplusplus:latest |
创建demo
创建一个目录
mkdir -p ~/afl-demo/in |
准备一个.c文件
|
这个程序故意把最多128字节读进32字节缓冲区里, 所以很容易被 fuzz 出崩溃.
程序是从文件获取输入的, 所以再准备一个文件
printf 'AAAA' > in/seed.txt |
启动
docker run -it --rm -v ~/afl-demo:/src aflplusplus/aflplusplus:latest |
启动之后我们的文件都在/src下, 现在开始编译程序
cd /src |
-O0 表示编译的时候不做优化, 跟gcc的参数是一样的
-g 生成调试符号
简单运行测试一下
./demo in/seed.txt |
正常是不会有输出的, 也不会卡住什么的
开始fuzz
afl-fuzz -i in -o out -- ./demo @@ |
因为这个程序是从文件读输入, 所以命令里要用 @@
这是 AFL++ 官方 quick start 的标准写法:如果目标从文件读取, AFL++ 会把 @@ 替换成自动生成的测试用例路径
大概页面是这样, 说明已经在fuzzing了
它是不会自动停止的, 这里大概跑了11min, saved crashes 变为了1, 说明有一个样例导致这个程序崩溃了.
这时我们按下 ctrl + c就能停止, 导致程序crash的输入会存放在out/default/crashes/目录下
[AFL++ 8f4d96b5a055] /src # ls out/default/crashes |
README不用管, 我们直接看id:000000,sig:11,src:000001,time:271,execs:413,op:havoc,rep:7
[AFL++ 8f4d96b5a055] /src # cat out/default/crashes/id\:000000\,sig\:11\,src\:000001\,time\:271\,execs\:413\,op\:havoc\,rep\:7 |
gdb一下
可以看到最终是在main函数返回之前的canary验证这里就死了
确实是溢出导致覆盖了canary
fuzzing 101 通关以及踩坑记录
1.Exercise 1 - Xpdf (Afl-clang-fast, Afl-fuzz, GDB)
For this exercize we will fuzz Xpdf PDF viewer. The goal is to find a crash/PoC for CVE-2019-13288 in XPDF 3.02.
项目构建
下载目标程序并解压, 最好提前创建一个目录
wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz |
正常构建(非必要)
cd xpdf-3.02 |
这里下载些pdf来进行测试
cd $HOME/fuzzing_xpdf |
测试一下
$HOME/fuzzing_xpdf/install/bin/pdfinfo -box -meta $HOME/fuzzing_xpdf/pdf_examples/helloworld.pdf |
能正常使用, 说明安装没什么问题, 现在使用 afl-cc 和 afl-c++ 来编译安装xpdf. 先把之前的目标文件和可执行文件清理一下
rm -r $HOME/fuzzing_xpdf/install |
接着用afl特制编译器来再做一次编译安装的动作, 因为我是用docker镜像的, 所以稍微会有点不一样, 先启动一下
docker run -it --rm -v ~/afl-demo/fuzzing_xpdf:/src aflplusplus/aflplusplus:latest |
然后进入之前clone的源码的目录下
cd /work/xpdf-3.02 |
用官方镜像的话就不需要更换llvm了
export CC=afl-cc |
配置并编译
./configure --prefix="$HOME/fuzzing_xpdf/install/" |
之前做测试的pdf被删了, 重新搞一下
cd $HOME/fuzzing_xpdf |
因为教程中后面两个示例pdf都404了, 这里我们的初始语料库就只能有一个pdf. 可以非常直观的看到语料库对fuzz的影响.
返回上级目录, 然后开始fuzz
fuzzing
cd .. |
等待
我这里跑了大概7分钟才跑出来第一个crash, 但是知乎的这个师傅只用了1分多钟
crash
ok, 得到样例之后我们可以复现一下这个漏洞.
…中间出现了一点插曲, 不小心按错退出了docker, 因为在run的时候加了 –rm, 他直接就给我删掉了, out的路径还不是挂载的那个目录, 所以得再来一次. 建议还是把out放在挂载在宿主机的目录下
重新编译, 加上-g方便后续调试
rm -r $HOME/fuzzing_xpdf/install # 记得换成自己安装的目录 |
然后gdb启动一下
gdb --args /src/work/xpdf/install/bin/pdftotext /src/work/out/default/crashes/id:000001 |
发现, 造成崩溃的原因跟 CVE-2019-13288 描述的不一样
这里应该是我的语料库太单调了, 只有一个 helloworld.pdf , 然后alf++ 对我的 helloworld.pdf 进行了疯狂的变异, 变了些神秘的16进制字符出来, 最后就死在了这个getchar()这, 既然这样, 我们就需要给语料库添加点素材
cd /in |
然后依旧是编译启动, 这里就省略了
居然这么快就跑了一个crash出来… 最后是跑了大概一分钟, 总共3个crash
依旧是
rm -r $HOME/fuzzing_xpdf/install # 记得换成自己安装的目录 |
然后就启动
gdb --args /src/work/xpdf/install/bin/pdftotext /src/work/out/default/crashes/id:000001,sig:11,src:000000,time:35363,execs:8962,op:havoc,rep:1 /dev/null |
发现依旧复现不出来…
这次是触发了一个空指针异常, 感觉是我找的pdf过于现代了, 包含了对象流和压缩结构这种比较新的东西, xpdf在解析对象的时候就 crash 了, 没法触发cve中所描述的无限递归导致的崩溃…
因为原课程中的另外两个pdf已经找不到了, 就没法很顺利的复现这个cve, 但不要紧, 个人认为能否复现cve不重要, 这个Exercise1 要我们做的就是熟悉Afl-clang-fast, Afl-fuzz, GDB而已, 我也确实做到了
2.Exercise 2 - libexif (Afl-clang-lto, Fuzz libraries, Eclipse IDE)
This time we will fuzz libexif EXIF parsing library. The goal is to find a crash/PoC for CVE-2009-3895 and another crash for CVE-2012-2836 in libexif 0.6.14.
项目构建
下载链接:
解压缩
tar -xzvf libexif-0.6.14.tar.gz |
老样子进docker并且挂载目录, 这里不用 –rm 来启动了, 有些库这个镜像里面没有, 每次装完之后退出了就全部没掉了…
docker run -it -v ~/afl-demo/libexif-0.6.14/libexif-0.6.14:/src aflplusplus/aflplusplus:latest |
安装配置参考: https://github.com/libexif/libexif
export CC=afl-clang-lto |
由于 libexif 是一个库, 我们就需要另外一个使用这个库的程序来进行fuzzing, 这里选择exif command-line
cd $HOME/fuzzing_libexif |
安装完成之后运行测试一下
建立语料库, 这里从 https://github.com/ianare/exif-samples 搞点样例下来
cd $HOME/fuzzing_libexif |
测试一下
$HOME/fuzzing_libexif/install/bin/exif $HOME/fuzzing_libexif/exif-samples-master/jpg/Canon_40D_photoshop_import.jpg |

fuzzing
让我们开始fuzz吧
afl-fuzz -i $HOME/fuzzing_libexif/exif-samples-master/jpg/ -o $HOME/fuzzing_libexif/out/ -s 123 -- $HOME/fuzzing_libexif/install/bin/exif @@ |
ok, 现在重新编译一下, 用带-g的来携带调试信息
cd /src |
gdb 分别跑一下4个crash, 感觉教程里面的 Eclipse 不是很必要啊, 要看源码的话gdb也可以
gdb --args ./install/bin/exif ./out/default/crashes/id\:000000\,sig\:11\,src\:000154\,time\:90716\,execs\:101045\,op\:havoc\,rep\:4 |
crash 0, 2
在一个memcpy崩掉了
搜了一下, 应该是 CVE-2007-6352 , 描述基本一致
crash 1
报错信息为<error: Cannot access memory at address 0x5946c51d9b8f> , 比较像是越界导致的一个非法堆地址, 跟fuzzing 101 中 solution 的演示是同一个地方, 具体cve没搜到…
crash 3
在exif-loader.c中无限递归, 是 CVE-2007-6351 , 描述基本一致

简单总结
afl-clang-fast
在 LLVM mode 下工作, 是常规的 clang/LLVM 插桩模式.
兼容性更好, 构建更省心, 老项目、古早 autotools/libtool 项目通常更容易先编通. 这个模式是 AFL++ 的常规 LLVM 方案.
afl-clang-lto
在链接阶段做插桩, 要求目标能走 LTO 流程, 并且 AFL++ 官方文档写明它需要 LLVM 12 或更高.
官方明确说它生成的二进制通常 跑得稍快、覆盖更好.
它的一个重要优势是能做到 non-colliding edge coverage, 也就是边覆盖冲突更少, 反馈更精细;官方 README 直接拿它和 afl-clang-fast CLASSIC 的碰撞作对比.
但它更挑工具链. 官方专门提醒: 出问题时要试 AR=llvm-ar RANLIB=llvm-ranlib AS=llvm-as, 有些目标还得设 LD=afl-clang-lto 或 LD=afl-ld-lto
所以fuzz比较老的项目时先用 fast , lto 限制比较多, 但他更高效.
Exercise 3 - TCPdump (ASan, Sanitizers)
In this exercise we will fuzz TCPdump packet analyzer. The goal is to find a crash/PoC for CVE-2017-13028 in TCPdump 4.9.2.
项目构建
下载链接
https://github.com/the-tcpdump-group/tcpdump-htdocs/blob/master/release/tcpdump-4.9.2.tar.gz
然后是 libcap, 这个是tcpdump的依赖库
挂载目录启动docker, 以后大概都不会加 –rm 参数了…
docker run -it -v ~/afl-demo/tcpdump-4.9.2:/src aflplusplus/aflplusplus:latest |
开始构建项目
export CC=afl-clang-lto |
然后是 tcpdump-4.9.2, 因为这时个相当有年代的项目, 直接编译会因为一些隐式函数声明和隐式整型视为致命错误并拒绝编译, 这里需要临时放宽编译器的语法限制. 这里配置tcpdump的configure时也要加AFL_USE_ASAN=1, 因为它的依赖库也加了ASAN
cd ../tcpdump-4.9.2/ |
fuzzing
afl-fuzz -m none -i ./tcpdump-4.9.2/tests/ -o ./out/ -s 123 -- ./install/sbin/tcpdump -vvvvXX -ee -nn -r @@ |
emm… 用docker挂载目录跑这个的话会变得很慢, 想了想下次启动还是不要挂载了, 感觉没很大必要
挂着干别的事情去了, 回来一看才1个crash…
crash 0
这里暂停一下看一下crash的原因, 有了 ASan 我们就不需要再重新编译然后再用gdb看了, ASan 直接就给我们整理好了
./install/sbin/tcpdump -vvvvXX -ee -nn -r ./out/default/crashes/id:000000,sig:06,src:003062,time:14509845,execs:3468706,op:arith16,pos:16,val:+29 |
直接吐了一大坨东西出来, 有一行比较显眼的红色的 heap-buffer-overflow
貌似跟课程中描述的 CVE-2017-13028 不太一样
crash的地方是
这里用 addr2line 去看看在什么地方崩掉了
[AFL++ 0aafd6e86b3c] /src # addr2line -e /src/install/sbin/tcpdump -f 0x53eccf |
在ospf6_print_lshdr 函数, 搜索了一下, 应该是这个 cve-2018-14880
小结之ASan
刚刚编译的时候都用到了AFL_USE_ASAN=1, 目的是让编译器会在程序每一次读写内存的前后, 都强行插入一段检查代码, 哪怕程序只是越界读写了 1 个字节, 或者使用了一块刚刚释放的内存, ASan 都会立刻报警并让程序崩溃. 虽然他确实很慢很慢
假如有这样一段代码, 越界写了一字节
char buf[10]; |
如果不加 ASAN, 这个字节写到了相邻的合法内存里,程序并没有挂掉,继续正常往下跑. AFL++ 认为这是一次正常执行,彻底漏掉了这个漏洞.
如果加了 ASAN, ASAN 瞬间发现越界写并调用 abort(). 让AFL++ 捕获到了一个 Crash
| 对比维度 | 加上 ASAN 的构建 | 不加 ASAN |
|---|---|---|
| 执行速度 | 慢. 通常会拖慢原生速度的 2 倍到 3 倍. | 极快. 几乎没有额外负担,跑分极高. |
| 内存消耗 | 大. 需要分配大量的影子内存来监控. | 极小. 与正常运行程序无异. |
| 漏洞发现能力 | 极高. 能发现非常隐蔽的越界读、Use-After-Free 等静默内存错误. | 低. 很多微小的内存溢出不会导致程序立刻崩溃,从而被漏掉. |
| 误报 | 可能因为内存耗尽产生假崩溃. | 几乎没有假阳性,崩了就是真崩了. |
Exercise 4 - LibTIFF (Code coverage, LCOV)
This time we will fuzz LibTIFF image library. The goal is to find a crash/PoC for CVE-2016-9297 in libtiff 4.0.4 and to measure the code coverage data of your crash/PoC.
项目构建
下载链接
老几样, 使用lvoc(覆盖率检测)编译libtiff
cd /src/tiff/tiff-4.0.4 |
获取覆盖率, 这里要求 gcov 和 gcc 的版本是一致的, 不然就会报错, 我这里的
[AFL++ 0aafd6e86b3c] /src/tiff/tiff-4.0.4 # gcc --version gcc (Ubuntu 11.5.0-1ubuntu1~24.04.1) 11.5.0 |
所以统一用 11 了
[AFL++ 0aafd6e86b3c] /src/tiff/tiff-4.0.4 # which gcov-11 |
获取覆盖率
cd /src/tiff/ |
- –gcov-tool /usr/bin/gcov-11 –zerocounters –directory ./
:清空之前运行留下的覆盖率计数,避免数据污染
lcov –gcov-tool /usr/bin/gcov-11
–capture –initial
–directory ./
–output-file app.info: 记录程序“所有可覆盖代码”的初始基线(还没运行时)/src/tiff/install/bin/tiffinfo -D -j -c -r -s -w /src/tiff/tiff-4.0.4/test/images/palette-1c-1b.tiff: 运行需要分析的应用
lcov –gcov-tool /usr/bin/gcov-11
–no-checksum
–directory ./
–capture
–output-file app2.info: 记录程序实际运行后“真正被执行到的代码”
将结果转化生成HTML输出
genhtml --highlight --legend -output-directory ./html-coverage/ ./app2.info |
编译文件
CC=afl-clang-lto ./configure --prefix="/src/tiff/install/" --disable-shared |
尽可能多使用参数, 这样fuzz到漏洞代码的几率更大些
fuzzing
afl-fuzz -m none -i /src/tiff/tiff-4.0.4/test/images/ -o /src/tiff/out/ -s 123 -- /src/tiff/install/bin/tiffinfo -D -j -c -r -s -w @@ |
这次的比较快
/src/tiff/install/bin/tiffinfo -D -j -c -r -s -w /src/tiff/out/default/crashes/ |
crash
好几个crash都是在这里崩的… 对应cve应该是 cve-2016-9297 虽然描述没说是哪个函数, 但看了下详情 Bug 2590 - CVE-2016-9297: segfault in _TIFFPrintField (tif_print.c:127) 基本跟我们这个一致
猜测是运行环境和拦截方式不同, 导致崩溃的地方稍微有点不同, 当然也不排除不是这个cve
我们可以来看一下刚刚用lcov搜集的覆盖率, 在刚刚执行 genhtml --highlight --legend -output-directory ./html-coverage/ ./app2.info 命令的目录下会有一个 html-coverage 文件夹, 打开里面的index.html就能看到了
因为我的fuzz也才跑了2分钟, 覆盖率可以说是惨不忍睹, 有些地方完全没跑到, 全跑完的话crash应该多很多.
小结之LOV
LCOV 是一个用来收集和展示 代码覆盖率 的工具。它通常配合 gcov 使用,用来统计程序运行时
- 哪些源代码行被执行了
- 哪些函数被调用了
- 哪些分支没有走到
它本身不负责 fuzz,也不负责找漏洞。它的作用更像是一个 分析工具,帮助你看测试是否充分。在实际流程里,一般是:
- 用带覆盖率选项的方式编译程序
- 运行测试或 fuzz 样本
- 用 LCOV 收集覆盖率结果
- 用 genhtml 生成可视化网页报告
这样做的目的,是评估 fuzz 的效果,并找到还没测试到的代码区域
Exercise 5 - LibXML2 (Dictionaries, Basic parallelization, Fuzzing command-line arguments)
For this exercize we will fuzz LibXML2 XML parsing library. The goal is to find a crash/PoC for CVE-2017-9048 in LibXML2 2.9.4.