模糊测试(将可能无效的数据、异常数据或随机数据作为输入内容提供给程序)是在大型软件系统中查找错误的一种非常有效的方式,也是软件开发生命周期的重要组成部分。LibFuzzer 与被测库相关联,并处理在模糊测试会话期间出现的所有输入选择、变更和崩溃报告。LLVM 的排错程序用于协助内存损坏检测以及提供代码覆盖率指标。
本篇文章简述libFuzzer原理,配合各个实例介绍参数功能意义,为最终进一步的完全利用奠定基础
理论篇
libFuzzer是什么?
LibFuzzer在概念上与American Fuzzy Lop(AFL)类似,但它是在单个进程中执行了所有模糊测试。进程内的模糊测试可能更具针对性,由于没有进程反复启动的开销,因此与AFL相比可能更快。
按照官方定义,libFuzzer是一个in-process(进程内的)
,coverage-guided(以覆盖率为引导的)
,evolutionary(进化的)
的fuzz
引擎,是LLVM
项目的一部分。据Google官方技术博客的表述,这三个特性可分别解释为如下意义:
-
**in-process(进程内的)**
:*we mean that we don’t launch a new process for every test case, and that we mutate inputs directly in memory.*我们并没有为每一个测试用例都开启一个新进程,而是在一个进程内直接将数据投放在内存中。 -
**coverage-guided(以覆盖率为引导的)**
:*we mean that we measure code coverage for every input, and accumulate test cases that increase overall coverage.*我们对每一个输入都进行代码覆盖率的计算,并且不断积累这些测试用例以使代码覆盖率最大化。 -
**evolutionary(进化的)**
:fuzz按照类型分为3类,这是最后一种。
第一类是基于生成的
Generation Based
通过对目标协议或文件格式建模的方法,从零开始产生测试用例,没有先前的状态;第二类为基于突变的
Evolutionary
基于一些规则,从已有的数据样本或存在的状态变异而来;最后一种就是基于进化的
Evolutionary
包含了上述两种,同时会根据代码覆盖率的回馈进行变异。
LibFuzzer和要被测试的库链接在一起,通过一个特殊的模糊测试进入点(目标函数),用测试用例feed(喂)要被测试的库。fuzzer会跟踪哪些代码区域已经测试过,然后在输入数据的语料库上产生变异,来最大化代码覆盖。其中代码覆盖的信息由LLVM的SanitizerCoverage插桩提供。
libFuzzer与传统Fuzz相比的特点
传统fuzz面临问题
-
搜索空间过于广泛
-
无法fuzz特定的函数
-
难以fuzz网络协议
-
常规fuzz速度太慢
传统的fuzz
大多通过对已有的样本 按照预先设置好的规则进行变异产生测试用例,然后喂给 目标程序同时监控目标程序的运行状态,这类fuzz
有很多,比如:peach
,FileFuzz
等。找寻漏洞的过程形如下图:
libFuzzer的优势
-
In-process, in-memory
-
会主动引导fuzz过程
-
针对函数/协议级别的fuzz非常有效率
-
1000x的快
-
编写基于libfuzzer的fuzzer很容易
-
可以单独跟随一个单元进行检测
libFuzzer所有的程序的主要功能都是对一些 字节序列进行操作,基于这一个事实(libfuzzer
生成 随机的 字节序列 ,扔给 待fuzz
的程序,然后检测是否有异常出现) 所以在libfuzzer
看来,fuzz
的目标 其实就是一个 以 字节序列为输入的 函数。其过程形如下图:
libFuzzer的理论过程
简单理解libfuzzer
就是,如果我们要fuzz
一个程序,找到一个入口函数,然后利用
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
.......
.......
}
接口(hardness),我们可以拿到libfuzzer
生成的 测试数据以及测试数据的长度,我们的任务就是把这些生成的测试数据 传入到目标程序中 让程序来处理 测试数据, 同时要尽可能的触发更多的代码逻辑。
libfuzzer
已经把 一个fuzzer
的核心(样本生成引擎和异常检测系统) 给做好了, 我们需要做的是根据目标程序的逻辑,把libfuzzer
生成的数据,交给目标程序处理,然后在编译时采取合适的Sanitizer
用于检测运行时出现的内存错误。
实践篇
实践部分建议学习查阅Google的**libFuzzerTutorial**,内容比较完善跟随不同的案例逐个验证libFuzzer的具体功能,但是因为介绍每个功能采用的案例不同,可能对于新手来说割裂感比较严重,我把共用的部分摘取总结出来把这部分变成一个工具书,理想的是在一个具体案例中能够运用一遍所有的常用功能,这样更加连贯,具体实践放在最后的案例篇。
安装
官方推荐使用Ubuntu16.04 x64安装,其本身是llvm
项目的一部分,和clang
是亲兄弟,二者项目源码分别可见于https://github.com/llvm/llvm-project/tree/master/compiler-rt/lib/fuzzer和https://github.com/llvm/llvm-project/tree/master/clang,就在同一个仓库里面,现在稍微新的版本的clang都已经内置libFuzzer了,也可以使用llvm官方提供的脚本进行安装。
#!/bin/bash -eux
# Copyright 2016 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
################################################################################
sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get autoremove -y
sudo apt-get install -y libc6-dev binutils libgcc-5-dev
LLVM_DEP_PACKAGES="build-essential make cmake ninja-build git python2.7"
sudo apt-get install -y $LLVM_DEP_PACKAGES
WORK_DIR=$PWD
mkdir -p $WORK_DIR/src
# Checkout
cd $WORK_DIR/src && git clone --depth 1 http://llvm.org/git/llvm.git
cd $WORK_DIR/src/llvm/tools && git clone --depth 1 http://llvm.org/git/clang.git
cd $WORK_DIR/src/llvm/projects && git clone --depth 1 http://llvm.org/git/compiler-rt.git
cd $WORK_DIR/src/llvm/projects && git clone --depth 1 http://llvm.org/git/libcxx.git
cd $WORK_DIR/src/llvm/projects && git clone --depth 1 http://llvm.org/git/libcxxabi.git
# Uncomment if you want *fresh* libFuzzer from checkouted repository.
#rm -r $WORK_DIR/libFuzzer/Fuzzer
#cp -r $WORK_DIR/src/llvm/projects/compiler-rt/lib/fuzzer/ $WORK_DIR/libFuzzer/Fuzzer
# Build & Install
mkdir -p $WORK_DIR/work/llvm
cd $WORK_DIR/work/llvm
# Consider adding of -DCMAKE_INSTALL_PREFIX=%PATH% flag, if you do not want to
# install fresh llvm binaries into standard system paths.
cmake -G "Ninja" \
-DLIBCXX_ENABLE_SHARED=OFF -DLIBCXX_ENABLE_STATIC_ABI_LIBRARY=ON \
-DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD="X86" \
$WORK_DIR/src/llvm
ninja -j$(nproc)
sudo ninja install
rm -rf $WORK_DIR/work/llvm
编写Fussing Target(hardness)
libFuzzer要求实现一个fuzz target
作为被测对象的接口,这个入口点用来接收 libFuzzer 生成的 测试用例(比特序列)
官方文档中的代码示例如下:
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Non-zero return values are reserved for future use.
}
名称参数返回值类型都不能动,并且注意参数中传来的字节数组Data
是通过底层const修饰了的,也就是不允许修改其中数据。
-
data
是libFuzzer
生成的 测试数据,size
是数据的长度 -
fuzz
引擎会在一个进程中进行多次fuzz
, 所以其效率非常高 -
要能处理各种各样的输入 (空数据, 大量的 或者 畸形的数据...)
-
内部不会调用
exit()
-
如果使用多线程的话,在函数末尾要把 线程
join
fuzzer target(即LLVMFuzzerTestOneInput
函数)目的是作为被测对象与libFuzzer库之间的一个中转接口,其作用在于接受libFuzzer提供的输入数据Data
字节串,(可能还需要进行数据格式转换,)然后传递给实际的被测函数(如上述示例中的DoSomethingInterestingWithMyAPI
)。
官方文档中对其有如下要求:
-
The fuzzing engine will execute the fuzz target many times with different inputs in the same process.
函数会在同一进程中多次执行,即被循环调用。 -
It must tolerate any kind of input (empty, huge, malformed, etc).
必须接受所有格式的输入。 -
It must not exit() on any input.
不允许主动退出,前面说了是循环调用,退出了就没法循环了。 -
It may use threads but ideally all threads should be joined at the end of the function.
可以开线程,但返回之前必须结束它,原因还是那个——循环调用,自己的线程自己关。 -
It must be as deterministic as possible. Non-determinism (e.g. random decisions not based on the input bytes) will make fuzzing inefficient.
其执行必须结果必须是具有确定性的,两次的Data如果一致,则两次执行的结果也必须一致。 -
It must be fast. Try avoiding cubic or greater complexity, logging, or excessive memory consumption.
速度,速度!毕竟模糊测试需要进行大量数据的测试。 -
Ideally, it should not modify any global state (although that’s not strict).
不允许修改全局变量,因为在同一个进程里,修改全局变量会导致下一次运行时读取的是修改后的结果,可能会违反前面说的确定性原则。 -
Usually, the narrower the target the better. E.g. if your target can parse several data formats, split it into several targets, one per format.
尽量窄范围测试,如果测试处理多种数据格式的目标,还是分割成多个子目标为好。这既是处于速度考量,也是出于模糊测试数据变异的效果考量。
编译连接
文档中给出的编译链接命令大致可归纳为:
clang++ -g -O1 -fsanitize=fuzzer,address -fsanitize-coverage=trace-pc-guard \
fuzz_target.cc ../../libFuzzer/Fuzzer/libFuzzer.a \
-o mytarget_fuzzer
-
-g
和-O1
是gcc/clang的通用选项,前者保留调试信息,使错误消息更易于阅读;后者指定优化等级为1(保守地少量优化),但这两个选项不是必须的。 -
-fsanitize=fuzzer
才是关键,通过这个选项启用libFuzzer,向libFuzzer提供进程中的覆盖率信息,并与libFuzzer运行时链接。 -
除了
fuzzer
外,还可以附加其他sanitize(漂白剂)选项也可以加进来,如-fsanitize=fuzzer,address
同时启用了地址检查。关于地址漂白剂详细作用可以查看llvm的官方文档AddressSanitizer
常用内存错误检测工具
AddressSanitizer
: 检测uaf
, 缓冲区溢出,stack-use-after-return
,container-overflow
等内存访问错误,使用-fsanitize = address
MemorySanitizer(MSAN)
: 检测未初始化内存的访问,使用-fsanitize = memory。MSAN不能与其他消毒剂结合使用,应单独使用。
UndefinedBehaviorSanitizer(UBSAN)
: 检测一些其他的漏洞,整数溢出,类型混淆等,检测到C / C ++的各种功能的使用,这些功能已明确列出来导致未定义的行为。使用-fsanitize = undefined,也可以将ASAN和UBSAN合并到一个版本中。
-
-fsanitize-coverage=trace-pc-guard
: 为libfuzzer
提供代码覆盖率信息 -
libFuzzer.a
: 为libfuzzer项目中执行build.sh
编译好生成的libFuzzer.a
-
-o fuzzer
:一个 生成 测试用例, 交给目标程序测试,然后检测程序是否出现异常的程序
这一步骤整体过程就是通过clang的-fsanitize=fuzzer
选项可以启用libFuzzer,这个选项在编译和链接过程中生效,实现了条件判断语句和分支执行的记录,并且辅以libFuzzer中的库函数,通过生成不同的测试样例然后能够获得代码的覆盖率情况,最终实现所谓的fuzz testing。
对这一过程感兴趣的可以阅读libFuzzer编译链接,博主对比了正常clang编译和使用libFuzzer编译从准备—预处理—编译—汇编—链接全过程的对比,展示了libFuzzer在具体编译过程中的作用。
这一步最终生成的就是这个fuzzer。
开始测试
被测程序在启用libFuzzer
并编译链接后,即成为了一个可接受用户参数的命令行程序,直接执行程序便是启动测试。
一般格式:
./your-fuzzer -flag1=val1 -flag2=val2 ... dir1 dir2 ...
flags代表各个控制测试过程的选项参数,可以提供零到任意个,但必须是严格的-flag=value
形式
-
选项前导用单横线,即使选项是一个词而非单个字符
-
选项必须要提供对应的值,即使只是一个开关选项如
-help
,必须要写作-help=1
,且选项与值中间只能用等号,不能用空格。
dirs表示语料库目录,它们的内容都会被读取作为初始语料库,但测试过程中生成的新输入只会被保存到第一个目录下。
常用有以下部分参数,全文我将附在博客的最后附件1中.
对于开关选项(如help
),效用一列表示当参数启用时(-help=1
)的效果
选项 | 默认 | 效用 |
---|---|---|
verbosity | 1 | 运行时输出详细日志 |
seed | 0 | 随机种子。如果为0,则自动生成 |
runs | -1 | 单个测试运行的次数(-1表示无限) |
max_len | 0 | 测试输入的最大长度。若为0,libFuzzer会自行猜测 |
minimize_crash | 0 | 如果为1,则最小化提供的崩溃输入。与-runs = N或-max_total_time = N一起使用以限制尝试次数。 |
reduce_inputs | 1 | 尝试减小输入的大小,同时保留其全部功能集 |
fork | 0 | 在子过程中发生fuzzing的实验模式 |
ignore_timeouts | 1 | 在fork模式下忽略超时 |
ignore_crashes | 0 | 在fork模式下忽略崩溃 |
ignore_ooms | 1 | 在fork模式下忽略OOM |
cross_over | 1 | 交叉输入 |
rss_limit_mb | 2048 | 内存使用限制,以Mb为单位。使用0则禁用限制。 |
mutate_depth | 5 | 每个输入连续突变的数量 |
shuffle | 1 | 启动时打乱初始语料库 |
prefer_small | 1 | 打乱语料库时将小输入置于优先位置 |
timeout | 1200 | 若为正,表示单元运行最大秒数。超时会被提前中止 |
error_exitcode | 77 | libFuzzer本身出错时的退出码 |
timeout_exitcode | 77 | libFuzzer超时退出码 |
max_total_time | 0 | 若为正,表示整个模糊测试运行最大秒数 |
dict | 0 | 提供输入关键字的字典 |
use_counters | 1 | 使用覆盖率计数器生成命中代码块的频率的近似计数;默认为1。 |
help | 0 | 打印帮助并退出 |
merge | 0 | 不损失覆盖率前提下,将第2/3/4/…个语料库合并到第一个中去 |
merge_control_file | 0 | 指定合并进程的控制文件,用于恢复合并状态 |
jobs | 0 | 运行的作业数量。所有作业的输出会被重定向到fuzz-JOB.log。 |
workers | 0 | 运行作业的并发进程数。若为0,实验CPU核心数一半 |
reload | 1 | 每N秒载主语料库,以知悉其他进程发现的单元 |
only_ascii | 0 | 仅生成ASCII(isprint + isspace)输入 |
artifact_prefix | 0 | 提供将模糊处理工件(崩溃,超时或缓慢的输入)另存为$(artifact_prefix)file 时要使用的前缀。默认为空。 |
exact_artifact_path | 0 | 如果为空则忽略(默认)。如果为非空,则将失败(崩溃,超时)时的单个工件写为$(exact_artifact_path) 。这将覆盖-artifact_prefix, 并且不会在文件名中使用校验和。请勿将相同的路径用于多个并行进程。 |
detect_leaks | 1 | 如果为1(默认值)并且启用了LeakSanitizer,则尝试在模糊测试期间检测内存泄漏(即,不仅在关闭时)。 |
print_final_stats | 0 | 退出时打印统计信息 |
print_corpus_stats | 0 | 退出时打印语料库元素统计信息 |
print_coverage | 0 | 退出时打印覆盖率信息 |
close_fd_mask | 0 | 为1则在关闭stdout,为2则关闭stderr,为3则关闭二者 |
重运行模式:
./your-fuzzer -flag1=val1 -flag2=val2 ... file1 file2 ...
与上面一样,但是选项后面接的是文件列表而非文件夹列表,这些输入样例将会重新读取并输入运行,不会产生新样例,在回归测试时十分有用。
这里有几个选项功能是值得单独说一下的
(1)Seed corpus 种子语料库
corpus语料库就是给目标程序的各种各样的输入
mkdir MY_CORPUS
./your-fuzzer MY_CORPUS/ seeds/
当基于libFuzzer的模糊器以另一个目录作为参数执行时,它将首先递归地从每个目录中读取文件(在本例中MY_CORPUS/和seeds/都读),并对所有目录执行目标函数。然后,任何触发感兴趣的代码路径的输入将被写回到第一个语料库目录(在本例中为MY_CORPUS
)。一般情况下我们将相关文件放在seeds的位置下,MY_CORPUS/目录为空,这样运行后生成的样本就存在MY_CORPUS/中了。
(2)精简语料库样本集
在模糊测试期间,测试语料库可能会增长到很大容量。如果希望最小化语料库,即创建具有相同覆盖率的语料库子集但容量却小很多,这就是一件性价比十分高的事情了。
mkdir corpus1_min
./your-fuzzer -merge=1 corpus1_min corpus1
-
corpus1_min
: 精简后的样本集存放的位置 -
corpus1
: 原始样本集存放的位置
(3)并行运行
提高模糊测试效率的另一种方法是使用更多的CPU。如果您使用-jobs=N
它运行模糊器,它将产生N个独立的作业,但最多不超过拥有的内核数的一半。用于-workers=M
设置允许的并行作业数。
当指定了多个任务时,程序启动时会先产生一个master
进程,同时并发启动相应数量个worker
进程,master会把jobs分配到workers上去执行,当某个任务结束后,相应进程终止,同时master会启动一个新的任务进程分配到对应的worker上,平均每个worker上会分配jobs/workers个任务。
./your-fuzzer MY_CORPUS/ seeds/ -jobs=8
在8核计算机上,这将产生4个并行工作器。如果其中一个被退出,将自动创建另一个,最多8个。
fuzzer -jobs=8
├─sh -c ./fuzzer >fuzz-0.log 2>&1
│ └─fuzzer
│ └─{fuzzer}
├─sh -c ./fuzzer >fuzz-1.log 2>&1
│ └─fuzzer
│ └─{fuzzer}
├─sh -c ./fuzzer >fuzz-2.log 2>&1
│ └─fuzzer
│ └─{fuzzer}
├─sh -c ./fuzzer >fuzz-3.log 2>&1
│ └─fuzzer
│ └─{fuzzer}
(4)Dictionaries 字典
字典最早是afl在2015年一篇博客上提出的afl-fuzz: making up grammar with a dictionary in hand,
基本思路就是应用程序都是都是处理具有一定格式的数据,比如xml
文档,png
图片等等。 这些数据中会有一些特殊字符序列 (或者说关键字), 比如 在xml
文档中 就有CDATA
, 等,png图片就有 png 图片头。
如果我们事先就把这些 字符序列列举出来,fuzz
直接使用这些关键字去 组合,就会就可以减少很多没有意义的 尝试,同时还有可能会走到更深的程序分支中去。
Dictionary
就是实现了这种思路。libfuzzer
和afl
使用的dictionary
文件的语法是一样的, 所以可以直接拿 afl 里面的dictionary
文件来给libfuzzer
使用。
如下是libFuzzer官方文档中的字典示例
# Lines starting with '#' and empty lines are ignored.
# Adds "blah" (w/o quotes) to the dictionary.
kw1="blah"
# Use \\ for backslash and \" for quotes.
kw2="\"ac\\dc\""
# Use \xAB for hex values
kw3="\xF7\xF8"
# the name of the keyword followed by '=' may be omitted:
"foo\x0Abar"
-
#
开头的行 和 空行会被忽略 -
kw1=
这些就类似于注释, 没有意义 -
真正有用的是由
"
包裹的字串,这些 字串就会作为一个个的关键字,libfuzzer
会用它们进行组合来生成样本。
libfuzzer
使用-dict
指定dict
文件,下面使用xml.dict
为dictionary
文件,进行fuzz
。
./your-fuzzer -dict=DICTIONARY_FILE
(5)输出
当fuzzer成功运行之后,信息会输出在屏幕上。输出行具有事件代码和统计信息的形式。常见的事件代码是:
-
READ
fuzzer已从语料库目录中读取了所有提供的输入样本。 -
INITED
fuzzer已完成初始化,其中包括通过被测代码运行每个初始输入样本。 -
NEW
fuzzer创建了一个测试输入,该输入涵盖了被测代码的新区域。此输入将保存到主要语料库目录。 -
pulse
fuzzer已生成 2的n次方个输入(定期生成以使用户确信fuzzer仍在工作)。 -
DONE
fuzzer已完成操作,因为它已达到指定的迭代限制(-runs
)或时间限制(-max_total_time
)。 -
RELOAD
fuzzer在定期从语料库目录中重新加载输入;这使它能够发现其他fuzzer进程发现的任何输入(请参阅并行模糊化)。
每条输出行还报告以下统计信息(非零时):
-
cov:
执行当前语料库所覆盖的代码块或边的总数。 -
ft:
libFuzzer使用不同的信号来评估代码覆盖率:边缘覆盖率,边缘计数器,值配置文件,间接调用方/被调用方对等。这些组合的信号称为功能(ft:)。 -
corp:
当前内存中测试语料库中的条目数及其大小(以字节为单位)。 -
exec/s:
每秒模糊器迭代的次数。 -
rss:
当前的内存消耗。
对于NEW
事件,输出行还包含有关产生新输入的变异操作的信息:
-
L:
新输入的大小(以字节为单位)。 -
MS: <n> <operations>
用于生成输入的变异操作的计数和列表。
我们以如下样例作解释:
INFO: Seed: 1608565063
INFO: Loaded 1 modules (37 guards): [0x788ec0, 0x788f54),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0 READ units: 1
#1 INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 11Mb
#3 NEW cov: 4 ft: 4 corp: 2/4b exec/s: 0 rss: 12Mb L: 3 MS: 2 InsertByte-InsertByte-
#3348 NEW cov: 5 ft: 5 corp: 3/65b exec/s: 0 rss: 12Mb L: 61 MS: 2 ChangeByte-InsertRepeatedBytes-
#468765 NEW cov: 6 ft: 6 corp: 4/78b exec/s: 0 rss: 49Mb L: 13 MS: 4 CrossOver-ChangeBit-EraseBytes-ChangeByte-
#564131 NEW cov: 7 ft: 7 corp: 5/97b exec/s: 0 rss: 56Mb L: 19 MS: 5 InsertRepeatedBytes-InsertByte-ChangeByte-InsertByte-InsertByte-
=================================================================
==32049==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200072bb93 at pc 0x000000528540 bp 0x7ffdb3439100 sp 0x7ffdb34390f8
READ of size 1 at 0x60200072bb93 thread T0
......................................................
......................................................
......................................................
0x60200072bb93 is located 0 bytes to the right of 3-byte region [0x60200072bb90,0x60200072bb93)
allocated by thread T0 here:
......................................................
......................................................
......................................................
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/haclh/vmdk_kernel/libfuzzer-workshop-master/lessons/04/./vulnerable_functions.h:22:14 in VulnerableFunction1(unsigned char const*, unsigned long)
Shadow bytes around the buggy address:
0x0c04800dd720: fa fa fd fd fa fa fd fa fa fa fd fa fa fa fd fa
0x0c04800dd730: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
0x0c04800dd740: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x0c04800dd750: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x0c04800dd760: fa fa fd fa fa fa fd fa fa fa fd fd fa fa fd fd
=>0x0c04800dd770: fa fa[03]fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c04800dd780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c04800dd790: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c04800dd7a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c04800dd7b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c04800dd7c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
......................................................
......................................................
==32049==ABORTING
MS: 1 CrossOver-; base unit: 38a223b0988bd9576fb17f5947af80b80203f0ef
0x46,0x55,0x5a,
FUZ
artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60
Base64: RlVa
首先我们可以看出来Seed: 1608565063
说明这次的种子数据,如果我们想重现重新运行-seed=1608565063
以得到相同的结果。其次-max_len is not provided, using 64
,-max_len
用于设置最大的数据长度,因为没有设置fuzzer会自己猜测,这里设置的数据不大于64KB。
接下来#
开头的行是fuzz
过程中找到的路径信息
# 564131 NEW cov: 7 ft: 7 corp: 5/97b exec/s: 0 rss: 56Mb L: 19 MS: 5 InsertRepeatedBytes-InsertByte-ChangeByte-InsertByte-InsertByte-
我们可以看出来libFuzzer尝试了至少564131个输入(#564131
),发现了5个输入,总共97个字节(corp: 5/97b
),它们总共覆盖了7个覆盖点(cov: 7
)。我们可以将覆盖点视为代码中的 基本块。
==32049==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200072bb93 at pc 0x000000528540 bp 0x7ffdb3439100 sp 0x7ffdb34390f8
READ of size 1 at 0x60200072bb93 thread T0
这个信息说明在其中一个输入上,AddressSanitizer已检测到heap-buffer-overflow
错误并中止了执行。
artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60
倒数第二行是触发漏洞的测试用例,在退出进程之前,libFuzzer已创建了一个文件,其中包含触发崩溃的字节和所有信息,要重现崩溃而无模糊运行可以使用
ASAN_OPTIONS=symbolize=1 ./first_fuzzer ./crash-0eb8e4ed029b774d80f2b66408203801
# ASAN_OPTIONS=symbolize=1 用于显示栈的符号信息
来重现crash
如果我们在fuzzer运行的选项里有使用字典-dictionary
和-print_final_stats
执行完打印统计信息,最后的输出可能还会多出两块,形如下面
###### Recommended dictionary. ######
"X\x00\x00\x00\x00\x00\x00\x00" # Uses: 1228
"prin" # Uses: 1353
...........................
...........................
...........................
"U</UTrri\x09</UTD" # Uses: 61
###### End of recommended dictionary. ######
Done 1464491 runs in 301 second(s)
stat::number_of_executed_units: 1464491
stat::average_exec_per_sec: 4865
stat::new_units_added: 1407
stat::slowest_unit_time_sec: 0
stat::peak_rss_mb: 407
开始由####
夹着的是libfuzzer
在fuzz
过程中挑选出来的dictionary
, 同时还给出了使用的次数,这些dictionary
可以在以后fuzz
同类型程序时 节省fuzz
的时间。
然后以stat:
开头的是一些 fuzz 的统计信息, 主要看stat::new_units_added
表示整个fuzz
过程中触发了多少个代码单元。
可以看到直接fuzz
,5
分钟 触发了1407
个代码单元
实例篇
以Freeimage为例进行测试
我们首先把最新版本的Freeimage给拉到本地,然后解压
wget https://downloads.sourceforge.net/freeimage/FreeImage3180.zip
unzip FreeImage3180.zip
对这个源包先进行一下编译
pushd FreeImage
# b44ExpLogTable.cpp only contains a definition of main().
sed -i 's/Source\/OpenEXR\/IlmImf\/b44ExpLogTable.cpp//' Makefile.srcs
make LIBRARIES=-lc++ -j$(nproc)
popd
编译成功后在Freeimage/Dist里应该就生成了libfreeimage.a
,.h
和.o
文件
随后根据Freeimage的功能特性写对应的hardness,也就是fuzzing target,这里直接使用OSS-fuzz的project中给出的Freeimage的hardness,在根目录保存为load_from_memory_fuzzer.cc文件
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <vector>
#include <FreeImage.h>
namespace {
// Returns true if the format should be attempted to loaded from memory.
bool SafeToLoadFromMemory(FREE_IMAGE_FORMAT fif) {
// For now, just load if it is a BMP. Future heuristics may need to be based
// on the expected size in different formats for memory regions to avoid OOMs.
return fif == FIF_BMP;
}
} // namespace
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
static bool initialized = false;
if (!initialized) {
FreeImage_Initialise();
}
if (size > 100 * 1000) {
return 0;
}
std::vector<uint8_t> fuzzer_data_vector(data, data + size);
FIMEMORY* fiMem = FreeImage_OpenMemory(
reinterpret_cast<unsigned char*>(fuzzer_data_vector.data()),
fuzzer_data_vector.size());
FREE_IMAGE_FORMAT fif = FreeImage_GetFileTypeFromMemory(fiMem, 0);
if (SafeToLoadFromMemory(fif)) {
FIBITMAP* fiBitmap = FreeImage_LoadFromMemory(fif, fiMem);
FreeImage_Unload(fiBitmap);
}
FreeImage_CloseMemory(fiMem);
return 0;
}
接下来的步骤就是开始编译fuzzer,把对应参数输入在后面,使用clang++开始编译
clang++ -g -fsanitize=fuzzer,address \
load_from_memory_fuzzer.cc ./FreeImage/Dist/libfreeimage.a \
-o load_from_memory_fuzzer
出现了一点错误,看来是Freeimage.h文件没有找到,需要用-I 指定一下文件的路径让他可以找到
clang++ -g -fsanitize=fuzzer,address -I'/home/fstark/FreeImage/Dist' \
load_from_memory_fuzzer.cc ./FreeImage/Dist/libfreeimage.a \
-o load_from_memory_fuzzer
这次成功了,发现已经成功生成了load_from_memory_fuzzer
不加任何附加命令直接运行一下试试,发现可以跑了,就是速度不怎么快
当我做到这一步时,学长点拨前面的编译是有问题的,运行时结果仅覆盖57是肯定有问题的。通过在开会时看学长的讲解分析,确实在编译的时候直接拉freeimage的包,里面的makefile需要修改。这里把编译选项更改一下,其实我又回顾了一下之前clusterfuzz踩的坑,其中心脏滴血在编译的时候就要求加上-fsanitize=address,fuzzer-no-link
,但之前没有细心注意。
我们在makefile.gnu里把默认的03改成01,再加上这几条推荐的编译选项,不得不说自己对于常见的编译过程真是陌生,要不是靠学长又是进坑几个小时,真要抽个时间好好补补这部分的内容了,做个编译过程大梳理和常见编译器对比什么的
这样再编译一遍,不放样本,速度也好了很多
简单准备个样本集,再精简一下
简单跑一下,这次就可以跑出crash了,但是很多是重复的在跑的时候附加的选项还是要多多限制,逐渐摸索,但整体流程就是这样了
附件一
Flags: | value | strictly in form -flag=value |
---|---|---|
verbosity | 1 | Verbosity level. |
seed | 0 | Random seed. If 0, seed is generated. |
runs | -1 | Number of individual test runs (-1 for infinite runs). |
max_len | 0 | Maximum length of the test input. If 0, libFuzzer tries to guess a good value based on the corpus and reports it. |
lea_control | 100 | Try generating small inputs first, then try larger inputs over time. Specifies the rate at which the length limit is increased (smaller == faster). If 0, immediately try inputs with size up to max_len. Default value is 0, if LLVMFuzzerCustomMutator is used. |
seed_inputs | 0 | A comma-separated list of input files to use as an additional seed corpus. Alternatively, an "@" followed by the name of a file containing the comma-seperated list. |
cross_over | 1 | If 1, cross over inputs. |
mutate_depth | 5 | Apply this number of consecutive mutations to each input. |
reduce_depth | 0 | Experimental/internal. Reduce depth if mutations lose unique features |
shuffle | 1 | Shuffle inputs at startup |
prefer_small | 1 | If 1, always prefer smaller inputs during the corpus shuffle. |
timeout | 1200 | Timeout in seconds (if positive). If one unit runs more than this number of seconds the process will abort. |
error_exitcode | 77 | When libFuzzer itself reports a bug this exit code will be used. |
timeout_exitcode | 70 | When libFuzzer reports a timeout this exit code will be used. |
max_total_time | 0 | If positive, indicates the maximal total time in seconds to run the fuzzer. |
help | 0 | Print help. |
fork | 0 | Experimental mode where fuzzing happens in a subprocess |
ignore_timeouts | 1 | Ignore timeouts in fork mode |
ignore_ooms | 1 | Ignore OOMs in fork mode |
ignore_crashes | 0 | Ignore crashes in fork mode |
merge | 0 | If 1, the 2-nd, 3-rd, etc corpora will be merged into the 1-st corpus. Only interesting units will be taken. This flag can be used to minimize a corpus. |
stop_file | 0 | Stop fuzzing ASAP if this file exists |
merge_control_file | 0 | Specify a control file used for the merge process. If a merge process gets killed it tries to leave this file in a state suitable for resuming the merge. By default a temporary file will be used. |
minimize_crash | 0 | If 1, minimizes the provided crash input. Use with -runs=N or -max_total_time=N to limit the number attempts. Use with -exact_artifact_path to specify the output. Combine with ASAN_OPTIONS=dedup_token_length=3 (or similar) to ensure that the minimized input triggers the same crash. |
cleanse_crash | 0 | If 1, tries to cleanse the provided crash input to make it contain fewer original bytes. Use with -exact_artifact_path to specify the output. |
use_counters | 1 | Use coverage counters |
use_memmem | 1 | Use hints from intercepting memmem, strstr, etc |
use_value_profile | 0 | Experimental. Use value profile to guide fuzzing. |
use_cmp | 1 | Use CMP traces to guide mutations |
shrink | 0 | Experimental. Try to shrink corpus inputs. |
reduce_inputs | 1 | Try to reduce the size of inputs while preserving their full feature sets |
jobs | 0 | Number of jobs to run. If jobs >= 1 we spawn this number of jobs in separate worker processes with stdout/stderr redirected to fuzz-JOB.log. |
workers | 0 | Number of simultaneous worker processes to run the jobs. If zero, "min(jobs,NumberOfCpuCores()/2)" is used. |
reload | 1 | Reload the main corpus every seconds to get new units discovered by other processes. If 0, disabled |
report_slow_units | 10 | Report slowest units if they run for more than this number of seconds. |
only_ascii | 0 | If 1, generate only ASCII (isprint+isspace) inputs. |
dict | 0 | Experimental. Use the dictionary file. |
artifact_prefix | 0 | Write fuzzing artifacts (crash, timeout, or slow inputs) as $(artifact_prefix)file |
exact_artifact_path | 0 | Write the single artifact on failure (crash, timeout) as $(exact_artifact_path). This overrides -artifact_prefix and will not use checksum in the file name. Do not use the same path for several parallel processes. |
print_pcs | 0 | If 1, print out newly covered PCs. |
print_funcs | 2 | If >=1, print out at most this number of newly covered functions. |
print_final_stats | 0 | If 1, print statistics at exit. |
print_corpus_stats | 0 | If 1, print statistics on corpus elements at exit. |
print_coverage | 0 | If 1, print coverage information as text at exit. |
dump_coverage | 0 | Deprecated. |
handle_segv | 1 | If 1, try to intercept SIGSEGV. |
handle_bus | 1 | If 1, try to intercept SIGBUS. |
handle_abrt | 1 | If 1, try to intercept SIGABRT. |
handle_ill | 1 | If 1, try to intercept SIGILL. |
handle_fpe | 1 | If 1, try to intercept SIGFPE. |
handle_int | 1 | If 1, try to intercept SIGINT. |
handle_term | 1 | If 1, try to intercept SIGTERM. |
handle_xfsz | 1 | If 1, try to intercept SIGXFSZ. |
handle_usr1 | 1 | If 1, try to intercept SIGUSR1. |
handle_usr2 | 1 | If 1, try to intercept SIGUSR2. |
lazy_counters | 0 | If 1, a performance optimization isenabled for the 8bit inline counters. Requires that libFuzzer successfully installs its SEGV handler |
close_fd_mask | 0 | If 1, close stdout at startup; if 2, close stderr; if 3, close both. Be careful, this will also close e.g. stderr of asan. |
detect_leaks | 1 | If 1, and if LeakSanitizer is enabled try to detect memory leaks during fuzzing (i.e. not only at shut down). |
purge_allocator_interval | 1 | Purge allocator caches and quarantines every seconds. When rss_limit_mb is specified (>0), purging starts when RSS exceeds 50% of rss_limit_mb. Pass purge_allocator_interval=-1 to disable this functionality. |
trace_malloc | 0 | If >= 1 will print all mallocs/frees. If >= 2 will also print stack traces. |
rss_limit_mb | 2048 | If non-zero, the fuzzer will exit uponreaching this limit of RSS memory usage. |
malloc_limit_mb | 0 | If non-zero, the fuzzer will exit if the target tries to allocate this number of Mb with one malloc call. If zero (default) same limit as rss_limit_mb is applied. |
exit_on_src_pos | 0 | Exit if a newly found PC originates from the given source location. Example: -exit_on_src_pos=foo.cc:123. Used primarily for testing libFuzzer itself. |
exit_on_item | 0 | Exit if an item with a given sha1 sum was added to the corpus. Used primarily for testing libFuzzer itself. |
ignore_remaining_args | 0 | If 1, ignore all arguments passed after this one. Useful for fuzzers that need to do their own argument parsing. |
focus_function | 0 | Experimental. Fuzzing will focus on inputs that trigger calls to this function. If -focus_function=auto and -data_flow_trace is used, libFuzzer will choose the focus functions automatically. |
analyze_dict | 0 | Experimental |
use_clang_coverage | 0 | Deprecated; don't use |
data_flow_trace | 0 | Experimental: use the data flow trace |
collect_data_flow | 0 | Experimental: collect the data flow trace |
参考文献
用libFuzzer搞事情 http://pwn4.fun/2017/07/15/%E7%94%A8libFuzzer%E6%90%9E%E4%BA%8B%E6%83%85/
libFuzzer——编译链接
https://i-m.dev/posts/20190831-143715.html
libFuzzer –用于覆盖率指导的模糊测试的库
https://releases.llvm.org/4.0.0/docs/LibFuzzer.html#startup-initialization
libFuzzerTutorial
https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md
fuzz实战之libfuzzer
https://www.secpulse.com/archives/71898.html
fuzzer-test-suite
https://github.com/google/fuzzer-test-suite
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)