Dowsing for Overflows: A Guided Fuzzer to Find Buffer Boundary Violations

目录

 

摘要

1.介绍

2.big picture

2.1运行示例

2.2高级概述

3.寻找候选指令

3.1 Building analysis groups

3.2 Conditions guarding analysis groups

3.3 Scoring array accesses

4.Using tainting to find inputs that matter

4.1 Baseline: dynamic taint analysis


摘要

Dowser是一种“引导式”模糊器,它结合了污点跟踪、程序分析和符号执行,以发现隐藏在程序逻辑深处的缓冲区溢出和下溢漏洞。关键的思想是,对程序的分析使我们能够精确地找出程序代码中要探测的正确区域,以及此探测需要的适当输入。

直观地说,对于典型的缓冲区溢出,我们只需要考虑循环中访问数组的代码,而不需要考虑程序中所有可能的指令。在找到所有这些候选指令集之后,我们根据对它们包含有趣漏洞的可能性的估计对它们进行排序。然后,我们对最有希望的集合进行进一步的测试。具体地说,我们首先使用污点分析来确定哪些输入字节影响数组索引,然后使用符号化输入

符号执行程序。通过不断地沿着最有可能导致溢出的分支结果引导符号执行,我们能够检测出实际程序中的深层bug(比如nginx webserver、inspircd IRC服务器和ffmpeg视频播放器)。我们发现的两个bug是之前在ffmpeg和poppler PDF呈现库中没有文档说明的缓冲区溢出。

1.介绍

我们将讨论Dowser,一种结合污点跟踪、程序分析和符号执行的“引导式”模糊器,以发现隐藏在程序逻辑深处的缓冲区溢出错误。缓冲区溢出经常出现在最危险的软件错误[12]的前3位中,最近的研究表明这种情况不会很快改变[41,38]。有两种方法来处理它们。我们要么使用内存保护程序来加强软件,当溢出发生时(在运行时)终止程序,要么在发布软件之前跟踪漏洞(例如,在测试阶段)。

内存保护器包括一些常见的解决方案如影子堆栈(当调用的时候将返回地址存储在另一个地方,当程序返回时比较堆栈中的返回地址和存储在别的地方的返回地址比较,如果不同将导致崩溃)和canaries[11],以及更复杂的编译器扩展,如WIT[3]。它们可以有效地防止程序被利用,但它们本身并不能消除溢出错误。虽然崩溃比允许使用要好,但是崩溃也是不可取的!

因此,供应商更喜欢事先消除bug,通常会通过模糊测试尽可能多地找到bug。Fuzzers向程序提供无效、意外或随机数据,以查看它们是否导致程序有崩溃或表现出意外行为。例如,微软为每种产品的每一个不受信任的界面强制使用模糊测试,而他们的模糊测试解决方案自2008年7月24日以来一直运行,总共运行了400多次的[18]。

不幸的是,大多数模糊器的有效性都很差,结果很少超出浅层bug的范围。大多数fuzzer采用“黑盒”方法,它只关注输入格式,而忽略测试的软件目标。Blackbox fuzzing是一种流行且快速的方法,但它忽略了许多相关的代码路径,因此也会产生许多bug。黑盒模糊有点像在黑暗中射击:你必须幸运地击中任何有趣的东西。

在[18,7,10]中实现的Whitebox fuzzing更有原则。通过符号执行,探索在程序中执行所有可能的执行路径,从而发现所有可能的bug——尽管这可能需要几年的时间发展。由于全符号执行速度较慢,且不能扩展到大型程序,因此很难使用它来发现大型程序中的复杂bug[7,10]。在实践中,目标是首先覆盖尽可能多的独特代码。因此,除非在非常简单的情况下,否则很难触发需要程序多次执行相同代码的bug(比如缓冲区溢出)。

由符号执行提供的最终完整性既是优点也是缺点,在本文中,我们将评估完全相反的策略。我们不是测试所有可能的执行路径,而是对一小部分代码区域执行抽查,这些代码区域看起来可能是缓冲区溢出错误的候选区域,然后依次测试每个代码区域。

这种方法的缺点是,我们以迭代的方式为每个候选代码区域执行符号运行。此外,我们只能在可以执行的循环中发现缓冲区溢出。另一方面,通过直接瞄准有希望的代码区域,我们大大加快了搜索速度,并设法在实际程序中找到了一些复杂的bug,而这些bug对于大多数现有的模糊器来说是很难找到的。

Contributions:我们给自己设定的目标是开发一种有效的模糊器,它可以主动地直接搜索缓冲区溢出。其中的关键是,对程序的仔细分析使我们能够精确地找到探测的正确位置和适当的输入。主要的贡献是,我们的fuzzer直接放大这些缓冲区溢出候选项,并在符号执行中探索一种新的“抽样检查”方法。

要使这一方法发挥作用,我们需要应对两项主要挑战。第一个挑战是如何控制程序的执行以增加发现漏洞的机会。Whitebox fuzzers“盲目”地试图执行尽可能多的程序,希望最终能碰到bug。相反,Dowser使用有关目标程序的信息来识别最容易受到缓冲区溢出影响的代码。

例如,缓冲区溢出(主要)发生在访问循环中的数组的代码中。因此,我们寻找这样的代码并忽略程序中其余的大部分指令。此外,Dowser对程序执行静态分析来对这些访问进行排序。我们将评估不同的排序函数,但是到目前为止,最好的一个根据复杂度对数组访问进行排序。直观的感觉是,使用卷积指针算法和/或复杂控制流的代码比直接的数组访问更容易出现内存错误。此外,通过关注这些代码,Dowser优先处理复杂的bug——通常是静态分析或随机模糊无法发现的那种漏洞。这样做的目的是减少浪费在浅层bug上的时间,这些bug也可以通过现有方法找到。尽管如此,其他排序方式也是可能的,Dowser完全不知道使用哪种排序函数。

我们要解决的第二个挑战是如何将程序的执行引导到这些“有趣的”代码区域。作为基线,我们使用动态符号执行[43]:具体值执行和符号值执行的组合,其中具体的(固定的)输入从符号执行开始。在Dowser中,我们通过两个优化来增强动态符号执行。

首先,提出了一种新的路径选择算法。正如我们前面看到的,传统的符号执行的目标是代码覆盖—最大化所执行的单个分支的比例[7,18]。相反,我们的目标是所选代码片段的指针值覆盖率。当Dowser检查一个有趣的指针取消引用时,它将沿着可能更改指针值的分支引导符号执行。

其次,我们尽可能减少符号输入的数量。具体来说,Dowser使用动态污点分析来确定哪些输入字节影响用于数组访问的指针。稍后,它只将这些输入视为符号。虽然污点分析本身并不新鲜,但是我们引入了新的优化,以达到一组尽可能精确的符号输入(既不需要太少的符号字节,也不需要太多的符号字节)。

总之,Dowser是一种新的fuzzer,它针对的是那些希望测试其代码的缓冲区溢位和溢位的供应商。我们在LLVM pass实现了对下行器的分析,而符号执行步骤使用了S2E[10]。最后,Dowser是一个实用的解决方案。它不是针对所有可能的安全bug,而是专门针对缓冲区溢出类(对于代码注入来说,如果不是最重要的攻击向量类,也是最重要的攻击向量类之一)。到目前为止,Dowser在nginx、ffmpeg和inspircd等复杂程序中发现了一些真正的bug。在现有的符号执行工具中,很难找到它们中的大多数。

假设和大纲:在本文中,我们假设我们有一个测试套件,它允许我们访问数组。我们无法测试我们无法达到的指令。剩余部分,我们从一个大的,正在运行的例子开始(第二节)。然后,我们依次讨论Dowser的三个主要组成部分:选择有趣的代码片段(第三节),使用动态污点分析,以确定哪些输入影响候选人指令(4节),和我们的方法推动该计划引发错误符号执行期间(第五节)。我们在第六节评估系统,在第七节讨论相关的项目。我们在第8节中结束。

2.big picture

Dowser的主要目标是操作指令用来访问循环中的数组的指针,希望强制缓冲区溢出或不足。

2.1运行示例

nginx中的缓冲区欠运行漏洞

int ngx_http_parse_complex_uri(ngx_http_request_t *r)
{
    state = sw_usual;
    u_char* p = r->uri_start;   // user input
    u_char* u = r->uri.data; // store normalized uri here
    u_char ch = *p++;            // the current character
    while (p <= r->uri_end) {
        switch (state) {
               case sw_usual:
                  if (ch == '/')
                      state = sw_slash; *u++ = ch;
                  else if /* many more options here */
                  ch = *p++; break; 
               case sw_slash:
                  if (ch == '/')
                      *u++ = ch;
                  else if (ch == '.')
                       state = sw_dot; *u++ = ch;
                  else if /* many more options here */
                  ch = *p++; break; 
               case sw_dot:
                  if (ch == '.')
                       state = sw_dot_dot; *u++ = ch;
                  else if /* many more options here */
                  ch = *p++; break; 
               case sw_dot_dot:
                  if (ch == '/')
                      state = sw_slash; u -=4;
                      while (*(u-1) != '/') u--;
                  else if /* many more options here */
                  ch = *p++; break;
           }
     }
 } 

 Nginx是一家网络服务器公司,在全球最繁忙的100万个网站中,其市场份额排名第三。在撰写本文时,它在全球拥有大约2200万个域名。0.6.38之前的版本有一个特别糟糕的漏洞[1]。当nginx接收到HTTP请求时,解析函数nginx HTTP parse complex uri首先在p=r->uri_start(第4行)中规范化uri路径,将结果存储在由u=r->uri.data指向的堆缓冲区中。while-switch实现了一个状态机,它每次消耗一个字符的输入,并将其转换为u中的规范形式。当提供一个精心设计的路径时,nginx错误地将u的开头设置为r->uri.data下面的某个位置。假设uri是“//../foo”。当p达到“/foo”时,u指向(r->uri.data+4),状态为sw_dot_dot(第30行)。该例程现在将u减少4(第32行),因此它指向r->uri.data。只要r->uri.data下面的内存不包含字符“/”,u就会进一步减小(第33行),即使它跨越了缓冲区边界。最后,用户提供的输入(“foo”)被复制到u指向的位置。在这种情况下,覆盖的缓冲区包含一个指向函数的指针,该函数最终将被nginx调用。因此,该漏洞允许攻击者修改函数指针,并在系统上执行任意程序。这是一个复杂的bug,很难用现有的解决方案找到它。依赖于符号输入的许多条件语句对于符号执行来说是有问题的,而依赖于输入的间接跳转对于静态分析来说也是一个糟糕的匹配。

在本文中,我们将使用图1中的函数来说明Dowser是如何工作的。该示例是nginx-0.6.32 web服务器中缓冲区未运行漏洞的简化版本。一个特别设计的输入欺骗程序将u指针设置为缓冲区边界之外的位置。当该指针稍后用于访问内存时,它允许攻击者重写函数指针,并在系统上执行任意程序。

图1只显示了原始函数的一段摘录,该函数实际上包含大约400行C代码。它在switch语句中包含许多附加选项,以及一些嵌套的条件if语句。这种复杂性严重阻碍了静态分析工具和符号执行引擎对bug的检测。例如,当我们将S2E[10]一直引导到易受攻击的函数,并仅将HTTP消息的7字节长uri路径设置为符号时,跟踪这个有问题的场景花费了60多分钟。在实践中,需要一个更可伸缩的解决方案。没有这些提示,S2E在长达8小时的执行过程中根本没有发现这个bug。相比之下,Dowser不到5分钟就找到了。

S2E中分析成本高的主要原因是依赖于(符号)输入的大量条件分支。对于每个分支,符号执行首先检查条件或其否定是否可满足。当两个分支都可行时,默认行为是检查两个分支。这个过程导致路径的指数增长。

这个真实的例子显示了(1)需要将强大而昂贵的符号执行集中在最有趣的情况上,(2)做出明智的分支选择,(3)最小化符号数据量。

2.2高级概述

图2展示了总体探测体系结构。首先,它对目标程序执行数据流分析,并对循环1中访问缓冲区的所有指令进行排序。虽然我们可以用不同的方式对它们进行排序,而且Dowser不知道我们使用的排序函数,但是我们目前的经验是,对复杂性的估计效果最好。具体地说,我们将计算和复杂条件的等级排列得比简单条件高。在图1中,u涉及三个不同的操作,即, u++, u-,和u-=4,在一个循环中的多个指令中。我们将看到,这些复杂的计算将u的取消引用放在nginx中最复杂的指针访问的前3%中。

在第二步2中,Dowser反复选择高级访问,并选择测试输入来执行它们。然后,它使用动态污点分析来确定哪些输入字节影响候选指令中取消引用的指针。这个想法是,给定的格式输入,Dowser fuzz(即当作符号),只有那些字段,影响潜在的脆弱的内存访问,并让剩下的不变。在图1中,我们了解到将HTTP请求中的uri路径视为符号就足够了。实际上,脆弱函数内部的计算独立于输入消息的其余部分。

接下来的3步,对于计算数组指针所涉及的每个候选指令和输入字节,Dowser使用符号执行来尝试将程序推向溢出缓冲区的方向。具体来说,我们符号执行包含候选指令的循环(因此应该对缓冲区溢出进行测试)——只将相关字节作为符号处理。我们将看到,一种新的路径选择算法有助于引导执行快速达到可能的溢出。

最后,我们检测可能发生的任何溢出。就像在whitebox fuzzers中一样,我们可以使用任何技术来做到这一点(例如Purify、Valgrind[30]或BinArmor[37])。在我们的工作中,我们使用谷歌的addresssanifier[34] 4。它利用受保护的程序来确保内存访问指令永远不会读或写所谓的“中毒”红区。红色区域是插入到任何两个堆栈、堆或全局对象之间的内存小区域。由于程序永远不应该处理它们,因此对它们的访问表明存在非法行为。该策略检测顺序缓冲区溢出和溢出,以及一些更复杂的指针损坏bug。这种技术在搜索新bug时非常有用,因为它还会在静默故障时触发,而不仅仅是应用程序崩溃。在nginx的例子中,addresssanifier检测u指针读取缓冲区边界之外的内存时的下溢(第33行)。

我们在第3节解释了第1步(静态分析),第4节解释了第2步(污染分析),第5节解释了第3步(引导执行)。

3.寻找候选指令

以前的研究表明,从软件构件中收集的软件复杂度度量有助于发现易受攻击的代码组件[16,44,35,32]。然而,尽管复杂性度量可以作为有用的指标,但它们也存在精度低或召回值低的问题。此外,目前的大多数方法都是在模块或文件的粒度上操作的,这对于Dowser中的定向符号执行来说太粗糙了。正如Zimmermann等人观察到的[44],我们需要利用漏洞的独特特征的度量,例如缓冲区溢出或整数溢出。原则上,Dowser可以使用任何能够对访问循环缓冲区的指令组进行排序的度量。所以,问题是如何设计一个好的复杂度度量来满足这个标准?在本节的其余部分中,我们将介绍这样一种度量:一种基于启发式的方法,它是专门为检测潜在缓冲区溢出漏洞而设计的。

我们利用了复杂缓冲区溢出背后的一个主要实用原因:程序员很难理解复杂的指针计算。因此,我们主要关注在循环内部实现的“复杂”数组访问。此外,我们将分析限制在与循环归纳变量一起演化的指针上,即,以访问数组的(各种)元素。

使用这个度量,Dowser通过评估数组索引(指针)计算中涉及的数据流和控制流的复杂性来对缓冲区访问进行排序。每个循环的程序时,它首先静态地确定(1)所涉及的所有指令修改数组指针(我们将称之为一个指针的分析组),和(2)保护这个分析组的条件,例如,包含数组索引计算的if或while语句的条件。接下来,它用反映其复杂性的分数给所有这些集合贴上标签。我们将在3.1、3.2和3.3节中详细解释这些步骤。

图3:图1中与u指针相关联的数据流图和分析组。为了清晰起见,图中用伪代码表示指针算术指令。PHI节点表示从不同的控制流合并数据的位置。方框中的数字表示Dowser分配的点。

3.1 Building analysis groups

假设一个指针p在一个循环中涉及到一个“有趣的”数组访问指令accp。与accp关联的分析组AG(accp)收集在循环执行期间影响取消引用指针值的所有指令。

为了确定AG(accp),我们计算一个过程内的数据流图,该数据流图表示循环中的操作,计算accp中取消引用的p的值。然后,我们检查这个图是否包含循环。循环表示p在前一个循环迭代中的值影响当前循环迭代中的值,因此p依赖于循环诱导变量。

如前所述,我们的这部分工作构建在LLVM[23]编译器基础设施之上。LLVM提供的静态单赋值(SSA)表单直接转换为数据流图。图3显示了一个示例。注意,由于指针u的所有解引用都共享它们的数据流图,所以它们也形成一个单独的分析组。因此,当Dowser稍后试图在这个分析组中找到一个非法的数组访问时,它会同时测试所有的取消引用——没有必要单独考虑它们。

3.2 Conditions guarding analysis groups

与数组指针关联的数据流可能很简单,但是由于一些复杂的控件更改,指针的值很难跟随。因此,探测者等级也控制着流量:影响分析组的条件。假设操作数组指针p的指令由变量var上的条件保护,例如if(var<10){*p++=0;}。如果var的值难以跟踪,那么p的值也难以跟踪。为了评估var的复杂性,Dowser分析其数据流,并确定分析组AG(var)(如3.1节所述)。此外,我们还递归地分析了循环内影响var和p的其他变量的分析组。因此,我们得到了一些分析组,我们将在下一个步骤中对它们进行排序(第3.3节)。

3.3 Scoring array accesses

对于在循环中实现的每个数组访问,Dowser评估3.1和3.2节中构建的分析组的复杂性。对于每个分析组,它考虑所有指令,并为它们分配点数。一个积分制的AG分数越多,它就越复杂。数组访问的总秩由得分的最大值决定。直观地说,它反映了最复杂的组件。

评分算法应该为语义相同的代码提供大致相同的结果。因此,我们强制执行LLVM编译器中提供的优化(例如,消除常见的子表达式)。这样,我们就可以最小化编译器选项所产生的指令量的差异。此外,我们还分析了LLVM代码生成策略,并定义了一组功能强大的等价规则,这些规则最小化了分配给语法不同但语义等价的代码的分数的变化。我们在下面突出显示它们。

表1介绍了所有类型的指令,并讨论了它们对最终得分的影响。原则上,除了我们认为有风险的两条指令:指针强制转换和返回指针计算中使用的非指针值的函数之外,数组索引计算中涉及的所有常见指令的顺序都是10个点。

每种类型的指令的绝对惩罚不是很重要。但是,我们确保这些点反映了不同代码片段之间复杂性的差异,而不是给所有数组访问相同的分数。也就是说,使数组索引复杂化的指令会对得分有贡献,而使索引非常复杂的指令相对于其他指令的得分也非常高。在第6节中,我们将我们的复杂性排名与备选方案进行比较。

