如何在安全研究中使用模糊测试

介绍

模糊测试是自动软件测试中最常用的一种方法。通过模糊测试,我们可以根据一组规则来生成大量的输入,然后将这些非预期的输入注入到程序中来观察程序的异常行为。在安全领域,模糊测试是一种发现漏洞的有效方法。

市场上现在有很多模糊测试框架,包括开源的框架和商业框架。模糊测试技术主要分为两类:

基于演化的模糊测试

它们使用遗传算法来增加代码覆盖率。它们会对提供的测试用例进行修改,目的是进一步的分析应用程序。不过坦白说,这需要进行一些代码插桩来给变化引擎提供反馈。基于演化的模糊测试其实就是忽视规定的输入格式,输入各种各样的格式来查看是否有异常并研究这个异常。这种技术在开源社区中有很好的支持和维护。有几个框架非常优秀,比如American Fuzzy Lop(AFL)libfuzzerhonggfuzz等。

基于世代的模糊测试

与基于演化的模糊测试相反,它们会基于与上下文环境相关的规则和格式来构建输入。这类技术中比较优秀的工具包括商业的Defensics和PeachFuzzer,开源的包括Peach,Spike和Sulley。

这两类技术并不是对立的,也不是相互排斥的。它们只是在设计模式上有所区别。有的工具同时包含了这两种技术,比如PeachFuzzer。

在应用和威胁情报(ATI)研究中心,我们的目标之一就是识别应用程序中的漏洞,并在漏洞被发现之前协助开发者修复漏洞。我们是通过将不同的应用程连接到我们的模糊测试框架来实现的。本文我们将会向大家展示我们在安全研究中是如何使用fuzzing技术的,主要是着重讲解我们在研究开源库过程中发现的一些漏洞来展示。中国菜刀

对SDL库进行模糊测试

SDL库是一个跨平台的库,提供了实现多媒体软件如游戏和模拟器的API。该库是由C语言编写的,由社区积极维护和使用。

选择模糊测试框架

本文我们将使用著名的AFL测试框架(lcamtuf编写)来对SDL库进行模糊测试。AFL使用运行时引导技术,编译时代码插桩和遗传算法来构造大量的非预期输入来测试应用程序。这个框架曾经因为发现重大漏洞而获奖,这也是它被公认为是最好的模糊测试框架之一的原因。于是一些研究员就开始仔细研究AFL框架,并且编写了修改某些组件行为的扩展,例如异常策略和不同代码分支的重要性。这些扩展又产生了其他的项目,比如FairFuzz,AFL-GO,afl-unicorn,AFLSmart和python-AFL。

我们这里将使用AFLFast,这个项目实现了一些模糊测试策略,不仅针对高频代码路径,也针对低频代码路径,“在相同的时间内,更加强调程序的行为”。简而言之,通过研究我们发现,对于某种模糊测试对比,跟vanilla AFL相比,这种优化方法大约提高了2倍的速度和更好的代码覆盖率。

模糊测试准备

要使用AFL,我们必须要用AFL的编译器包装器来编译库的源代码。

$ ./configure CC=afl-clang-fast \
CFLAGS ="-O2 -D_FORTIFY_SOURCE=0 -fsanitize=address" \
LDFLAGS="-O2 -D_FORTIFY_SOURCE=0 -fsanitize=address"
$ make; sudo make install

如上所示,我们会同时使用AFL插桩和ASAN(地址净化器)编译工具,ASAN编译工具是用来识别内存相关的报错的。GitHub上关于ASAN的项目中提到,ASAN为已测试过程序的执行速度添加了2倍的减速,但是效果却更好,让我们可以检测到与内存相关的问题,如:

· UAF漏洞(悬空指针取消引用)

· 堆缓冲区溢出

· 栈缓冲区溢出

· 全局缓冲区溢出

· Use After Return(返回后使用)

· Use After Scope(范围后使用)

· 初始化命令错误

· 内存泄露

此外,为了优化模糊测试过程,我们使用下面这些参数编译了源代码:

-D_FORTIFY_SOURCE=0

因为ASAN不支持源代码强化,所以我们禁用它来避免错误警告

-O2

使用-O参数,开启所有的优化标志;对于LLVM3.6而言,默认设置是-O1

$ checksec /usr/local/lib/libSDL-1.2.so.0
[*] '/usr/local/lib/libSDL-1.2.so.0'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    ASAN:     Enabled

Checksec是一个不错的工具,我们可以用它来检测二进制文件的安全选项,比如二进制文件是使用非可执行堆栈(NX)构建的,还是使用重定位表作为只读(RELRO)构建的。它还可以检测二进制文件是否使用ASAN插桩构建,这也是我们需要的。这个工具是pwntools Python包的一部分。如上所示,二进制文件是启用了ASAN插桩进行编译的,现在我们来继续编译!

编写测试工具

AFL模糊测试操作包括三个主要步骤:

· 第一步:fork一个新的进程

· 第二步:输入由变化引擎修改过的内容

· 第三步:通过跟踪使用此输入最终达到了哪些路径来监控代码覆盖率,这样我们就能得知程序是否崩溃或者挂起了。

这是由AFL自动完成的,对于那些接受输入作为参数并进行解析的二进制文件,AFL就是一款非常理想的工具。但对于测试库文件而言,我们需要先编写一款测试工具并进行编译。在这里,我们的测试工具就是一个简单的C程序,可以使用库中的某些方法,让你可以对它进行间接的模糊测试。天空彩

#include <stdlib.h>
#include "SDL_config.h"
#include "SDL.h"
 
struct {
  SDL_AudioSpec spec;
  Uint8   *sound;     /* Pointer to wave data */
  Uint32   soundlen;    /* Length of wave data */
  int      soundpos;    /* Current play position */
} wave;
 
/* Call this instead of exit(), to clean up SDL. */
static void quit(int rc){
  SDL_Quit();
  exit(rc);
}
 
int main(int argc, char *argv[]){ 
    /* Load the SDL library */
  if ( SDL_Init(SDL_INIT_AUDIO) < 0 ) {
    fprintf(stderr, "[-] Couldn't initialize SDL: %s\n",SDL_GetError());
    return(1);
  }
  if ( argv[1] == NULL ) {
    fprintf(stderr, "[-] No input supplied.\n");
  }
  /* Load the wave file */
  if ( SDL_LoadWAV(argv[1], &wave.spec, &wave.sound, &wave.soundlen) == NULL ) {
    fprintf(stderr, "Couldn't load %s: %s\n",
            argv[1], SDL_GetError());
    quit(1);
  }
  /* Free up the memory */    
  SDL_FreeWAV(wave.sound);
  SDL_Quit();
  return(0);
 
}

这里我们的目的是初始化SDL环境,然后对与SDL音频模块相关的SDL_LoadWAV方法进行模糊测试。我们需要提供一个WAV示例文件,AFL将使用变化引擎来尽可能的篡改库代码。引入一些新的模糊测试术语,该文件代表着我们的初始化种子,将会放在corpus_wav文件夹中。

现在我们来进行编译:

$ afl-clang-fast -o harness_sdl harness_sdl.c -g -O2 \
  -D_FORTIFY_SOURCE=0 -fsanitize=address \
  -I/usr/local/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT \
  -L/usr/local/lib -Wl,-rpath,/usr/local/lib -lSDL -lX11  -lpthread

然后开始模糊测试:

$ afl-fuzz -i corpus_wave/ -o output_wave -m none -M fuzzer_1_SDL_sound \
    -- /home/radu/apps/sdl_player_lib/harness_sdl @@

可以看到,启动模糊测试工作十分简单,只需要使用下列参数来执行afl-fuzz:

· 初始语料库(-i corpus_wave)

· 模糊测试输出(-o output_wave)

· 编译工具路径

· 提示AFL如何发送测试样例到模糊测试程序中(提供它来作为参数)

· 子进程内存限制(-m none 因为在x86_64系统架构中,ASAN需要接近20TB的内存)

你还可以使用其他有用的参数,例如指定字典,包含与特定文件格式相关的字符串,理论上可以帮助变化引擎更快的到达某些路径。不过这里,我们就暂时先使用这些参数,来看看是如何执行的:

如何在安全研究中使用模糊测试

