LibFuzzer实战复现
默文 历史精选 327浏览 · 2025-03-01 02:39

LibFuzzer

LibFuzzer 是一个in-process(进程内的),coverage-guided(以覆盖率为引导的),evolutionary(进化的) 的 fuzz 引擎,是 LLVM 项目的一部分,主要用于对C/C++程序进行Fuzz测试。LibFuzzer三个特性的具体含义为:

in-process:不会为每个测试用例启动一个进程,而是将所有的测试数据投放在同一个进程的内存空间中

coverage-guided:对每一个测试输入都进行代码覆盖率计算,不断累积测试用例使得代码覆盖率最大化

evolutionary:结合了变异和生成两种形势的Fuzz引擎

变异:基于已有的数据样本,通过一些变异规则,产生新的测试用例

生成:通过对目标协议或接口规范进行建模,从零开始产生测试用例

LibFuzzer与待测的library进行链接,通过向指定的fuzzing入口(即target函数)发送测试数据,并跟踪被触达的代码区域,然后对输入的数据进行变异,以达到代码覆盖率最大的目的,其中代码覆盖率的信息由LLVM的SanitizerCoverage工具提供。

使用环境

现在的 libfuzzer 已经被集成在 Clang 中,Clang是一个类似GCC的C/C++语言编译工具。所以直接安装 Clang 即可。

检查是否安装成功

图片加载失败


CVE-2016-5180-fuzz复现

先来实战一下再来详细分析,CVE-2016-5180 漏洞为 Heap overflow in c-ares ,此错误是导致 ChromeOS 漏洞利用成为可能的两个错误链之一: 重新启动后以 guest 模式执行代码

查找时间:< 1 秒。

GitHub地址 : google/fuzzer-test-suite:模糊测试引擎的测试集 --- google/fuzzer-test-suite: Set of tests for fuzzing engines

配置环境

编写fuzz函数

因为在上面的项目地址中已经有了target.cc文件所以这一步您可以选择跳过。

编写 target.cc 实现LLVMFuzzerTestOneInput函数,将LibFuzzer输入的字节流进行转换,并调用ares_create_query函数。

编译Fuzz target

使用fuzzer-test-suite提供的编译脚本,执行 build.sh 脚本,会自动调用 custom-build.shcommon.sh 进行编译。

文件脚本布局如下,直接执行 build.sh 脚本会自动从 github 上面拉取下来项目,如果需要指定文件然后解压并进行编译需要修改脚本,后面会讲如果改造脚本,实现本地编译并进阶到 promptfuzz

图片加载失败


执行 build.sh 脚本,需要注意的是不能在当前目录下直接执行 build.sh,这个目录管理很干净确实不错。

图片加载失败


编译完成后会出现 SRC(源码地址),BUILD(构建项目的文件夹),可执行fuzz程序。

图片加载失败


执行这个fuzz程序,因为样例简单没1秒就会出crash

图片加载失败


libfuzzer 输出参数详细介绍

