“反馈驱动:通过监控样本触发的代码覆盖率,进而改进输入样本以提高代码覆盖率,增加发现漏洞的概率。”
目前业界中基于代码覆盖率的最著名的三大Fuzzer,当属Google开发的AFL、libfuzzer和honggfuzz,且他们都是开源的,在github上都可以搜索到。
相信很多搞fuzzing的同学都听过或用过这三款工具,本系列主要讲honggfuzz的原理与应用,之前把honggfuzz源码都阅读过一遍,并且二次开发过,该工具的挖洞效果还是不错的。
计算代码覆盖率情况就需要有个计量单位,通常有3种:
函数(Fuction-Level)
基本块(BasicBlock-Level)
边界(Edge-Level)
1. 函数(Fuction-Level)
先说下函数,这个很容易理解,就是代码执行时调用到哪些函数,但是函数里面的具体代码行却不作统计,相对比较粗糙但高效的统计方式。
所以,通常的统计方式是用基本块,简称BB。
2.基本块(BasicBlock-Level)
什么是基本块,直接看下图就很容易理解了。
IDA中每一块代码就代表着一个基本块,就是以指令跳转为作划分界限的。
3.边界(Edge-Level)
edge本身就涵盖了基本块部分,唯一的差别是edge多记录了一些执行边界的信息。比如示例代码:
在IDA中可以看到A、B、C这3个基本块,但当a为假时,程序就会从A执行到C。
前面基本块的方式就无法确切地知道是否曾从A执行到C,尤其是该段代码被多次执行的情况,就更无法知道,这时edge覆盖方式就出现了。
edge会在A跟C之间建立虚拟块D,通过判断D是否执行过,来确认是否曾从A执行到C,这种方式也会比较消耗性能就是:
统计代码覆盖率又分两种情况:
1.【有源码】:直接使用SanitizerCoverage即可,在编译选项中添加相应的覆盖率统计方式,比如基本块统计方式可以添加:
CFLAG=“-fsanitize=address -fsanitize-coverage=bb”
2.【无源码】:使用Pin、DynamoRIO等二进制插桩工具去hook统计,或者pediy改指令的方式去监控也是可以的,本系列的后续文章可能会细说。
本文我们默认以基本块作为代码覆盖率的统计方式,比如采用如下编译选项:
ASAN_OPTIONS=coverage=1:coverage_direct=1 -fsanitize=address -fsanitize-coverage=bb
执行后它会生成两个文件:.sancov.map和.sancov.raw,这是honggfuzz处理过的scancov文件。
其中.sancov.map文件记录是模块信息,.sancov.raw文件则记录着执行过的基本块地址信息:
1、首先,honggfuzz会先去分析.sancov.raw文件,保存命中的基本块地址,记录BB数,以及总共的BB数:
2、计算代码覆盖率 = 命中BB数 / (命中BB数 + 未命中的BB数)
以下是旧版honggfuzz关于coverage的显示内容格式:
新版的变成只记录BB/edge数:
3、发现新路径或者新插桩链接库加载,则添加此变异样本为新的输入样本,供后面作进一步变异,以触发更多新路径
honggfuzz最早创建于2015年,跟AFL的发布时间差不多,AFL的出现可以说在安全界是项里程碑标志,给漏洞挖掘技术带来新的技术革命,网上对其源码分析的文章也到处可见,但对honggfuzz的详细分析还没有,因此才作此系列记录下。
个人也曾给honggfuzz贡献过代码,但由于某次pull request未被采用,于是就自己二次开发不再提交合并。因为我是在macOS下开发,而原作者主要是用Linux,那次提交的功能对我很有用,所以只好自己维护一份二次开发版本。
后来自己也在Windows和macOS平台上扩展了许多新功能,在本系列文章中,后面可能会单篇聊一聊,也感谢作者robertswiecki开发出这么好用的fuzzer,让我借此刷了不少CVE。
上篇《honggfuzz漏洞挖掘技术深究系列(1)——反馈驱动(Feedback-Driven)》讲到基于软件的代码覆盖率驱动fuzzing的方式,除了软件还有硬件方式,即基于Intel BTS (Branch Trace Store) 或Intel PT (Processor Tracing) 去计算代码覆盖率,同时要求Linux内核>=4.2,这种方式的最大好处是完全由硬件支配,无所谓软件是闭源还是开源。由于硬件环境受限,我也一直未使用过,有此条件的同学可以试下。
本篇主要讲下持久型fuzzing(Persistent Fuzzing),即fuzzing API,这种方式会更精准和高效的。
先看使用方法:
$ cat test.c
#include <inttypes.h>
#include <testlib.h>
extern int LLVMFuzzerTestOneInput(uint8_t **buf, size_t *len);int LLVMFuzzerTestOneInput(uint8_t *buf, size_t len) {
_FuncFromFuzzedLib_(buf, len); // 目标函数
return 0;
}
$ clang-4.0 -fsanitize-coverage=trace-pc,indirect-calls,trace-cmp fuzzedlib.c -o fuzzedlib.o
$ clang-4.0 test.c fuzzedlib.o honggfuzz/libhfuzz/libhfuzz.a -o test
$ honggfuzz -z -P -f INPUT.corpus -- ./test
这里用到几个编译选项:
trace-pc:追踪执行过的基本块BB,在每个edge中插入__saitizer_cov_trace_pc()函数,可定义该函数作为相应的回调处理
indirect-calls:在每个间接调用中添加PC追踪,与前面的trace-pc或trace-pc-guard联合使用,回调函数:__sanitizer_cov_trace_pc_indir
trace-cmp:追踪每个比较指令和swith语句
以trace-pc为例,测试代码如下:
用trace_pc编译:
可以看到自定义的函数被执行,输出执行过的不同pc地址,其它编译选项的用法同上。
下面是honggfuzz对各个回调函数的定义情况:
然后就是记录代码覆盖率情况并进行统计,跟驱动反馈的方式一样了。
再回头看使用示例中的LLVMFuzzerTestOneInput函数,honggfuzz是如何处理它的呢?
通过for无限循环调用目标函数进行Fuzzing,其中参数buf,即样本文件内容,len是数据长度。
最后根据发现的新路径,将相应的样本作为新样本继续fuzzing。
honggfuzz在对输入文件进行变异前,会先创建个临时文件名(honggfuzz+pid+time),然后将输入数据变异后写入临时文件。
fuzz策略的实现主要集中在mangle.c中,在循环的fuzzloop函数中,会根据用户的选择的fuzz方式来调用动态fuzz或者静态fuzz的方法,但最后都是调用mangle_mangleContent来变异文件数据:
跟进mangle_mangleContent函数:
重点就在于后半部分,它会随机选择变异函数进行处理,更改的字节数也是随机的,根据用户指定的mutation变异率来定,即允许变异文件大小的百分比,变异函数列表如下:
这些函数都是在mangle_init中初始化,各函数之间也会相互调用:
把这些函数过一遍就是honggfuzz中所有的文件变异规则了,如果想实现自己的fuzzer,这些规则来扣出来用Python实现一遍,顺便把afl的规则也扣过来就更完美了,下面是我之前写office fuzzer时的半成品代码,最后偷懒直接用radamas去实现变异了:
再回到刚才的变异函数列表,我们一个个走读源码。
1、mangle_Resize函数:
用空格填充随机位置
2、mangle_Byte函数:
向随机位置写随机的uint8类型的数据
3、mangle_Bit函数:
取随机位置的数值做位翻转
4、mangle_Bytes函数:
在随机位置覆盖写2~4字节数据
5、mangle_Magic函数:
取各种边界值进行覆写,这些边界值部分跟AFL还不一样,我在自己的fuzzer里面把它们作了整合。由于边幅所限,我省略了不少边界值:
6、mangle_IncByte函数:
取随机位置的数据加1
7、mangle_DecByte函数:
取随机位置的数据减1
8、mangle_NegByte函数:
取随机位置的数据取反
9、mangle_AddSub函数:
取随机位置的1、2、4或8字节的数据长度作加减操作,操作数取 rand(0~8192)-4096
10、mangle_Dictionary函数:
变异目录名,也是随机取文件夹名称进行变异,如果有多个目录,那被变异的目录数也是随机的
11、mangle_DictionaryInsert函数:
在目录的随机位置中插入随机数据
12、mangle_MemMove函数:
取随机位置的数据拷贝随机长度的数据,里面就是调用memmove函数实现的
13、mangle_MemSet函数:
取随机位置、随机大小,用UINT8_MAX数值填充
14、mangle_Random函数:
取随机位置、随机大小的缓冲区,用随机数填充
15、mangle_CloneByte函数:
取两处随机位置的作数据交换
16、mangle_Expand函数:
文件末尾扩展随机长度的空间,用空格填充,然后在随机位置,取前面的随机长度作数据拷贝
17、mangle_Shrink函数:
删除随机长度的文件内容
18、mangle_InsertRnd函数:
在文件的随机位置插入随机长度的数据
19、mangle_ASCIIVal函数:
在随机位置覆盖32字节的随机数
总结
在Fuzzing过程中,很多变异规则是共用的,可以参考一些主源的开源软件,比如afl\peach\honggfuzz\libfuzzer,提取规则作整合,然后写个自己的fuzzing框架,在后面作针对的fuzzer时,可以直接套用。
从上面的fuzz策略可以总结出常规的变异规则:
随机数据替换
数据值增减
已知边界值替换
插入随机数据
删减文件内容
目录变异
数据拷贝覆盖
……
对于一些复合文件格式,如果只是单纯的暴力Fuzzing,会导致生成很多无法被解析的文件,因此需要对文件变异作一些定制化的工作,比如docx、doc等office复合文件,docx是个压缩包,doc是个OLE格式,如果fuzz docx自然需要将其zip解压,再针对感兴趣的文件作变异,对于doc最好是作文件格式解析,只对感兴趣的stream作文件变异,这样的fuzzing的有效性才会更高。
庆幸地是,honggfuzz提供-c参数用于扩展变异规则以代替原有变异方式,同时提供有--pprocess_cmd在原有的文件变异后再作处理:
--mutate_cmd|-c VALUE
External command producing fuzz files (instead of internal mutators)
--pprocess_cmd VALUE
External command postprocessing files produced by internal mutators
-c功能比较有用,也是我用得比较多的,另一个--pprocess_cmd基本我没用过。
当你通过-f提供输入样本目录后,在fuzzing时,随机提取的文件会直接传递给-c参数指定的扩展命令作变异。
比如想针对某文件特定offset范围内的内容进行变异,下面是针对macOS/iOS字体文件中的虚拟指令作Fuzzing时写的脚本:
#!/usr/bin/env pythonimport mmap
import os
from random import randint
import sysRANGE_START = 0x16D8
RANGE_END = 0x304D
MIN_BYTES_TO_FLIP = 1
MAX_BYTES_TO_FLIP = 5if ".DS_Store" in sys.argv[1]:
exit(1)with open(sys.argv[1], "r+b") as f:
mapped = mmap.mmap(f.fileno(), 0)
#print "file size: 0x%x" % len(mapped)
bytes_to_flip = randint(MIN_BYTES_TO_FLIP, MAX_BYTES_TO_FLIP)
bytes_flipped = 0while bytes_flipped < bytes_to_flip:
byte_pos = randint(RANGE_START, RANGE_END)
#print "byte_pos: 0x%x" %byte_pos
byte_new = chr(randint(0, 255))
mapped[byte_pos] = byte_new
bytes_flipped += 1mapped.close()
变异效果:
最后挖到一个TTF字体虚拟指令漏洞:
orig file: 2F90h: 00 3F C5 CD 2B 10 C1 10 DE 3F C5 【CD】 2B 10 C5 10 poc file: 2F90h: 00 3F C5 CD 2B 10 C1 10 DE 3F C5 【DD】 2B 10 C5 10 glyf table -> SimpleGlyf[] -> Instructions('0xCD' => ‘0xDD') -> MDRP指令
同样的,你也可以写个doc、docx等office文件格式解析并变异的扩展fuzzer,比如利用olefile库(但只支持修改同等大小不变的doc,要插入或删除需要自行实现),或者通过COM接口来实现操作。
比如之前有段时间doc中的公式编辑器存在很多漏洞,你就可以专门针对Equation Native流作fuzzing。
最后放两张图(riufuzz是自己对honggfuzz二次开发的版本,后面有机会再讲):
对于闭源程序的反馈驱动Fuzzing,通常有3种方式:
二进制插桩:使用Pin或DynamoRIO动态插桩监控代码覆盖率,比如winafl
虚拟化技术:使用Qemu或Boch等虚拟化技术实现应用层和内核层的代码覆盖率监控,比如afl、bochpwn
硬件级技术:使用Intel Processor Trace(PT)技术,比如honggfuzz
Intel® Processor Trace (Intel® PT) 是在i5/i7 5000以上型号上加入的功能,由于它是硬件级的特性,相比Qemu或Boch,在性能上和代码工作量会占有一定优势。在Linux上可以通过perf来使用PT,可以先简单看是否支持PT:
查看是否支持PT:
ls /sys/devices/intel_pt/format追踪程序执行:
perf record -e intel_pt// program
也可以使用开源工具simple-pt中的ptfeature命令行工具来检测:
./ptfeature pt
Supports PT
最新版GDB也支持pt功能了:
gdb program
start
record btrace pt
contrecord instruction-history /m # show instructions
record function-history # show functions executed
在程序内通过perf_event_open(http://man7.org/linux/man-pages/man2/perf_event_open.2.html)函数可以使用PT实现BB基本块的覆盖率追踪,传递给指定进程pid来实现监控:
将返回的文件描述符传递给mmap映射为可读写的用户内存空间,以便从中读取PT记录的追踪数据:
PT记录的追踪数据采用压缩的二进制格式输出,每秒每个CPU都会持续记录并输出,由于是硬件记录的,最早自然是出现在内核空间,为了使用它,就需要将其导出到用户空间,即通过前面mmap方法映射到用户可写的内存空间,然后再去定位数据解码。PT导出的追踪数据被存储在一个叫AUX space的内存区域,它相对perfMmapBuf的偏移记录在perf_event_mmap_page->aux_offset,大小为perf_event_mmap_page->aux_size,上面代码的第二步mmap就是去映射AUX space。
接下来就是利用libpt来解码捕获到追踪数据,实现函数位于perf_ptAnalyzePkt中:
最后将执行到的BB基本块信息更新到feedback map,之后的实现步骤就跟本系列第1篇驱动反馈中所讲的一致。
到这里,关于《honggfuzz漏洞挖掘技术深究系列》的文章先暂告一段落了,它就相当于是自己的学习笔记,也可以留作日后查询。
之前看到论坛有些朋友也在问关于《漏洞战争》的问题,平时比较少用PC登陆论坛,可能没法看到并回复,如果有问题,可以关注公众号私信(没开留言功能,见谅,如果论坛不允许打广告的话,版主可以直接删除下图^_^)。
好久没上看雪发帖了,出来冒个泡,顺便说下,新编辑器确实比以前好用多了,点赞!
[公告]LV6级以上的看雪会员可以免费获得《2019安全开发者峰会》门票一张!!
最后于 2018-11-28 10:04 被riusksk编辑 ,原因: