Driller分析与改进(一)

  • Author:ZERO-A-ONE
  • Date:2021-03-19

一、引子

​ 关注我的读者们应该对模糊测和符号执行两种技术已经十分熟悉了,那我觉得读者们一定会自然而然的联想到,是不是会存在一种可能性,将模糊测试和符号执行相结合起来,会产生一种充满了惊喜的产物

“I have a Angr, I have a AFL, ah, Angr AFL~~"

​ 哈哈,当然不叫Angr-AFL啦,而是叫做Driller。Driller是来自安全学术界的顶刊NDSS在2016年的文章《Driller: AugmentingFuzzing Through Selective Symbolic Execution》引入的,这个团队来自的大学大家应该很熟悉了,就是Angr来自的同一所大学——加州大学圣塔芭芭拉分校(yyds)。这个项目的产生也是源于美国国防部的比赛,DAPRA的Cyber Grand Challenge(CGC),国内阿里好像也有类似的比赛

​ 其实根据我之前的研究,国内很早也有人申请了专利,应该是无锡江南计算技术研究所(也是yyds)于2014年12月11日申请的专利《CN104375942B”一种面向二进制的混合模糊测试方法“》,江南所主要的思路大致都是采用模糊测试作为前端处理,其中利用测试用例来驱动被测程序的执行,采用二进制代码覆盖率监测处理作为中间层处理,其中在被测程序的执行过程中,记录被测试程序覆盖的基本块,由此计算模糊测试的覆盖率。当计算出的模糊测试的覆盖率不再提高时,执行作为后端处理的符号执行处理,在符号执行处理中生成覆盖与当前测试用例的路径不同的其它路径的新的测试用例,然后将新的测试用例反馈至模糊测试,使得模糊测试利用新的测试用例来驱动被测程序的执行

​ Driller的实现与上述的专利是几乎差不多的,Drilller是基于fuzz工具AFL和符号执行工具angr来实现的,当模糊程序卡住时调用符号执行来求解能够到达新路径的输入,使得fuzz能够快速突破条件判断语句。具体实现是,通过监测AFL的执行,我们可以决定什么时候开始符号执行以探索新路径。如果AFL执行了x轮后,bitmap上显示没有发现新的状态转换(也即新的代码块转移),说明AFL卡住了,这时候调用angr进行符号执行。每个具体输入对应于PathGroup中的单个路径, 在PathGroup的每一步中,检查每个分支以确保最新的跳转指令引入先前AFL未知的路径。 当发现这样的跳转时,SMT求解器被查询以创建一个输入来驱动执行到新的跳转。这个输入反馈给AFL,AFL在未来的模糊步骤中进行变异。 这个反馈循环使我们能够将昂贵的符号执行时间与廉价的模糊时间进行平衡,并且减轻了模糊对程序操作的低语义洞察力

二、理论阐述

2.1 Driller概述

这一部分主要来自Driller自己的论文部分,Driller通过结合fuzzing的速度和符号执行输入生成能力实现功能。

主要是做出了以下贡献:

  • 提出了一种新方法,通过利用符号执行技术来获得更深层的程序代码来提高模糊测试的有效性,同时通过使用模糊测试来缓解路径爆炸,从而提高了符号执行的可扩展性
  • 设计并实施了一个工具Driller来演示这种方法
  • 通过在同一数据集上识别与Cyber Grand Challenge资格赛的获胜团队相同数量的漏洞来证明Driller的有效性

Driller设计背后的一个核心思想就是,应用程序处理的用户输入其实主要就是两类:

  • general:代表普遍的通用的一种输入(也就是怎么输入都不怎会影响程序执行流程)
  • specific:代表必须采用少数几个可能的值之一(也就是只有少数几个输入才能改变程序执行流程)

从概念上讲,应用程序对后一种输入的检查将应用程序划分为多个部分。通过检查特定输入,执行流在各个基本块之间移动,而应用程序在基本块中处理常规输入

2.2 Driller组成部分

Driller是由多个部分组成的:

  • 输入测试用例:Driller可以在没有输入测试用例的情况下运行。但是如果有的话,这些测试案例可以加速fuzzing的初始化
  • 模糊测试器:启动Driller,首先启动一个fuzzer,fuzzer开始探索应用程序,当到达了一个具有复杂判断逻辑的部分,模糊测试可能会在这里陷入卡住的状态,无法再提高程序覆盖率
  • 混合执行:
    • 当模糊引擎卡住时,Driller会调用符号执行部分;符号执行分析应用,根据上一步Fuzzing得到的输入值来限制用户输入,从而避免路径爆炸;
    • 然后根据fuzzer得到的输入,符号执行优化constraint solver,识别执行未探索过的路径的输入。
  • 迭代器:
    • 一旦符号执行发现新的输入,立即传递回Fuzzing部分
    • Fuzzing部分根据这些输入产生变异,模糊执行新的ACom
    • Driller在Fuzzing和符号执行之间循环执行,直到发现导致Crash的输入

What is Application Compartment (ACom)? An ACom of an application is a subset of a ROBAC where only the users, permissions, roles, and organizations applicable to the application are included. Role and Organization Based Access Control (ROBAC)

2.3 例子

在这里插入图片描述

在此示例中,应用程序解析通过输入流接收到的包含幻数的配置文件。如果接收到的数据包含语法错误或错误的幻数,则程序退出。否则,控制流将基于多个基本块之间的输入进行切换,其中一些包含内存损坏漏洞
在这里插入图片描述

  • 图1:对应用程序的第一部分进行模糊测试,然后模糊测试引擎陷入了与幻数的比较中
  • 图2: Driller执行conconolic执行引擎,以符号执行引擎解析出检查通过的输入然后进入其他程序部分的输入
  • 图3:Driller再次进入其模糊测试阶段,对第二个基本块进行模糊测试。除默认值外,模糊器无法找到能到达其它分支的输入
  • 图4:当第二个模糊测试调用被卡住时,Driller会利用其conlicolic执行引擎来发现“ crashstring”和“ set_option”输入

2.4 模糊测试

模糊测试使用大量输入来执行应用程序,通过检查这些输入是否会导致应用程序崩溃

为了实现Driller,我们利用了一种流行的现成模糊器,即American Fuzzy Lop(AFL)。 我们的改进主要涉及将模糊器与Concolic Execution引擎集成。 没有改变AFL的逻辑;AFL依靠插桩来做出有关哪条路径感兴趣的明智决策。 该工具可以在编译时引入,也可以通过修改后的QEMU引入,Driller选择了QEMU后端来消除对源代码可用性的依赖

2.4.1 模糊器的功能

现代模糊器实现了许多功能,可以更好地识别crashing inputs。我们将列出并描述最重要的AFL功能,并提及Driller如何使用它们

  • 遗传模糊:AFL通过遗传算法进行输入生成,根据遗传学启发的规则(转录,插入等)改变输入并通过适应度函数对它们进行排序。对于AFL,适应度函数基于代码覆盖,即可以产生与其他输入触发不同的执行路径
  • 状态转换跟踪: AFL跟踪它从输入中看到的控制流转换的并集,作为源和目标基本块的元组。基于发现新的控制流转换,输入被优先用于遗传算法中的种子,这意味着导致应用以不同方式执行的输入在未来输入的生成中获得优先权
  • Loop bucketization:处理循环是模糊引擎和类似执行引擎的复杂问题。为了帮助减小循环的路径空间的大小,执行以下启发式操作。当AFL检测到路径包含循环的迭代时,将触发二次计算以确定该路径是否有资格进行breeding
    • AFL确定执行的循环迭代次数,并将其与导致路径经过同一循环的先前输入进行比较。这些路径都通过其循环迭代计数的对数(即,1,2,4,8等)放入“桶”中。在遗传算法中考虑来自每个桶的一条路径用于育种。这样,对于每个循环,只考虑log(N)路径,而不是N路径的方法
  • 去随机化:程序随机化干扰了遗传模糊器对输入的评估 - 在给定的随机种子下产生有趣路径的输入可能不会在另一个下产生。我们将AFL的QEMU后端预先设置为特定的随机种子,以确保一致执行。稍后,当发现崩溃输入时,我们使用我们的concolic execution引擎来恢复任何依赖于泄漏随机性的“质询 - 响应”行为或漏洞。例如,二进制文件中的“质询 - 响应”过程将随机数据回送给用户,并期望相同的数据回显给它。在不删除随机化的情况下,模糊测试组件每次都可能无法通过此检查并探索很少的路径。如果随机性是恒定的,程序每次都接受相同的输入,让模糊器(或者执行组件)自由地找到这一个值,然后进一步探索。发现崩溃后,可以用符号方式对随机性进行建模,如第V-D4节所述,并且可以相应地修补崩溃输入

这些功能允许AFL快速发现应用程序中的唯一路径,在应用程序的给定隔离区中执行路径发现工作的主要任务。然而,模糊测试的局限性是众所周知的

2.4.2 模糊器限制

过去的模糊测试器以AFL为例子,虽然尽管采用了遗传模糊,但仍然是在二进制层级进行变异而去生成新的测试样例,这样子的测试样例能够快速的发现和处理一般(General)输入中的不同路径,但在面对具有复杂检查的特定(specific)输入的情况下,则效果就不太理想

int main(void)
{
    int x;
    read(0, &x, sizeof(x));
    
    if (x == 0x0123ABCD)
        vulnerable();
}

上面的例子中应用程序从用户输入读取值然后将其与特定值进行比较,如果提供了正确的值,应用程序将崩溃。 但是,由于模糊测试的性质,想要刚好撞上这个特定值,概率很小难度很大。对于非插桩的模糊器(即,为输入选择随机值的模糊器),模糊器将发现该错误的可能性是无穷小。对于插桩的模糊器,将导致该二进制文件的控制流局限在一条大概率已经被发现的道路上。如果没有对新路径进行优先排序的能力,一个插桩的模糊器将被简化为在现有路径上应用随机变化,这实质上与非插桩案例相同,具有相同的极小机会成功

2.4.3 过度到混合执行

Driller旨在通过利用符号执行的优势,补充模糊测试的基本弱点,确定通过复杂检查所需的特定用户输入。当模糊测试的样例经历了预定量(与输入长度成比例)的突变而没有识别出新的状态转换时,我们就认为这个样例陷入了卡顿之中。然后,Driller检索模糊器在当前卡住的基本块中认为有趣的输入,调用concolic execution引擎

如果满足以下两个条件之一,则模糊器会将输入标识为有趣的:

  • 输入导致应用程序采用的路径是第一个触发某些状态转换的路径
  • 输入导致应用程序采用的路径是第一个放入唯一“Loop bucketization”的路径

2.5 选择性混合执行

当Driller确定模糊器无法找到其他状态转换时,会调用concolic execution引擎。 Driller使用concolic execution的见解如下:模糊器未能在程序中找到新状态转换的主要原因之一是模糊器无法生成特定输入以满足代码中的复杂检查。 concolic execution引擎用于利用符号求解器来改变现有的输入(这些输入路径可达但不能满足复杂的检查),以得到新输入(路径达到并满足此类检查)

当Driller调用concolic execution引擎时,它会传递由模糊测试引擎识别的所有“有趣”输入。 象征性地跟踪每个输入以识别模糊引擎不能满足的状态转换

当识别出这样的转换时,concolic execution引擎产生输入,该输入将通过该状态转换驱动执行。 在concolic execution引擎完成处理提供的输入之后,其结果被反馈到模糊测试引擎的队列中,并且控制被传递回模糊引擎,以便它可以快速浏览新发现的应用程序的分区

2.5.1 符号执行

我们利用angr,一个最近开源的符号执行引擎,用于Driller的concolic execution引擎。该引擎是基于MayhemS2E推广和改进的模型。首先,引擎将二进制代码转换为ValgrindVEX中间表示,该表示被解释为确定程序代码对符号状态的影响。此符号状态使用符号变量来表示可能来自用户的输入或非常量的其他数据,例如来自环境的数据。符号变量是一个变量(例如X),它可以产生许多可能的具体解决方案(例如数字5)。其他值(例如程序中硬编码的常量)被建模为具体值。随着执行的进行,符号约束被添加到这些变量中。约束是对符号值的潜在解决方案的限制性陈述(例如,X <100)。具体的解决方案是满足这些约束的X的任何值

分析引擎在整个执行过程中跟踪存储器中的所有具体和符号值以及寄存器(上述符号状态)。在引擎到达的程序中的任何点处,可以执行约束解析以确定满足对状态中的所有符号变量的约束的可能输入。当传递给应用程序的正常执行时,这样的输入将驱动应用程序到那一点。有条理执行的优点是它可以探索和查找约束求解器可以满足的任何路径的输入。这使得它可以用于识别复杂比较的解决方案(包括某些散列函数),模糊器不太可能暴力破解

Driller的符号记忆模型可以存储具体和符号值。它使用基于索引的内存模型,其中读取地址可能是符号,但写入地址始终具体化。这种由Mayhem推广的方法是保持分析可行的一个重要优化:如果读取和写入地址都是符号,则使用相同符号索引重复读取和写入将导致符号约束的二次增加,或者取决于符号执行引擎的实现细节,存储的符号表达式的复杂性。因此,符号写地址始终具体化为单个有效解决方案。在某些条件下,如本领域文献所提出的,符号值被具体化为单一的潜在解决方案