我们这里是在一台RAM为32G的机器上进行研究测试,该机器有2个AMD Opteron CPU,每个CPU每个插槽有4个内核,每个内核有2个线程,共有16个线程。我们可以看到,我们的模糊测试速度是每秒获得170个评估样本。我们是否能做得更好?

优化配置来提升模糊测试速度

有一些地方我们可以进行一些调增:

默认情况下,AFL在每次测试不同的输入时都会fork出一个进程,我们可以控制AFL在单个程序实例中运行多次模糊测试,不用为了每个测试样本,都将程序恢复到原始状态。这将减少内核空间中所花费的时间,从而提升模糊测试速度。这被称为AFL_PERSISTENT模式。我们可以使用测试工具加_AFL_LOOP(1000) macro参数来实现。根据这一点,指定macro,将强制AFL运行1000次,使用1000个不同的输入提供给库。之后,AFL重新启动该进程,这确保我们在定期更换过程中避免出现内存泄漏。

指定为初始语料库的测试用例大小为119KB,这个太大了,也许我们能找到一个更小的测试用例,或者提供更多测试用例,以提高初始代码覆盖率。

我们现在是在硬盘中运行模糊测试,如果我们切换到ramdisk,强制模糊测试器直接从RAM中获取测试用例,这样也能提升模糊测试速度。

最后一点,我们可以并行运行多个实例,强制AFL为每个测试实例使用1个CPU。

做了这些修改之后,来看看我们的模糊测试器如何执行,如图:

如何在安全研究中使用模糊测试

对于一个实例,速度提升了2.4倍,而且已经崩溃了。运行一个主实例和4个从属实例,状态如下:

$ afl-whatsup -s output_wave/
status check tool for afl-fuzz by <lcamtuf@google.com>
 
Summary stats
=============
 
       Fuzzers alive : 5
      Total run time : 0 days, 0 hours
         Total execs : 0 million
    Cumulative speed : 1587 execs/sec
       Pending paths : 6 faves, 35 total
  Pending per fuzzer : 1 faves, 7 total (on average)
       Crashes found : 22 locally unique

同时运行5个fuzzers,每秒有超过1500次的执行,这个速度已经相当不错了,我们来看下它们是如何运行的,如图:

如何在安全研究中使用模糊测试

总结