表1:指针算术操作中涉及的指令及其惩罚点的概述。
指令 基本原理/等价规则
数组索引操作
基本索引算法,即加减法 GetElemPtr(按索引增加或减少指针)得分相同。因此,指针上的操作等同于偏移量上的操作。如果一条指令修改了一个未传递给下一个循环迭代的值,则该指令得分为1。 1 or 5
其他索引算术instr。例如,除法、移位或异或 这些指令涉及到比标准的添加或子指针计算更复杂的指针计算,因此,我们对它们进行了更多的惩罚。 10
不同的常量值 用于修改指针的多个常量使其值难以跟随。跟踪总是以相同值递增的指针更容易。 每个值10分
用于访问结构字段的常量 我们假设编译器正确地处理对结构的访问。我们只考虑用于计算数组索引的常量,而不考虑字段的地址。 0
在循环外确定的数值 虽然在循环上下文中它们只是常量,但是编译器不能预测它们的值。因此,它们很难推理,更容易出错。 30
返回非指针值的非内联函数 由于将指针的计算与其使用解耦可能很容易导致错误,因此我们将严重惩罚这种操作。 500
数据转移指令 移动(标量或指针)数据不会增加计算的复杂性。 0
高级操作
加载循环外计算的指针 它表示检索对象的基本指针,或使用内存分配器。我们以同样的方式对待所有远程指针——所有的得分都为0。 0
GetElemPtr 从基和偏移量计算指针的LLVM指令。 1或5
指针类型操作 由于转换指令经常指示不等同于标准指针操作的操作(上面列出的),因此值得仔细检查。 100

4.Using tainting to find inputs that matter

一旦Dowser按复杂度对循环中的数组访问进行了排序,我们将依次检查它们。通常,只有一小部分的输入影响的执行一个特定的分析小组,所以我们要寻找一个错误仅仅通过修改这部分的输入,同时保持其他常数(参见第五节)。在当前的部分中,我们说明Dowser识别程序的组件输入之间的联系和不同的分析。注意,这个结果也有利于基于模糊的其他bug发现工具,而不仅仅是Dowser和动态符号执行。

我们将讨论重点放在与数组指针引用accp关联的分析组AG(accp)上。我们假设我们可以获得一个测试输入I,它用于测试潜在的易受攻击的分析组。虽然这可能并不总是正确的,但我们相信这是一个合理的假设。大多数供应商都有测试套件来测试他们的软件,并且它们通常包含至少一个输入来测试每个复杂的循环。

4.1 Baseline: dynamic taint analysis

作为一种基本方法,Dowser对输入I执行动态污点分析(DTA)[31](用唯一的颜色污染每个输入字节,并在数据移动和算术操作上传播颜色)。然后,它记录在AG(accp)指令中涉及的所有颜色和输入字节。给定输入的格式,Dowser将这些字节映射到各个字段。在图1中,Dowser将uri视为符号就足够了。

如上所述,DTA的问题是它完全忽略了隐式流(也称为控制依赖关系)[14,21]。这样的流没有将受污染的值直接分配给变量——变量将由DTA传播。相反,变量的值完全由条件下受污染变量的值决定。在图1中,即使第12行u的值依赖于第11行受污染的字符ch,污染也不会直接流向u,因此DTA不会报告这种依赖关系。隐式流是出了名的难以跟踪[36,9],但是忽略它们完全降低了我们的准确性。因此,Dowser采用了一种基于Bao等人的[6]工作的解决方案,但是采用了一种新的优化来提高分析的准确性(第4.2节)。

与Bao等人的[6]一样,Dowser实现了严格的控制依赖关系。直观地说,我们只在信息最丰富(或信息保存)的依赖项上传播颜色。具体来说,我们需要在受污染的变量和编译时常量之间进行直接比较。例如,在图1中,我们将第11行ch的颜色传播到变量state,并将u传播到第12行。但是,如果第11行中的条件是“if(ch!= ' / ')”或“if(ch< ' / ')”,我们将保持状态和u不受污染。由于隐式流不是本文的重点,所以我们希望有兴趣的读者参考[6]了解更多细节

4.2 Field shifting to weed out false dependencies

图4:图中显示了Dowser如何打乱输入,以确定哪些字段真正影响分析组。假设一个解析器逐个提取输入的字段,并且分析组依赖于字段B和D(分别使用颜色B和D)。处理程序中的颜色显示后续处理程序严格依赖于哪些字段,阴影矩形表示传播到分析组的颜色。排除在外的颜色被排除在我们的分析之外。

 如上所述,Dowser改进了Bao等人对严格控制依赖关系的处理。当输入格式中的字段顺序不固定时,就会出现问题,例如HTTP、SMTP(以及大多数程序的命令行)。来自[6]的方法可能错误地认为字段依赖于到目前为止提取的所有字段。

例如,lighttpd在循环中读取新的头字段,并将它们与各种选项进行比较,大致如下:

while()
{
    if(cmp(field, "Content") == 0)
        ...
    else if(cmp(field, "Range") == 0)
        ...
    else exit (-1);
    field = extract_new_header_field(); 
}

当解析器测试等价性时,隐式流将从一个字段传播到下一个字段,即使根本没有真正的依赖关系!最后,最后一个字段似乎依赖于整个头部。

Dowser通过移动顺序不固定的字段来确定哪些选项对分析组中的指令真正重要。参见图4,假设我使用A,B, C, D和E五个选项运行程序和我们的分析组实际上取决于B和D .一旦消息被处理,我们看到AG并不依赖于E, E可以排除在进一步分析。由于最后观察到的颜色,D,对AG有直接的影响,它是一个真正的依赖关系。通过对D、A、B、C、E的顺序进行循环移位,Dowser只找到与A、B、D对应的颜色。在下一个循环移动之后,Dowser将颜色减少为B和D。