符号内存优化增加了concolic execution引擎的可伸缩性,但可能导致不完整的状态空间,可能的解决方案更少。 不幸的是,这是必须进行的权衡,以便对现实世界的二进制文件进行分析

2.5.2 例子

concolic execution比模糊测试擅长解决不同的问题。 回想一下下面这个示例,展示了模糊化的缺点。由于传递检查所需的输入的准确性来保护对vulnerable()的调用,因此模糊测试无法在合理的时间范围内探索该段代码

int main(void)
{
    int x;
    read(0, &x, sizeof(x));
    
    if (x == 0x0123ABCD)
        vulnerable();
}

但是,一个独特的执行引擎将能够轻松地满足此检查并触发易受攻击的功能。 对于这个例子,在这个例子中,concolic execution只需要探索少量路径来找到一个到达bug的路径,但对于更大的二进制文件和真实世界的例子,将会以相同的方式探索太多的路径

2.5.3 约束限制

传统的concolic execution方法包括从程序开始就concolic execution,并使用符号执行引擎探索路径状态,以找到尽可能多的bug。然而,这种方法存在两个主要限制

首先,concolic execution很慢。这是由于需要解释应用程序代码(与本机执行它相反,如使用模糊器)以及约束求解步骤中涉及的开销。具体而言,后一种操作涉及NP完全问题的解决方案,使得潜在输入的生成(以及确定哪些条件跳转是可行的)是耗时的

更糟糕的是,符号执行受到状态爆炸问题的困扰。随着符号执行引擎探索程序,路径的数量呈指数级增长,探索超过一小部分路径也很快变得不可行。考虑下表中的示例。在此程序中,当用户输入正好25个B字符时会触发vulnerability(),但这是在符号执行框架中难以表达的条件。当模拟CPU将递归调用降级为check()函数时,该程序的符号执行将导致巨大的状态爆炸。每个执行三元条件比较一个字符与文字B将每个模拟状态分成两个,最终导致 2 100 ) 2^{100}) 2100)个可能的状态,这是一个不可行的处理量

int check(char *x, int depth) {
    if (depth >= 100) {
        return 0;
    } else {
        int count =(*x == 'B') ? 1 : 0;
        count += check(x+1, depth+1);
        return count;
    }
}

int main(void)
{
    int x;
    read(0, &x, sizeof(x));
    
    if (check(x, 0) == 25)
        vulnerable();
}

另一方面,基于状态转换选择输入的遗传模糊器不会推断程序的整个状态空间,而只会推断由输入触发的状态转换。 也就是说,它将主要关注次数,例如,第5行的检查成功。 也就是说,无论输入中B字符的位置如何,都将根据输入中的数字来判断状态,避免路径爆炸问题。

虽然通过智能状态合并已经在减少这个问题方面取得了进展,但仍然存在一般性问题

2.5.4 Driller的执行

在大多数情况下,模糊测试可以单独充分探索大部分路径,只需通过随机位翻转和其他变异策略找到它们。通过利用本机执行,它可以在大多数可以随机触发路径的情况下胜过符号执行。因此,大部分工作都从concolic execution引擎转移到模糊器,这将快速找到许多路径,让常规引擎能够解决更难的约束

当模糊测试无法发现输入产生新执行路径时,将调用concolic execution引擎。它跟踪模糊测试发现的路径,识别分散到新程序组件中的输入,并执行有限的符号探索。另外,当模糊测试组件发现崩溃输入时,该concolic execution引擎“重新随机化”它以恢复依赖于随机性和其他环境因素的崩溃输入的部分

预约束追踪

Driller使用concolic execution来跟踪来自模糊器的有趣路径并生成新输入。这种方法有效性的一个关键因素是它允许Driller避免在符号探索中固有的路径爆炸,因为只分析了代表应用程序处理该输入的路径

当跟踪从模糊器传递到符号执行时,目标是发现模糊测试之前未找到的新过渡。 Driller的concolic execution引擎跟踪输入,遵循模糊器采用的相同路径。**当Driller发生条件控制流转移时,它会检查反转该条件是否会导致发现新的状态转换。**如果愿意,Driller会生成一个示例输入,通过新的状态转换而不是原始控制流来驱动执行