经过一天的模糊测试后,我们总共发生了60次特殊的崩溃事件。对它们进行分类,我们总结出了值得注意的12种崩溃事件,并且已经提交到SDL社区和MITRE。实际上,这些提交的漏洞也获得了很多CVE编号,比如CVE-2019-7572,CVE-2019-7573,CVE-2019-7574,CVE-2019-7575,CVE-2019-7576,CVE-2019-7577,CVE-2019-7578,CVE-2019-7635,CVE-2019-7636,CVE-2019-7637,CVE-2019-7638。库的维护者也承认在最新的2.0.9版本中确实存在上述这些漏洞。只是为了强调有的漏洞能够隐藏很多年,这次提交的漏洞中,有几个是因为2006年所提交的一个漏洞所引入的,一直都没人发现,直到现在。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
测试的主要评测方法 简介   测试的主要评测方法包括覆盖和质量。   测试覆盖是对测试完全程度的评测,它建立在测试覆盖基础上,测试覆盖是由测试需求和测试用例的覆盖或已执行代码的覆盖表示的。   质量是对测试对象(系统或测试的应用程序)的可靠性、稳定性以及性能的评测。质量建立在对测试结果的评估和对测试过程确定的变更请求(缺陷)的分析的基础上。 覆盖评测   覆盖指标提供了"测试的完全程度如何?"这一问题的答案。最常用的覆盖评测是基于需求的测试覆盖和基于代码的测试覆盖。简而言之,测试覆盖是就需求(基于需求的)或代码的设计/实施标准(基于代码的)而言的完全程度的任意评测,如用例的核实(基于需求的)或所有代码行的执行(基于代码的)。   系统的测试活动建立在至少一个测试覆盖策略基础上。覆盖策略陈述测试的一般目的,指导测试用例的设计。覆盖策略的陈述可以简单到只说明核实所有性能。   如果需求已经完全分类,则基于需求的覆盖策略可能足以生成测试完全程度的可计量评测。例如,如果已经确定了所有性能测试需求,则可以引用测试结果来得到评测,如已经核实了 75% 的性能测试需求。   如果应用基于代码的覆盖,则测试策略是根据测试已经执行的源代码的多少来表示的。这种测试覆盖策略类型对于安全至上的系统来说非常重要。   两种评测都可以手工得到(公式如下所示)或通过测试自动化工具计算得到。 基于需求的测试覆盖   基于需求的测试覆盖在测试生命周期要评测多次,并在测试生命周期的里程碑处提供测试覆盖的标识(如已计划的、已实施的、已执行的和成功的测试覆盖)。   在执行测试活动使用两个测试覆盖评测,一个确定通过执行测试获得的测试覆盖,另一个确定成功的测试覆盖(即执行时未出现失败的测试,如没有出现缺陷或意外结果的测试)。   这些覆盖评测通过以下公式计算:   这一关于测试覆盖的陈述是有意义的,可以将其与已定义的成功标准进行对比。如果不符合该标准,则此陈述将成为预测剩余测试工作量的基础。 基于代码的测试覆盖   基于代码的测试覆盖评测测试过程已经执行的代码的多少,与之相对的是要执行的剩余代码的多少。代码覆盖可以建立在控制流(语句、分支或路径)或数据流的基础上。控制流覆盖的目的是测试代码行、分支条件、代码的路径或软件控制流的其他元素。数据流覆盖的目的是通过软件操作测试数据状态是否有效,例如,数据元素在使用之前是否已作定义。   基于代码的测试覆盖通过以下公式计算: 质量评测   测试覆盖的评估提供对测试完全程度的评测,在测试过程已发现缺陷的评估提供了最佳的软件质量指标。因为质量是软件与需求相符程度的指标,所以在这种环境,缺陷被标识为一种更改请求,该更改请求测试对象与需求不符。   缺陷评估可能建立在各种方法上,这些方法种类繁多,从简单的缺陷计数到严格的统计建模不一而足。   严格的评估假定测试过程缺陷达到的比率或发现的比率。常用模型假定该比率符合泊松分布。则有关缺陷率的实际数据可以适用于这一模型。生成的评估将评估当前软件的可靠性,并且预测继续测试并排除缺陷时可靠性如何增长。该评估被描述为软件可靠性增长建模,这是一个活跃的研究领域。由于该类型的评估缺乏工具支持,所以应该慎重平衡成本与其增加价值。   缺陷分析就是分析缺陷在与缺陷关联关系的一个或多个参数值上的分布。缺陷分析提供了一个软件可靠性指标。   对于缺陷分析,常用的主要缺陷参数有四个:   • 状态:缺陷的当前状态(打开的、正在修复或关闭的等)。   • 优先级:必须处理和解决缺陷的相对重要性。   • 严重性:缺陷的相关影响。对最终用户、组织或第三方的影响等等。   • 起源:导致缺陷的起源故障及其位置,或排除该缺陷需要修复的构件。   可以将缺陷计数作为时间的函数来报告,即创建缺陷趋势图或报告;也可以将缺陷计数作为一个或多个缺陷参数的函数来报告,如作为缺陷密度报告采用的严重性或状态参数的函数。这些分析类型分别为揭示软件可靠性的缺陷趋势或缺陷分布提供了判断依据。   例如,预期缺陷发现率将随着测试进度和修复进度而最终减少。可以设定一个阈值,在缺陷发现率低于该阈值时才能部署软件。也可根据执行模型的起源报告缺陷计数,以允许检测"较差的模块"、"热点"或需要再三修复的软件部分,从而指示一些更基本的设计缺陷。   这种分析包含的缺陷必须是已确认的缺陷。不是所有已报告的缺陷都报告实际的缺陷,这是因为某些缺陷可能是扩展请求,超出了项目的规模,或描述的是已报告的缺陷。然而,需要查看并分析一下,为什么许多报告的缺陷不是重复的缺陷就是未经确认的缺陷,这样做是有价值的。 缺陷报告   Rational Unified Process 以三类形式的报告提供缺陷
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值