优化是基于两个观察:(1)最后一个传播到AG的场对AG有直接影响,需要保持,(2)超过这个场的所有场保证对AG没有影响。通过执行循环移位,并在更新的输入上运行DTA, Dowser消除了不必要的依赖。

尽管这种优化需要对输入有一些最小的了解,但我们不需要完全理解输入语法,比如字段的内容或效果。识别顺序不固定的字段就足够了。幸运的是,这些信息对于许多应用程序都是可用的——特别是当供应商测试他们自己的代码时。

5 Exploring candidate instructions

一旦我们知道了程序输入的哪一部分影响分析组AG(accp),我们就对这一部分进行模糊处理,并试图以非法的方式推动程序使用指针p。更严格地说,我们将输入中有趣的组件视为符号,其余部分视为固定的(具体的),并象征性地执行与AG(accp)关联的循环。

然而,由于整个循环遍历的成本在原则上是指数级的,因此循环是符号执行[19]最困难的问题之一。因此,在分析循环时,我们尝试选择在上下文中最有希望的路径。具体来说,Dowser优先选择显示可能出现复杂指针算法的路径。正如我们在第6节中所示,我们的技术显著地优化了溢出的搜索。

Dowser的循环探测过程有两个主要阶段:学习和bug发现。在学习阶段,Dowser为循环中的每个分支分配一个权值,该权值近似于沿着这个方向的路径包含新指针引用的概率。权重基于在执行短符号输入期间观察到的指针值的变化的统计数据。

接下来,在bug查找阶段,Dowser使用第一步中确定的权重来过滤循环中不感兴趣的部分,并对重要的路径进行优先级排序。当与某个分支关联的权重为0时,Dowser甚至不会进一步探索它。在脆弱的nginx解析循环中(图1显示了其中的一段摘录),60个分支中只有19个得到非零的值,因此考虑执行。在这个阶段,符号输入代表一个真实的场景,因此它相对较长。因此,使用流行的符号执行工具进行分析将非常昂贵。

在第5.1节中,我们简要回顾了动态符号执行的一般概念,然后分别在第5.2节和5.3节中讨论了这两个阶段。

5.1 Baseline: concrete + symbolic execution

像DART和SAGE[17,18]一样,Dowser通过结合具体值和符号执行来生成新的测试输入。这种技术称为动态符号执行[33]。它在具体的输入上运行程序,同时从沿途遇到的条件语句中收集符号约束。为了测试可选路径,它系统地否定收集的约束,并检查新集合是否可满足。如果是,则生成一个新的输入。为了引导该过程,Dowser接受一个测试输入,该测试输入执行分析组AG(accp)。

如前所述,应用这种方法的一个挑战是如何首先选择要探索的路径。经典的解决方案是通过回溯[22]来深度优先探索路径。然而,由于这样做会导致需要测试的路径呈指数级增长,研究社区已经提出了各种启发式方法来将执行引导到未开发的区域。我们将在第7节中讨论这些技术。

5.2 Phase 1: learning

学习阶段的目的是对依赖于循环l中的符号输入的所有条件分支的真方向和假方向进行评级。,我们不期望在其他结果中发现的偏差)。因此,我们回答了这样一个问题:当我们沿着这条路走下去时,我们期望获得多少收益,而不是另一种选择。我们把这些信息编码成权重。

具体来说,权重表示唯一访问模式的可能性。指针p的访问模式是循环执行期间p的所有解引用值的序列。在图1中,当我们用u0表示u的初值时,输入"//../"触发指针u的以下访问模式:(u0, u0+1, u0+2,u0-2,…)

为了计算权重,我们学习了单个分支的影响。原则上,它们中的每一个都可能(a)直接影响指针的值,(b)是另一个重要分支的先决条件,或者(c)与计算无关。为了区分这些情况,Dowser分析了短符号输入的所有可能执行。通过比较一个分支的两个结果观察到的p的访问模式集,它发现哪些分支不影响指针引用的多样性(即。,无关)。

在第4节中,我们确定了我们需要将测试输入I的哪一部分进行符号化。我们用IS表示它。在学习阶段,Dowser执行循环L。出于性能原因,我们进一步限制符号数据的数量,只生成一小段IS符号。例如,对于图1,学习阶段只生成uri符号的前4个字节(不足以触发bug),而在bug查找阶段扩展到50个符号字节。