通过这样做,Driller的concolic execution引擎将模糊引擎引导到应用程序的新隔间。生成输入后,Driller继续跟踪匹配路径以查找其他新状态转换

输入预约束

Driller使用预约束来确保concolic execution引擎的结果与本机执行中的结果相同,同时保持发现新状态转换的能力。在预约束执行中,输入的每个字节被约束以匹配由模糊器输出的每个实际字节,例如/dev/stdin[0]=='A'。当发现新的可能的基本块转换时,简单地移除预约束,允许Driller求解将偏离到该状态转换的输入。预约束是必要的,以在符号执行引擎中生成相同的跟踪,并使有限的范围探索可行

int check(char *x, int depth) {
    if (depth >= 100) {
        return 0;
    } else {
        int count =(*x == 'B') ? 1 : 0;
        count += check(x+1, depth+1);
        return count;
    }
}

int main(void)
{
    char x[100];
    int magic;
    read(0, x, 100);
    read(0, &magic, 4);
    
    if (check(x, 0) == 25)
        if (magic == 0x42d614f8)
            vulnerable();
}

为了演示输入预约束如何在Driller中工作,我们使用上表中的示例,它类似于V-C中的示例,另外为了达到vulnerable(),我们必须在第18行提供幻数(0x42d614f8)。在对输入进行模糊测试之后,Driller最终意识到它没有发现任何新的状态转换,因为单独的模糊器无法猜出正确的值

当调用concolic execution来跟踪输入时,Driller首先约束符号输入中的所有字节以匹配跟踪输入的字节。由于程序是象征性执行的,每个分支只有一种可能性,因此只遵循一条路径。这可以防止第V-C节中描述的路径爆炸。然而,当执行到达第18行时,Driller认识到在模糊测试期间从未进行过替换状态转换。然后,Driller删除在执行开始时添加的预约束,不包括通过跟踪输入以符号方式执行程序而放置的谓词。字符数组x中的字节部分受路径约束,而magic的值受等式检查if(magic == 0x42d614f8)约束。因此,concolic execution引擎创建一个包含25个B实例和magic = 0x42d614f8的输入。这通过了第18行的检查并达到了vulnerable()

有限的符号探索

为了减少昂贵的符号引擎调用的数量,我们还引入了一个符号探索存根,以发现在新发现的状态转换后直接存在的更多状态转换。这个符号探索存根探索状态转换的周围区域,直到资源管理器遍历了可配置数量的基本块。一旦发现了这个数量的块,Driller就会为浏览器发现的所有路径提供输入。我们认为这样做可以防止模糊器在提供Driller生成的输入后快速“卡住”。在许多情况下,Driller生成一个新输入,只能通过多部分复杂检查中途,并且必须立即回溯以允许模糊器更深入地进入二进制文件。符号探索存根是一个小优化,它允许Driller在请求之前找到进一步的状态转换,而不必回溯其步骤

重新随机化

在程序运行期间引入的随机值可以破坏模糊测试,如前所述。下表显示了一个小程序,challenge反映用户随机输入。这使得模糊不稳定,因为在不监视程序输出的情况下我们永远无法知道challenge的具体值

int main(void) {
    int challenge;
    int response;
    
    challenge = random();
    
    write(1, &challenge, sizeof(challenge));
    read(0, &response, sizeof(response));
    if (challenge == response)
        abort();
}

一旦发现漏洞,我们使用符号执行来跟踪崩溃的输入并恢复需要满足目标二进制文件提出的动态检查的输入字节(例如上表中的challenge-response)。 通过在崩溃时检查符号状态并找到应用程序输出和崩溃输入之间的关系,Driller可以确定应用程序的challenge-response协议。 在这个例子中,我们可以看到由read调用引入的符号字节被约束为等于write写出的字节。 确定这些关系后,我们可以生成一个漏洞规范,用于处理在真实环境中发生的随机性

三、结论

在本文中,我们介绍了Driller,这是一个结合了动态模糊和符号执行的最佳工具,可以有效地发现隐藏在二进制文件中的bug。 我们介绍了二进制文件的概念,它在很大程度上将功能和代码分开。 在Driller中,模糊测试可以快速而便宜地查看隔间,有效地探索循环和简单检查,但通常无法在隔间之间进行过渡。 在考虑循环和内部检查时,选择性的符号执行会进入状态爆炸,但在查找二进制文件的分区之间的路径时非常有效。 通过将这两种技术相结合,每个技术都失败,Driller能够在二进制文件中探索更大的功能空间

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值