字段
含义
当前值分析
cov
代码覆盖率(覆盖的基本块数)
初始值14,逐步增长至29
ft
模糊器跟踪的独特特征数(包含路径/条件分支等)
从15增至45,表明发现新执行路径
corp
语料库状态(有效测试用例数/总字节数)
9个用例,21字节总规模
exec/s
每秒执行次数
0值表明测试初期或资源受限
rss
内存占用(Resident Set Size)
稳定在31MB,没有内存泄漏迹象
L
输入长度(实际长度/允许最大值)
多数用例在4字节限制内
MS
变异策略组合(如 ChangeBit-CrossOver
显示输入变异的智能组合

asan信息分析

第一个阶段为 libfuzzer输出的信息,下面是asan出现的信息。

错误类型

heap-buffer-overflow:表示程序尝试访问未分配的内存区域,通常是因为数组越界或访问已释放的内存。

报错堆地址 0x603000032a25

图片加载失败


图片加载失败


编译脚本

执行 build.sh 脚本,会自动调用 custom-build.shcommon.sh 进行编译。因为我使用的这三个脚本进行自动编译,当然也可以手动进行编译,可以跳过这一部分。

图片加载失败


build.sh脚本

custom-build.sh编译选项解释

注意需要将原始脚本中的-fsanitize-coverage=trace-pc-guard替换为-fsanitize=fuzzer,否则执行Fuzz时会出现错误:-fsanitize-coverage=trace-pc-guard is no longer supported by libFuzzer。

common.sh脚本

构建 target.cc

大致了解了三个 .sh 脚本所作的事之后,开始构造 target.cc 这个 cpp 用来测试fuzz库的一个接口。

在使用 LibFuzzer 时,第一步就是要实现 target 函数(LLVMFuzzerTestOneInput),该函数传参使用两个参数@Data,@size,即以byte 数组作为函数输入,然后在函数内部调用你想要 fuzz 的库函数并传入 Data

target函数名称参数类型返回值类型都不能改变是定死的。

编译 target

编写好 target 之后就需要进行编译,如果看过上面的sh脚本那肯定对编译选项就不太陌生,下面还是详细介绍一下常见的编译选项。

编译选项
附件参数
作用
-g

保留符号
-O (uppercase o)
-O0 -O1 -O2 -O3
优化等级,越大优化越多
-o (lowercase o)
file_name
指定编译后的文件名称
-I (uppercase i)
header_file_path
添加头文件路径
-fsanitize

启用libfuzzer进行插装

=fuzzer
链接libFuzzer库文件,使用libFuzzer的main

=fuzzer-no-link
不链接libFuzzer,适用于有main函数的源码

=address
堆栈溢出、UAF(悬垂指针)

=undefined
未定义行为检测(除零、类型转换错误等)

=memory
检测未初始化的内存访问
-fsanitize-coverage

启动覆盖率统计

trace-pc-guard
记录每个基本块的执行 现在-fsanitize=fuzzer代替

trace-cmp
为每个比较操作插入调用,追踪比较。

trace-div
为除法和取模操作插入调用,追踪除法。

执行 fuzz

编译完成后会出现一个可执行的 fuzz 文件,最简单的启动方式是直接启动,但是有许多附件参数稍后详细列出。

在启动 fuzz-target 后,程序会一直进行 fuzz 并输出相关信息,直到出现 crash 才会停止。

flag
默认值
作用
verbosity
1
运行时输出详细日志
seed
0
随机种子。如果为0,则自动生成随机种子
runs
-1
测试运行的次数(-1表示无限)
max_len
0
测试输入的最大长度。若为0,libFuzzer会自行猜测
shuffle
1
为1表示启动时打乱初始语料库
prefer_small
1
为1表示打乱语料库时,较小输入更优先
timeout
1200
超时时长,单位为秒。如果单次运行超过时长,Fuzz将被终止
max_total_time
0
最大运行时长,单位为秒。若为正,表示Fuzz最长运行时间
help
0
为1表示打印帮助信息
merge
0
为1表示在不损失代码覆盖率的情况下,进行语料库合并
merge_control_file
0
指定合并进程的控制文件,用于恢复合并状态
minimize_crash
0
为1表示将提供的崩溃输入进行最小化。与-runs = N或-max_total_time = N一起使用以限制尝试次数
jobs
0
job的数量,多个job将被分配到workers上执行,每个job的stdout/stderr被重定向到fuzz-<JOB>.log
workers
0
worker的数量,为0将使用min(jobs, number_of_cpu_cores/2)
reload
1
设置重新加载主语料库的间隔秒数。在并行模式下,在多个job中同步语料集。为0表示禁止
reduce_inputs
1
为1表示尝试减少输入数据的大小,同时保留其完整的特征集
rss_limit_mb
2048
RSS内存用量限制,单位为Mb。为0表示无限制
purge_allocator_interval
1
清理缓存的建个时长,单位为秒。当指定rss_limit_mb且rss使用率超过50%时,开始清理。为-1表示禁止
malloc_limit_mb
0
单次malloc申请内存的大小限制,单位为Mb。为0则采用rss_limit_mb进行限制
detect_leaks
1
为1,且启用LeakSanitizer消毒器时,将在Fuzz过程中检测内存泄漏,而不仅是在Fuzz结束时才检测
print_coverage
0
退出时打印覆盖率信息
print_corpus_stats
0
退出时打印语料信息
print_final_stats
0
退出时打印统计信息
only_ascii
0
为1表示只生成ASCII(isprint + isspace)字符作为输入
artifact_prefix
0
将fuzzing artifacts(crash、timeout等file)保存为文件时所使用的前缀,即文件将保存为$(artifact_prefix)file
exact_artifact_path
0
将单个fuzz artfifact保存为文件时所使用的前缀。将覆盖-artifact_prefix,并行任务中不要使用相同取值

例如使用 -print_coverage=1 开启退出时打印覆盖率信息

图片加载失败


自定义使用 seed这里执行步骤比第一次fuzz要少了很多,可见种子的重要性。

图片加载失败


语料库的使用

fuzz 中语料库是最重要的一部分,提供一个好的语料库可以节省很多资源。模糊测试的核心原理是通过动态生成或改造输入数据来探测系统漏洞。与完全随机生成测试用例不同,该方法采用种子语料库作为基础模板进行智能变异,特别在处理结构化输入时展现显著优势。接下来介绍语料库的使用。

使用 corpus 来提供 fuzz 最初的一个或多个存放种子语料。

可见当我提供好的种子的时候,执行步骤显而易见的减少了。

图片加载失败


当种子很多的时候,那有些种子其实就没必要去测试了,可以使用 merge 来进行种子合并。这点 AFL 中也有相对的工具tmin,cmin。

tmin_corpus 为存放精简后的种子,corpus 为原始输入种子库。第五行显示MERGE-OUTER: 19 files这是输入了19个种子,最后一行 MERGE-OUTER: 15 new files with 65 new features added; 31 new coverage edges,从19个种子优化到了15个种子。

图片加载失败


提取语料库

在测试的时候,可以把 fuzz 产生的种子保存下来,以后继续 fuzz 可能会参考这些种子。

传入一个空文件夹,libfuzzer 就会把过程中产生的种子保存到这个文件夹中。

图片加载失败


字典使用

当然为了演示这里的字典是乱写的,真实fuzz的时候就需要针对程序写fuzz,比如在某个程序中有一个 strcmp("mowen",buf);那通过随机变异来生产"mowen"是很困难的,就需要导入字典来增加fuzz速度。

图片加载失败


图片加载失败


CVE-2014-0160

CVE-2014-0160(Heartbleed,心脏出血漏洞)是OpenSSL库中的一个严重漏洞,允许攻击者通过恶意构造的TLS心跳请求(Heartbeat Request)读取服务器内存中的敏感信息(如私钥、用户会话数据)。

之前fuzz复现使用的自动脚本进行编译,现在我们使用手动编译,然后下节我们修改自动脚本完成本地文件的自动编译。

复现环境 GitHub地址:libfuzzer-workshop/lessons/05/README.md at master · Dor1s/libfuzzer-workshop

google/fuzzer-test-suite:模糊测试引擎的测试集 --- google/fuzzer-test-suite: Set of tests for fuzzing engines

查找时间:< 3 秒。

编译openssl库

编译完成之后头文件就会出现在./include下。

图片加载失败


编写target.cc,脚本都来自于GitHub,目前暂时不详细讲如何编写target.cc,目的就是调用要fuzz的库函数就行。

然后需要先创建两文件 server.keyserver.pem

编译 target.cc

然后执行 target 等一会就会报出 crash。显示 heap-buffer-overflow ,是一个堆溢出 。

图片加载失败


tls1_process_heartbeat

VScode调试

对于大型大型开源项目,单纯使用gdb调式会稍微复杂一点(其实也是很方便的)。个人还是喜欢gdb调试,vscode调试虽然可以在源码上直接断点,但也就这一个优点了。

前置条件需要自行安装,然后通过ssh或wsl连接到linux中。然后安装 Native Debug

图片加载失败


在运行和调试窗口中点击创建 launch.json 文件 。

图片加载失败


环境选择 GDB

图片加载失败


默认配置为这样,什么都不用改就需要改一个 target 为你要fuzz的程序就可以。

图片加载失败


那对于我来说的配置为这样。

调试的时候显然不太能用 fuzz 生成的程序,所以要清理之前编译的文件。

重新编译 target.cc

还需要篡改 target.cc 文件,因为不用 fuzz了,所以需要有main函数来引导执行。先把crash 传进来,这里使用手动赋值的方式,你也可以写一个文件的接口。手动赋值需要注意一下16进制的转换。

图片加载失败


使用全局搜索找到函数的定位处,然后右边下断点就行。

图片加载失败


调试进来在tls1_process_heartbeat处确实是payload未检测长度,可以完成堆溢出,这里继续分析漏洞如果利用,如果有感兴趣的师傅可以自行复现。

图片加载失败


0 条评论
某人
表情
可输入 255

没有评论