模糊测试
基本概念
模糊测试是一种软件测试技术,其核心思想是自动或半自动的生成随机数据输入到一个程序中,并监视程序异常,如崩溃,断言(assertion)失败,以发现可能的程序错误,比如内存泄漏。
假如该程序在应对任一输入值时失效,开始出现冲突、锁住、消耗大量内存或者产生不可控制的程序错误,开发者就知道代码中某处出现了bug。
比较有效的模糊测试,并不是用大量纯随机数据直接来测试,而是对已知有效数据、故意错误数据和随机数据的联合调试。使用已知有效数据的目的是跳过不感兴趣的代码片段,或者说是为了防止程序在抵达一个欠缺的代码块前出现拒绝信息。使用故意错误数据的目的是利用已知或我怀疑将成为代码中缺陷的情况。最后,使用随机数据的目的就是看看会发生什么。
模糊测试流程
确定测试目标
-
确定输入向量
几乎漏洞的形成原因大都是对于客户端传输的数据没有经过过滤等,在服务端造成了恶意的影响。
例如WEB端进行fuzz:从客户端向服务端传输数据包{ 能传输的任何数据(例如header,useragent等)},这些传输的数据大多数都可能为模糊测试变量。
-
生成模糊测试数据
基于测试中,如何选择自动化输入的数据,是使用字典,还是用随机数,还是特定生成的字典呢。
-
执行模糊测试数据
在这个步骤中,就要向你的服务端发送数据包(WEB端),发送在上一步骤中生成的数据包。当然web端还是要小心waf,如果一次性发10k个数据包,那么只会引起反效果。
-
监视回显
在这一步,根据测试目标的不同自然监视方式也不同,要根据测试的类型来设置各样的监视
-
判断漏洞是否能被利用
根据测试的类型不同,那么前几个阶段的权重,顺序或许可以进行调整,但是仍不能保证可百分百发现所有安全相关的缺陷。
WinAFL
从原理上来说 AFL 通过变异软件输入的数据来进行软件 Bug 挖掘,与一些模糊测试器不同的是 AFL 变异数据的方法是通过覆盖率算法来实现的,而不是通过格式。AFL通过对源码进行重新编译时进行插桩(简称编译时插桩)的方式自动产生测试用例来探索二进制程序内部新的执行路径。由于需要在相关代码处插桩,因此AFL主要用于对开源软件进行测试。
WinAFL 的原理和其他的模糊测试工具类似,通过对程序输入的数据进行变异处理,观察程序在处理这些变异数据是否会产生 Crash,以此来验证程序是否有 Bug。但是相对于其它模糊测试工具来说则更为智能,因为它会通过进化算法不停的改变程序输入的数据,并且结合程序的覆盖率以进行下一步操作。运行的速度越快,效果越明显。
最开始给afl初始种子,然后经过变异得到输入,将输入给到待测试程序,并通过插桩技术对程序跟踪,或者覆盖率。然后覆盖率指导之前的流程,周而复始,直到遇到崩溃情况,便将其保存。这种覆盖率的想法使得输入不再是随机盲目的。
插桩编译——fuzzing——分析crashes
(将testcase作为程序的输入执行程序,afl会在这个testcase的基础上进行自动变异输入)
安装过程见下面的两篇博客: VisualStudio2017+Windows10
https://www.giantbranch.cn/2020/12/25/winafl%E7%BC%96%E8%AF%91%E4%B8%8E%E6%B5%8B%E8%AF%95/
https://www.anquanke.com/post/id/210457
WinAFL 项目地址:https://github.com/ivanfratric/winafl
DynamoRIO 项目地址:https://github.com/DynamoRIO/dynamorio/wiki/Downloads
实例分析
使用winafl下的test_gdiplus.exe进行测试
1. DynamoRIO
winafl 动态插桩是调用DynamoRIO 下的 “drrun.exe” 进行的。动态二进制插桩框架DynamoRIO通过将程序代码进行反复插桩(Instrumentation)执行构建了源程序代码与操纵代码之间的桥梁。我们可以简单使用”drrun.exe”来获取目标程序执行过程中所加载的模块,从而推断DynamoRIO编译是成功并可用的,这也是官方教程里的做法。
任意找一个bmp格式的图片放到上面我们编译后的winafl\b32\bin\Release目录下。然后运行下面命令:
D:\WinAFL\my_winafl_fuzz\DynamoRIO-Windows-8.0.0-1\bin64\drrun.exe -c winafl.dll -debug -target_module test_gdiplus.exe -target_offset 0x10e0 -fuzz_iterations 5 -nargs 2 -- test_gdiplus.exe 1.bmp
这里的偏移量0x10e0是通过ida查看main函数是sub_1400010E0,基址是0x140000000,偏移是0x10e0。
drrun的参数说明:
-c <client> [client options] — <app and args to run>
// winafl.dll 参数说明。这也是插桩参数的使用[instrumentation options]
-debug # debug模式, 它会生成一个log文件
-target_module # 目标程序(只能有一个), 也是target_offset所在的模块
-target_offset # 目标程序偏移,相对于target_module的偏移,在method无法导出的时候使用
-fuzz_iterations # 在重新启动目标进程之前,目标函数要运行的最大迭代次数
-nargs # 目标程序执行所需要的参数个数(包括目标程序本身)
-target_module # 目标函数,需要export或者调试符号(pdb)
-coverage_module # 计算覆盖率的模块,也就是目标程序会调用的模块(dll); (可以有多个)
执行之后产生一个log文件,打开是目标程序执行过程中所所调用的dll
2.WinAFL
在winafl\build64\bin\Release目录下,分别新建 in、out 文件夹;然后把bmp格式图片放到 in 文件夹下。然后运行下面命令。
afl-fuzz.exe -i in -o out -D D:\WinAFL\my_winafl_fuzz\DynamoRIO-Windows-8.0.0-1\bin64\ -t 20000 -- -coverage_module gdiplus.dll -target_module test_gdiplus.exe -target_offset 0x10e0 -fuzz_iterations 20 -nargs 2 -- test_gdiplus.exe @@
参数说明:
-i # 存放样本的目录
-o # 保存输出数据,包括 crash文件、测试用例等
-D # DynamoRIO的路径 (drrun, drconfig)
-t msec # 每一次样本执行的超时时间
-- # 分割符
****************************
第一个"--"分割符: 后面跟的是插桩的参数
-coverage_module gdiplus.dll 为fuzzing对象程序会调用的模块,也就是说你fuzzing的偏移地址的函数会调用到这些模块里面的函数,通过插桩时winafl.dll的-debug模式获得,然后根据反汇编代码判断调用了那些模块,
在无法自行判断的情况下我们写的跟-target_module一样即可。
****************************
第二个"--"分割符: 后面跟的是目标程序的参数
在执行过程中,afl-fuzz会把@@替代测试样本,第一个参数可以之前拷贝到in目录下的1.bmp,用@@来代替,@@所代表传入的是变异后的输入。最后实际执行的命令为test_gdiplus.exe 1.bmp/变异输入
查看fuzzing状态,主要看右上角的“uniq crashes”:
process timing:包括当前fuzzer的运行时间、最近一次发现新执行路径的时间、最近一次崩溃的时间、最近一次超时的时间。
-
overall results:包括运行的总周期数、总路径数、崩溃次数、超时次数。
总周期数可以用来作为何时停止fuzzing的参考。随着不断地fuzzing,周期数会不断增大,其颜色也会由洋红色,逐步变为黄色、蓝色、绿色。一般来说,当其变为绿色时,代表可执行的内容已经很少了,继续fuzzing下去也不会有什么新的发现了。此时,我们便可以通过Ctrl-C,中止当前的fuzzing
-
stage progress:包括正在测试的fuzzing策略、进度、目标的执行总次数、目标的执行速度
执行速度可以直观地反映当前跑的快不快,如果速度过慢,比如低于500次每秒,那么测试时间就会变得非常漫长。如果发生了这种情况,那么我们需要进一步调整优化我们的fuzzing
3. BugID
等所有测试结束之后,打开out文件夹,crashes文件夹里面是我们产生crash的样例,hangs里面是产生超时的样例,queue里面是每个不同执行路径的测试用例。
整理被模糊测试找出的 Bug,调试一下出现 Crash 的原因,以及是否可以被利用。当然这个过程比较繁琐,利用 BugID(https://github.com/SkyLined/BugId) 可以帮助你进行 Bug 分类,或者编写 Windbg 脚本。
winafl的局限性体现在如果fuzzing的目标程序,没有函数导出表(dll)、不接受命令行参数、源代码也没有、环境变量也不调用的话,就很难fuzzing。解决的办法就只有hook和逆向。
参考资料:
https://zhuanlan.zhihu.com/p/89039547
https://www.cnblogs.com/studyskill/p/7115913.html
https://blog.csdn.net/lei_saint/article/details/49158943