基于反馈驱动的Fuzz 工具设计
2020-11-05赵晖
赵晖
(山东理工大学,山东 淄博255000)
1 网络安全及漏洞挖掘简介
随着Internet 网络规模的不断扩大,各种软件硬件的应用不断加入,随着互联网深入政治、经济、民生乃至军事的各个方面,安全漏洞不开避免,安全更是不能忽视的。安全漏洞是指程序代码、协议、硬件在功能实现上的一种缺陷。漏洞一旦被攻击者发现,就可以使目标机器拒绝服务(宕机)、在未授权的情况下执行任意代码、从一个比较低的权限获取更高的权限,用户的信息安全不能够得到保障。
近些年以来,软件数量和种类在不断的增加,软件的代码量也在不断地增加,漏洞挖掘就比较重要了。当前,软件漏洞挖掘技术正在不断朝着高度自动化、智能化的方向进行发展,以模糊测试为代表的软件漏洞挖掘技术更是成为研究的热点。
2 内存漏洞
2.1 栈溢出
程序在运行的时候,每个函数都有一个函数运行栈,在栈中保存了函数运行时所使用的临时变量、缓冲区等信息,函数运行栈的范围通过基地址寄存器(EBP/RBP)跟栈顶寄存器(ESP/RSP)框定。栈上除了临时变量外还有函数运行时缓冲区、函数运行时参数,在他们中间还有一个值是返回地址,这个地址一般表示为该函数执行完以后要执行的汇编指令的地址,如果这个值被攻击者恶意覆盖以后,就可以控制指令指令指针寄存器(EIP/RIP),就可以控制代码流,就可以做到执行任意代码。在使用一些输入函数,比如gets()、read()、scanf()等函数时,如果没有对输入字符串长度进行验证,就会发生溢出。
2.2 堆溢出
在计算机中,堆与栈虽然都是用来保存数据的内存区域,但是这两个内存区域区别很大而且栈空间的生命周期是在函数运行时,但是堆内存空间可以贯穿在整个程序运行周期内。假设程序申请了0x10 的堆内存空间,但是输入0x50 长度的字符串,因此可以导致溢出。但是堆内存空间中没有相关数据、函数指针,因此该例子并不能劫持程序流,也就意味着程序可能会正常退出,一般方法无法捕获到该漏洞。
2.3 整数溢出
单独的整数溢出漏洞,但是整数溢出漏洞,往往会产生堆栈溢出漏洞,整数溢出漏洞的产生往往是对变量定义不严格导致。
3 漏洞挖掘技术的分类
漏洞挖掘技术主要有以下几种方法:(1)静态审计,这种方法十分依赖经验,而且需要耗费大量的精力;(2)静态分析,这种技术不用运行目标程序就可以发现漏洞,但是存在较高的误报率;(3)污点分析跟符号执行,这两项技术需要较大的性能开销,而且程序越大,对性能的开销越大,因此在工业中的应用存在较大的局限性;(4)模糊测试,通过传入畸形数据,检测程序行为,从而发现漏洞。
因为模糊测试对系统性能开销低效率高,因此在工业界应用广泛。
4 模糊测试
Fuzzing 技术是一种自动或者半自动化的软件漏洞挖掘技术,通过向目标程序(文件处理、协议处理、API)注入畸形数据或者其他非预期输入,程序在解析这些文件或者数据时,因为预定义类型的问题,可能会导致解析结果与预期不一致,但是仍能通过一些程序的条件判断,因此就会出现异常的行为、输出,因此可以发现漏洞,与手工分析的区别是,fuzz 技术是一种半自动化技术,只需要用户指定被测程序,并提供输入样本就可以自动化的执行目标程序,并将样本输入到程序中,效率非常高。
主流fuzz 平台设计一般有以下几种类型:
基于变异:根据已知数据样本通过随机数据变异的方法进行fuzz。例如AFL。
基于生成:根据已知协议接口或规范进行建模生成测试用例。例如Syzkaller。
基于机器学习:神经网络输入的随机突变由覆盖度量指导。
基于符号执行:将输入进行符号化,对于所有可能的路径进行约束求解,从而达到更好的代码覆盖率,增大发现漏洞的概率。但是符号执行虽然在理论上可行,但是在实际系统中存在大量的路径分支,路径爆炸的问题很难解决。
5 fuzz 主程序的核心模块及工作流程
5.1 主要涉的核心模块
插桩模块:依据编译原理,在编译时,对目标程序基本块进行插装,以便目标程序向fuzz 系统反馈样本能够触发的路径信息,程序运行状态信息,以及程序退出信息。
监控模块:fuzz 程序根据目标程序反馈的路径信息判断被测样本是否有效,以及可以出发的路径数量;根据反馈的程序退出信息,判断程序是正常退出还是因为触发了内存破坏漏洞等原因导致退出。
变异模块:fuzz 程序结合目标程序反馈的路径信息,对文件的关键字段进行变异,以增加代码覆盖率。
forkserver 模块:该模块主要是为了启动被fuzz 程序的子进程,通过forkserver 模型,可以减少系统性能与内存开销。
插桩模块,将实现特定功能的汇编指令插入到程序的每一个基本块中;路径信息反馈模块通过插桩模块插入到被fuzz 程序中,并通过共享内存与主fuzz 进程通信;变异模块主要是通过启发式算法检测出文件token,其余的策略基于效率考虑的数据变异,主要是为了让程序在解析相关字段的时候出现非预期的值导致崩溃。
5.2 Fuzz 核心模块的工作流程
其工作流程是首先将样本读入进内存,通过forkserver 框架通过fork 系统调用产生一个被fuzz 的子进程,并将测试样本读入程序,fuzzer 记录下程序运行时相关信息,比如代码覆盖率等。然后捕获程序退出时发出的信号,并根据此信号判断程序是否异常退出,然后根据程序运行时相关信息对测试样本进行变异,然后继续读入内存传递给被fuzz 程序。如果产生crash,则将可以导致crash 的样本放入crash 文件夹,供crash 监控进程读取。
6 fuzz 运行流程
6.1 模糊测试
首先下载开源软件,并编译生成可执行文件。然后运行软件查看功能。
本次选取的软件名为pdfcrack,是一款linux 下开源的pdf文件密码破解工具。
第一步:首先将建立fuzz 所需要的输入输出目录,并将种子文件放入输入文件夹中。
第二步:编写配置文件,主要参数如下:
(1)输入目录
(2)输出目录
(3)Crashes 目录
(4)文件输入方式
(5)被fuzz 程序路径
第三步:执行fuzz 框架主程序,并读入配置文件
几分钟之后,便发现了crash,如下图1 所示。
图1 fuzz 运行
并且崩溃日志通过云端推送到了微信上面。
图2 崩溃位置
6.2 漏洞分析
结合软件崩溃位置,使用gdb 调试器可以发现崩溃位置,如图2 所示。
通过调试起可以发现这个地方s_handler 的值为0,程序试图往一个0 地址进行写入,但是操作系统是禁止这一行为的,所以存在一个0 地址解引用漏洞。
通过gdb 调试器进行函数调用回溯,发现该漏洞存在于函数loadstate 中,如图3 所示。
图3 调用回溯
通过对该函数代码进行分析,对e->s_handler 的复制存在于下面的代码中,如图4 所示。
图4 漏洞代码
函数对判断len 的大小,只要len 大于0 并且len 小于256就会通过malloc 分配一段内存,并将堆内存指针赋给e->s_handler,但是这里没有考虑len 等于0 的情况,当len 等于0,就会直接跳过申请内存,然后直接对e->s_handler 进行写操作,并没有判断是否等于0 的情况,因此造成空指针解引用,如图5 所示。
图5 崩溃位置
因为len 等于0,因此不会进入for 循环,所以崩溃发生在最后一行。
7 结论
本文以设计基于反馈驱动的fuzz 工具设计为目的,通过一系列的变异策略,可以在软件中发现更多的漏洞,并且通过实际的开源软件进行测试,证实该fuzz 工具确实有发现软件漏洞的能力。该fuzz 工具可以融合进devsecops 中,开发者自己提供软件编译,然后进行测试,这样做的好处是开发者更懂自己的程序功能,因此可以提供更好的测试样本,而且这样可以减少软件中潜在的漏洞。