算法探测器对一个简短的符号输入执行L,并记录在条件分支语句中所做的决策如何影响指针引用指令。对于执行路径上的每个分支b,我们保留在此执行期间实现的p的访问模式AP(p)。我们将其非正式地解释为“如果您选择分支b的true(分别为false)方向,则期望访问模式AP(p)(分别为AP ' (p))”。这个过程为每个分支语句分别生成两组访问模式,分别为已取分支和未取分支。每个方向的最终权重是该方向唯一的访问模式的百分比,即,而另一组则没有观察到。

上面的描述解释了学习机制背后的直觉,但是完整的算法更加复杂。问题是,条件分支b可能在执行路径中被执行多次,并且所有b的实例都可能影响观察到的访问模式。

直观地说,为了考虑到这一点,我们不将访问模式与b上的单个决策关联起来(true或false)。相反,每次执行b时,我们还保留之前为b选择的方向。因此,如果遵循b的真(分别为假)方向,我们仍然收集“期望”访问模式,但是我们使用一个前提条件来增强它们。通过这种方式,当我们比较真集和假集来确定b的权重时,我们将基于对访问模式是如何实现的更深入的理解来获得分数。

Discussion

对于我们的算法来说,避免错误否定是很重要的:我们不应该错误地将一个分支标记为不相关的——这将阻止它在错误发现阶段被探索。假设instr是一条取消引用指针p的指令。要了解分支直接影响instr,执行它就足够了。类似地,由于分支保留p的完全访问模式,所以有关正在执行的instr的信息也会“传播”到它的所有先决条件。因此,为了完全避免假阴性,该算法需要对分析组中的指令进行完全覆盖。我们强调需要执行所有指令,而不是循环中的所有路径。正如[7]所观察到的,即使是很短的符号输入的穷举执行在实践中也提供了很好的指令覆盖。

虽然假阳性也是不受欢迎的,但它们只会导致Dowser在第二阶段执行比绝对必要的更多的路径。由于路径覆盖范围有限,在某些情况下会出现误报。即便如此,在nginx中,60个分支中只有19个得到非零值,这使得我们可以使用50字节长的符号输入执行复杂循环。

5.3 Phase 2: hunting bugs

在这个步骤中,Dowser执行一个实际大小的符号输入,希望找到一个触发bug的值。Dowser使用来自学习阶段的反馈(第5.2节)将其符号执行引导到新的、有趣的指针解引用。我们启发式的目标是避免不带来任何新的指针操作指令的执行路径。因此,Dowser将符号执行的目标从传统的代码覆盖转移到指针值覆盖。

Dowser的策略是由权重决定的。作为基线,执行遵循深度优先探测,当Dowser根据符号输入选择分支b的方向时,它遵循以下规则:

  • 如果b的真方向和假方向的权值都为0,我们不认为b会影响访问模式的多样性。因此,Dowser随机选择方向,而不打算检查其他方向。
  • 如果只有一个方向的权值为非零,那么我们期望只有在执行路径遵循这个方向时才能观察到唯一的访问模式,而Dowser倾向于这个方向。
  • 如果b的两个方向的权值都是非零的,true和false选项都可能带来唯一的访问模式。Dowser检查了两个方向,并按照它们的权值大小安排它们。

直观地说,Dowser的符号执行尝试选择更有可能导致溢出的路径。

Guided fuzzing 

这就结束了我们对Dowser架构的描述。总之,Dowser通过以下方法帮助模糊处理:(1)发现“有趣的”数组访问,(2)确定影响访问的输入,(3)智能模糊处理以覆盖数组。此外,基于指针值覆盖率和少量符号输入值的目标选择过程允许Dowser快速发现bug并扩展到更大的应用程序。此外,数组访问的排序允许我们放大更复杂的数组访问。

6 Evaluation

在本节中,我们首先放大图1中运行的nginx示例,以详细评估系统的各个组件(第6.1节)。在第6.2节中,我们考虑了七个实际应用程序。基于它们的脆弱性,我们评估了我们的探测机制。最后,我们概述了Dowser检测到的攻击。由于Dowser使用“抽查”而不是“代码覆盖”方法来检测bug,所以它必须单独分析每个复杂分析组,从排名最高的组开始,然后是第二组,以此类推。它们都在运行,直到找到bug或终止。问题是什么时候应该终止符号执行运行。由于在Dowser中对单个循环的符号执行进行了高度优化,所以我们在不到11分钟的时间内发现了每个bug,所以我们执行每个符号运行最多15分钟。

我们的测试平台是一个Linux 3.1系统,使用Intel(R) Core(TM) i7 CPU, CPU频率为2.7GHz,缓存为4096KB。这个系统有8GB的内存。在我们的实验中,我们使用了OpenSUSE 12.1安装。我们运行每个测试多次并给出中值。

7 相关工作

Dowser是一种“引导性”模糊器,它从多个领域汲取知识。在本节中,我们将系统置于现有方法的上下文中。我们从评分函数和代码片段的选择开始。接下来,我们讨论传统的模糊。在此基础上,回顾了模糊处理中动态污染分析的研究现状,最后讨论了白盒模糊处理和符号执行方面的研究现状。

软件复杂性度量

许多研究表明,软件复杂度度量与缺陷密度或安全漏洞呈正相关[29,35,16,44,35,32]。然而,Nagappan等人的[29]认为没有一组度量标准适合所有项目,而Zimmermann等人的[44]强调需要利用漏洞的独特特征的度量标准,例如缓冲区溢出或整数溢出。所有这些方法考虑发布后的广义类缺陷或安全漏洞,并考虑一组通用的测量,例如,基本块的数量在一个函数的控制流图,读取或写入全局或局部变量的数量,if或while语句的最大嵌套级等等。Dowser在这方面很不一样,据我们所知,他是第一个这样的人。我们只关注一小部分安全漏洞,即,缓冲区溢出,因此我们的评分函数是定制的,以反映指针操作指令的复杂性。

传统的fuzz

软件模糊测试开始于90年代,Miller等人[25]描述了他们如何向(UNIX)实用程序提供随机输入,并设法使25-33%的目标程序崩溃。沿着相同路线的更高级的模糊器,如Spike[39]和snze[5],会故意生成格式错误的输入,而针对更深层错误的后期模糊器通常基于输入语法(如Kaksonen[20]和[40])。DeMott[13]提供了一个关于fuzz测试工具的调查。正如Godefroid等人所观察到的[18],传统的模糊器是有用的,但通常只能找到浅层的bug。

DTA在模糊测试中的应用

BuzzFuzz[15]使用DTA来定位影响库调用中使用的值的种子输入文件的区域。他们特别选择库调用,因为它们通常由不同的人开发,而不是调用程序的作者,而且常常缺乏对API的完美描述。Buzzfuzz根本不使用符号执行,而是使用DTA来确保它们保持正确的输入格式。与Dowser不同的是,它完全忽略了隐式流,所以它永远找不到nginx中的bug(图1)。很难评估哪些库调用是重要的,并且需要更仔细的检查,而Dowser则显式地选择复杂的代码片段。

TaintScope[42]与此类似,它还使用DTA选择影响安全敏感点(例如,系统/库调用)的输入种子字段。此外,TaintScope能够识别和绕过校验和检查。与Buzzfuzz一样,它与Dowser的不同之处在于它忽略了隐式流,只假设库调用是有趣的点。与BuzzFuzz不同,TaintScope在二进制级别而不是源代码上运行。

Symbolic-execution-based fuzz

近年来,人们对白盒模糊、静态符号执行、动态符号执行和约束求解等方面的研究引起了极大的兴趣。例如EXE[8]、KLEE[7]、CUTE[33]、DART[17]、SAGE[18]以及Moser等人的作品[28]。例如,微软的SAGE从格式良好的输入开始,象征性地执行测试中的程序,试图遍历程序的所有可行执行路径。在此过程中,它使用AppVerifier检查安全属性。所有这些系统都用符号值替换(一些)程序输入,在程序跟踪上收集输入约束,并生成新的输入,以执行程序中的不同路径。它们非常强大,可以详细地分析程序,但是很难使它们具有一定规模(特别是如果您想研究许多基于循环的数组访问)。问题是路径的数量增长非常快。

Zesti[24]采用了一种不同的方法,并符号执行现有的回归测试。直观地说,它检查它们是否可以通过稍微修改测试输入来触发脆弱的条件。这种技术可以更好地扩展,并且对于在现有测试套件附近的路径中发现bug非常有用。它不适合远离这些路径的bug。例如,执行图1中脆弱循环的通用输入的uri形式为“//{任意字符}”,触发bug的最短输入是“//../”。当使用“//abc”时,[24]找不到bug——因为它不是为这个场景设计的。相反,它需要一个更接近漏洞条件的输入,例如,“//..”{一个任意字符}”。对于Dowser,一般的输入就足够了。

SmartFuzz[27]关注整数bug。它使用符号执行来构造测试用例,这些测试用例触发算术溢出、非保值宽度转换或危险的有符号/无符号转换。相反,Dowser的目标是更常见(也更难找到)的缓冲区溢出情况。最后,Babi ' c等人的[4]将符号执行引导到静态分析检测到的潜在易受攻击的程序点。然而,所提出的过程间上下文和流敏感静态分析方法不能很好地应用于实际程序,实验结果只包含了很短的跟踪。

8 结论

Dowser是一种有指导意义的模糊器,它结合了静态分析、动态污染分析和符号执行来发现程序逻辑深处的缓冲区溢出漏洞。它首先确定“有趣的”数组访问,即。,最可能包含缓冲区溢出的访问。它按照复杂性对这些访问进行了排序——如果需要,允许安全专家关注复杂的bug。接下来,它使用污点分析来确定哪些输入影响这些数组访问,并且只模糊这些字节。具体地说,它在随后的符号执行中(仅)使这些字节具有符号性。在可能的情况下,Dowser的符号执行引擎选择最有可能导致溢出的路径。每个三个步骤包含论文本身的贡献(如数组访问的排名,隐式流处理污染分析,和象征性执行基于指针的值覆盖),但整体的贡献是一个新的、实用和完整的模糊方法,扩展到真实的应用程序和复杂的缺陷,很难或不可能找到与现有技术。此外,Dowser提出了一种新的“抽查”方法来发现实际软件中的缓冲区溢出。

猜你喜欢

转载自blog.csdn.net/zhang14916/article/details/89510661
今日推荐