20199127 2019-2020 《网络攻防实践》综合实践:Fuzzing漏洞挖掘论文研究与复现
综合实践:Fuzzing漏洞挖掘论文研究与复现
论文题目:
Driller: Augmenting Fuzzing Through Selective Symbolic Execution
所属会议:ndss2016
作者:
Nick Stephens, John Grosen, Christopher Salls, Audrey Dutcher, Ruoyu Wang, Jacopo Corbetta, Yan Shoshitaishvili, Christopher Kruegel, Giovanni Vigna
UC Santa Barbara
1研究内容
论文其实是在一个很高的起点开始写的,换句话说,论文的作者默认读者对模糊测试和符号执行都有所了解,并没有过多阐述原理,可以说比较有难度,所以粗看了一遍论文之后实际上论文研究的内容的基础知识是通过其他资料学习的。
1.1 模糊测试(fuzzing)
①概念
模糊测试(Fuzzing)技术是一种基于缺陷注入的自动化测试技术,没有具体的执行规则,旨在预测软件中可能存在的错误以及什么样的输入能够触发错误。其通过模糊器向目标应用发送大量的畸形数据并监视程序运行异常以发现软件故障,通过记录触发异常的输入数据来进一步定位异常位置。
与基于源代码的白盒测试相比,模糊测试的测试对象是二进制目标文件,因而具有更好的适用性:模糊测试是一种自动化的动态漏洞挖掘技术,不存在误报,也不需要人工进行大量的逆向分析工作。
用自己的理解来解释fuzzing的话,就是最初的漏洞或者bug是人工无意或尝试发现的,但因为人工无法想到所有可能的异常情况,也无法穷举所有的输入来进行测试。所以需要一种非常随机,比较全面而又不确定的/模糊的工具代替人们进行这个工作。
②工作原理
简单来说就是通过向被测试目标输入大量的畸形数据并检测异常来发现漏洞。完整的fuzzing经过以下几个基本阶段。
(1)识别目标
在没有确定测试对象测试范围的情况下,无法对模糊测试工具或技术做出选择。通常我们需要考虑以下问题:被测目标类型,如被测目标是客户端程序还是服务端程序,是应用层协议还是网络层协议:被测目标历史上是否出现过漏洞,漏洞原因在哪里等。
(2) 识别输入
几乎所有可被人利用的漏洞都是因为应用程序接受了用户的输入并且在处理输入数据时没有首先清除非法数据或执行确认流程。枚举输入向量对模糊测试的成功至关重要。未能定位可能的输入源或预期的输入值对模糊测试将产生严重的限制。任何从客户端发往目标应用程序的输入都应该被认为是输入向量。这些输入包括消息头、文件名、环境变量、注册键值等等。所有这些都应该被认为是输入向量,因此都应该是可能的模糊测试变量。
(3)生成模糊测试数据
一旦识别出输入向量,就可以生成模糊测试数据。可依据测试对象的特征,制定相应的模糊测试数据生成策略。例如可通过变异己有的数据动态生成数据。不管选择什么策略,生成模糊测试数据过程中都应该引入自动化。
(4) 执行模糊测试数据
执行过程可能包括发送数据包给自标应用程序、打开一个文件或发起一个目标进程。同样,这个过程中的自动化也是至关重要的。没有自动化,便无法执行真正的模糊测试。
(5) 监视异常
在模糊测试过程中,对故障或异常的监视过程有重要意义。例如,如果我们没有办法准确指出是哪一个数据包引起崩溃,那么向目标Web服务器发送10000个模糊测试数据包,最终导致服务器崩溃便失去意义。监视可以采用多种形式,同时不应该依赖目标应用程序和所选择的模糊测试类型。
(6) 确定可利用性
一旦确定被测目标存在故障,则需要确定所发现的Bug是否可重现,重现故障最常用的手段就是重放检测,即调用数据包重放工具将转储的网络数据包进行重放。重现成功后,还需进一步判断该Bug是否可被利用。这是一个典型的人工过程,需要具备安全领域的专业知识。
③特点
Fuzzing测试是一个无限空间的测试,所以硬件支持的话,理论上可以有无限个测试用例。Fuzz ing最主要的特点就是自动且简单高效,一不依赖程序的源代码发现问题,二不受限于被测系统的内部复杂程度。
Fuzzing会对输入进行变异,这样就能够快速地发现和处理一般输入,并产生不同路径。
④局限性
Fuzz ing虽然简单高效,但局限性也是明显的。因为模糊器随机对输入进行变异,所以发现和处理某路径上的某个值导致的漏洞比较可行,但是模糊器对于比较深层或者生成通过程序中复杂检查的特定输入是作用不大的,换句话说就是识别单个漏洞是比较快的,但是对于多个小漏洞链和深层漏洞构成的合成漏洞作用不大。
1.2 混合符号执行(Concolic Execution)
①概念
混合符号执行 (Concolic Execution)是一种程序分析技术,通过分析程序来得到让特定代码区域执行的输入。换言之,在使用符号执行分析一个程序时,该程序会使用符号值(或“符号变量”)作为输入,而非一般执行程序时使用的具体值。在达到目标代码时,分析器可以得到相应的路径约束,然后通过约束求解器来得到可以触发目标代码的具体值。
实际上,混合符号执行是精确执行(concrete)和符号执行(symbolic)的混合交集。
用自己的理解来解释的话,“符号变量”是可以产生许多可能具体值的变量(例如数字 5可能表示为X)。而其他的例如程序中的硬编码常量,被建模为“具体值”。随着执行的进行,符号约束被添加到这些符号变量上。约束即是对符号值的限制语句(例如,X <100)。
符号执行主要目标是: 在给定的探索尽可能多的、不同的程序路径(program path)。对于每一条程序路径,(1) 生成一个具体输入的集合(主要能力);(2) 检查是否存在各种错误,包括断言违规、未捕获异常、安全漏洞和内存损坏。
②工作原理
首先是符号执行(symbolic),它的基本原理就是按照其关键思想铺展开的。简言之,就是一个程序执行的路径有多条,通常是true和false条件的序列,这些条件是在分支语句处产生的。在序列的ith位置如果值是true,那么意味着ith条件语句走的是then这个分支;反之如果是false就意味着程序执行走的是else分支。如果用一个形式的例子进行解释的话,其实就是一个执行树来遍历程序。
左边的代码中,testme()函数有3条执行路径,组成了右图中的执行树。直观上来看,我们只要给出三个输入就可以遍历这三个路径,即图中绿色的x和y取值。符号执行的目标就是能够生成这样的输入集合,在给定的时间内探索所有的路径。
符号执行会在全局维护两个变量。其一是符号状态e,它表示的是一个从变量到符号表达式的映射。其二是符号化路径约束PC,这是一个无量词的一阶公式,用来表示路径条件。在符号执行的开始,符号状态e会先初始化为一个空的映射,而符号化路径约束PC初始化为true。e和PC在符号执行的过程中会不断更新。在符号执行结束时,PC就会用约束求解器进行求解,以生成实际的输入值。这个实际的输入值如果用程序执行,就会走符号执行过程中探索的那条路径,即此时PC的公式所表示的路径。
以左图的例子来阐述这个过程。当符号执行开始时,符号状态e为空,符号路径约束PC为true。当我们遇到一个读语句,形式为var=sym_input(),即接收程序输入,符号执行就会在符号状态e中加入一个映射var->s,这里s就是一个新的未约束的符号值。左图中代码,main()函数的前两行会得到结果e={x->x0,y->y0},其中x0和y0是两个初始的未约束的符号化值。当我们遇到一个赋值语句,形式为v=e0,符号执行就会将符号状态e更新,加入一个v到e{e0}的映射,其中e{e0}就是在当前符号化状态计算e0得到的表达式。
当我们遇到条件语句if(e) S1 else S2,PC会有两个不同更新。首先是PC更新为PC∩e{e0},这就表示then分支;然后是建立一个路径约束PC’,初始化为PC∩~e{e0},这就表示else分支。如果PC是可满足的,给一些实际值,那么程序执行就会走then分支,此时的状态为:符号状态e和符号路径约束PC。反之如果PC’是可满足的,那么会建立另一个符号实例,其符号状态为e,符号路径约束为PC’,走else分支。如果PC和PC’都不能满足,那么执行就会在对应路径终止。
如果符号执行遇到了exit语句或者错误(指的是程序崩溃、违反断言等),符号执行的当前实例会终止,利用约束求解器对当前符号路径约束赋一个可满足的值,而可满足的赋值就构成了测试输入:如果程序执行这些实际输入值,就会在同样的路径结束。例如,在左图例子中,经过符号执行的计算会得到三个测试输入:{x=0, y=1}, {x=2, y=1}, {x=30, y=15}。
混合符号执行。Concolic执行维护一个实际状态和一个符号化状态:实际状态将所有变量映射到实际值,符号状态只映射那些有非实际值的变量。Concolic执行首先用一些给定的或者随机的输入来执行程序,收集执行过程中条件语句对输入的符号化约束,然后使用约束求解器去推理输入的变化,从而将下一次程序的执行导向另一条执行路径。
用上面的例子继续解释的话,就是Concolic执行会先产生一些随机输入,例如{x=22, y=7},然后同时实际地和符号化地执行程序。这个实际执行会走到第7行的else分支,符号化执行会在实际执行路径生成路径约束x0≠2y0. 然后concolic执行会将路径约束的连接词取反,求解x0=2y0得到一个测试输入{x=2,y=1},这个新输入就会让执行走向一条不同的路径。之后,concolic执行会在这个新的测试输入上再同时进行实际的和符号化的执行,执行会取与此前路径不同的分支,即第7行的then分支和第8行的else分支,这时产生的约束就是(x0=2y0)(x0≤y0+10),生成新的测试输入让程序执行没有被执行过的路径。再探索新的路径,就需要将上述的条件取反,也就是(x0=2y0)(x0>y0+10),通过求解约束得到测试输入{x=30, y=15},程序会在这个输入上遇到ERROR语句。如此一来,我们就完成了所有3条路径的探索。实际上concolic执行使用的是深度优先的搜索策略。
③特点:
如果说模糊器像是广度优先地搜索漏洞的话,那么混合符号执行的优势就体现在其深度优先的搜索策略,同样基于理想软硬件情况下,混合符号执行能够更好地探索路径的完整性,如果漏洞存在的路径较少,而漏洞触发只发生在特定值的情况下,concolic执行在理论上应该能够更快找到漏洞。
④局限性
Concolic的缺点也是明显的。首先(1)符号执行速度缓慢,因为需要解释被测试程序的代码(因为fuzz ing不用,所以是他简单高效的重要原因),以及解约束的过程,都涉及比较大的开销,解约束甚至涉及NP问题,因此耗时严重。第二(2),受路径爆炸困扰,对于大的程序代码,路径的数量庞大呈指数增长,所以符号执行其实很快就会探索不下去,这个问题单靠concolic几乎是没法解决的。
二 论文提出的新概念和新系统归纳
新概念
模糊测试fuzzing和混合符号执行concolic都是较为成熟的方法,论文的目标是将两者的优势结合,并规避他们各自的局限性。作者的主要思路是,整个系统旨在用混合符号执行弥补模糊测试的根本弱点,在模糊测试探索不下去的时候(原文为“被‘卡住’”的状态)跳转到符号执行,由符号执行来进行下一步的挖掘。为了更好的说明问题,引入了一些新的概念。
1.一般输入和特定输入。一般输入表示可以采用大范围有效值的输入,特定输入表示必须采用几个可能值之一的输入。
2.浅层漏洞和深层漏洞。浅层漏洞是程序执行前期,通过一般输入可到达的漏洞。深层漏洞是经过多次条件转移,需要特定输入才能到达的漏洞。
3.隔区。漏洞检测系统对特定输入的检查之后,将被测程序拆分成各个隔区。执行流在隔区之间通过针对特定输入的检查来进行移动,而在隔区内,用模糊测试处理一般输入。
4.复杂检测。那些具体的,通过模糊器对输入进行变异不能满足的检查。
5.基本块。作者在论文中,将if代码分隔开的语句称为基本块,但一个语句并不意味着一个基本块,有的情况下,相同的语句被认为是不同的基本块。
**新系统 ** driller
整个系统分为以下组件:
①测试用例生成模块。系统可以在没有测试用例输入的情况下运行。然而输入测试用例可以通过引导模糊器指向某些隔区来加快初始模糊测试步骤。
②模糊测试模块。当系统执行时,它通过启动模糊引擎开始。模糊引擎探索应用程序的第一个隔区,直到它达到第一个复杂检查。在此,模糊引擎被“卡住”并且不能识别能够在程序中搜索新路径的输入。
③符号执行模块。当模糊引擎卡住时,系统调用它的符号执行组件。该组件分析应用程序,采用先前的模糊测试步骤发现的唯一输入,来约束用户输入以防止路径爆炸。接管模糊器产生的输入之后,符号执行组件利用其约束求解引擎来识别输入,以强制执行先前未被探索的后续路径。如果模糊引擎在卡住之前遍历了之前的隔区,则这些路径代表了到新隔区的执行流。
④任务管理模块。一旦符号执行组件识别到新的输入,则执行权被传递回模糊执行组件,其继续在这些输入上突变以探索新的隔区。系统继续在模糊测试和符号执行之间循环,直到发现使应用程序崩溃的输入。
各模块之间数据流转移如下图
实例
如下代码中,应用程序解析通过输入流接收到配置文件,该文件中包含特定魔数。如果接收到的数据包含语法错误或不正确的数字,程序退出。否则控制流基于各隔区之间的检查切换,其中一些包含内存崩溃缺陷。
系统通过调用其模糊引擎在程序的第一个隔区开始操作。这些模糊节点在Fig.1. 的程序的控制流程图中以阴影示出。该模糊步骤探索第一隔区并且停留在第一个复杂检查,即与特定魔数进行比较。然后,系统执行符号执行引擎,以识别能够通过该检查的输入,进入其他隔区。对于该示例,由Fig.2. 中示出了由符号执行组件发现的额外转换。
随后,系统再次进入其模糊阶段,覆盖第二个隔间(初始化代码和对配置文件中的键的检查)。 第二模糊阶段的覆盖如Fig.3. 所示。如图所示,除了默认值之外,模糊器找不到任何关键值。当第二次模糊调用被阻塞时,系统利用其符号执行引擎来发现“crashstring”和“set_option”输入,如Fig.4. 所示。前者直接导致二进制中的错误。
虽然符号执行和模糊测试单独都不能发现这个bug,但结合后的新系统可以。在这个例子中有几个区域需要系统的混合方法。解析配置和初始化代码具有大量的复杂控制流推理,这将导致路径爆炸,从而将符号执行减慢到毫无作用。此外如前所述,通过魔数检查要求高度特定的输入,很难在有限时间内在其搜索空间中发现,这导致传统的模糊测试方法不可能成功,阻碍模糊测试的其他常见技术还包括使用散列函数来做输入验证等。因此,符号执行和模糊测试结合具有获得更好结果的能力。
driller的特点
整个系统通过将模糊测试的速度与符号执行的输入推理能力结合起来工作。不仅能够快速探索二进制文件,不对用户输入施加复杂的要求,同时还能够处理纯粹的符号执行中存在的可扩展性问题,以及模糊测试对特定输入的复杂检查问题。
三 论文工作复现
① 模糊测试
这里系统用的是开源的模糊器AFL(American Fuzzy Lop),没有改变AFL原有的逻辑。
虽然AFL的逻辑没有改变,但是要与混合符号执行结合用的话,需要一个跳转步骤。当模糊组件经过预定值(与输入长度成正比)而没有识别新的状态转换时,我们认为它“卡住”。然后,系统检索模糊器在当前分区中认为“感兴趣”的输入,并在它们上调用符号执行引擎。
其中如果输入满足以下两个条件之一,则模糊器将其识别为感兴趣: 1)使应用程序采取的路径,触发了状态转换。2)使应用程序采取的路径,放置到了唯一的“循环桶”。漏洞发现系统中,AFL执行的进度可以在fuzzer_stats文件中找到的“pending_favs”属性确定,其表示等待执行模糊测试的感兴趣路径。当该属性为0时,调用符号执行。
这里我比较惊讶地一点是,确定模糊测试掉入“卡住”状态的判断居然是AFL中自带的某个属性值,而不是作者改动的。这说明AFL这个模糊器工具的开发者自己也清楚模糊器会由被“卡住”的情况,已经预想到可供研究的地方。所以只要在调用符号执行前加入一句判断这个属性值是否为0,就能完成两者间的跳转了。
安装AFL
安装中有许多波折,具体问题很多就不分析了。
装好可以打开单独测试。这里是按照教程测试的binutils。
② 混合符号执行
系统使用开源地符号执行引擎angr,但是作者对其进行了改进,因为他的主要功能是辅助模糊测试探索一些特殊输入,所以事实上大部分工作是fuzzer做的,符号执行模块在查找新路径时会调用angr,找到了通过检查的输入,就会返回下一步路径集合,没找到就返回空集合。
约束生成
在系统执行序列遇到条件跳转时,根据临时寄存器EFLAGS中存储的临时变量值生成特定的约束条件。如果当前指令是条件跳转指令,系统根据EFLAGES寄存器中存储值得变化确定最终约束。例如EFLAGS寄存器ZF为1,表示条件跳转结果为0,然后生成特定约束条件时对约束条件取反。相反如果ZF位为0,则生成相反的约束条件。如果遇到其他条件的指令,根据以上描述,同样根据零食寄存器 EFLAGS 中符号位的值生成对于约束条件,并取反生成相反的约束条件。
混合符号执行进行了一些改进。
(1)执行前加入了预约束控制。系统使用预约束控制以确保符号执行引擎的结果与模糊器结果相同,同时保持发现新的状态转换的能力。在预约束执行中,输入的每个字节被约束与模糊器输出的每个实际字节匹配,例如/dev/stdin[0] == 'A'。当发现新的可能的基本块转换时,暂时地去除预约束,允许系统求解偏离该状态转换的输入。
这一步比较关键,用一个例子进行说明。
当调用符号执行跟踪输入时,系统首先约束符号输入中的所有字节以匹配跟踪到的输入字节。由于程序是以符号方式执行的,因此每个分支只有一种可能性,因此只跟随一个路径,这防止了路径爆炸。然而,当执行到达行18时,系统识别出在模糊测试期间从未采取的备选状态转换。然后,系统删除在执行开始时添加的预约束。字符数组x 中的字节被路径部分约束,magic的值只受等式检查if(magic== 0x42d614f8)约束。因此,符号执行引擎创建一个包含25个 B 和一个魔数0x42d614f8的输入。
(2)引入了缓存探索器
往往一个状态转换后很快就有另一个状态转换,这将导致模糊器立即卡住,并转到符号执行,这将导致系统性能大大降低。引入了缓存探索器可以防止模糊器在接受符号执行生成的输入后被卡住。
(3)地址转换(符号执行返回至模糊器)
符号执行依靠模糊器传递来的输入,沿着模糊器探索到的地址进行探索。alf-fuzz 将会每分钟将当前内存执行状态写入到fuzz_bitmap文件中。依靠bitmap,每次继续探索之前会判断当前地址与丢失分支的最后一个跟踪地址(符号执行前一个跟踪到的地址)是否相遇。当地址相遇时,模糊器从这个地址继续探索。
安装angr
两部分都装上后,再安装Driller。
angr和driller拼接两部分组件安装遇到很多问题,主要原因都是python2退役导致的。还有一些不知道的问题,试着试着就自己解决了,可能是网速问题,比如下载到最后卡住之类的。
测试一下angr
层层波折安装完后,测试driller,在python环境里说明AFL和angr都装好了,如果有一项没安装好都无法运行。
③ 两者结合总结
系统采用celery分布式任务队列进行任务分发与管理,其中分发队列采用rabittmq消息队列,任务完成或中断时采用redis存储任务状态,恢复后可继续执行。系统包含两个任务队列:
(1)模糊器到混合符号执行:接收二进制文件路径作为参数,输入为默认初始化或从文件读入,并初始化模糊器,开启监听进程监听符号执行产生的新输入。循环检查模糊器,当没有发现崩溃也没有超时,检查fuzzer_stats文件中的pending_favs属性,为0时表示模糊器卡住,将任务交给符号执行处理。当发现崩溃,将崩溃 信息写入redis,并撤销仍在执行的符号执行任务。
(2)混合符号执行到模糊器:
读取模糊器输出目录中的fuzz_bitmap文件,并写入redis。对于 fuzzer输出目录中的每一个未被跟踪过的输入文件,读取redis中的模糊器bitmap,并开启符号执行引擎。
我按照自己的理解画了个图梳理了一下,调用符号执行前后,两个模块分别执行的内容大致是这样。
我的复现思路是按照作者论文行文的顺序进行的,被测试的程序都是CGC比赛上用的,还能看懂,但是driller用python写的代码确实看不太明白,论文中也没有进行什么解释,网上能找到一些driller的讲解,但是和作者给的源代码的方法却不一样。
论文涉及的内容远没我最初想象的容易。事实上,我能够复现的内容并不多,整个系统比较复杂,经过多天的研究,我复现的大概就只是AFL模糊器和angr符号执行引擎两者之间的相互跳转调用,但是没有更详细的说明要分析出整个流程还是有很大困难。
四 论文成果
论文的数据集来自CGC比赛(DARPA网络挑战,历时两年)提供的126个有漏洞的应用程序,但是论文作者没给,最初我还以为像这样级别的比赛的测试数据集应该会公开,在网上我也没找到。事实上,通过看论文了解,每一个应用程序的检测几乎都是几个小时以上才能够发现,所以我就照着分析一下论文中的说了。
①漏洞发掘量比较
在 126 个应用程序中,符号执行只发现了 16 个漏洞。
模糊测试能够发现 68 个的崩溃,在剩余的 58 个二进制文件中,41 个被“卡住”,有17个能够找到新的路径,却没有发现崩溃。
借助符号执行,在这41个被“卡住”的二进制文件中,AFL能够为其中13个找到101个新的输入。利用这些输入能够额外发现9次崩溃,所以系统能够发现的总崩溃数是77次,即整个系统在这个数据集上对fuzzer实现了12%的改进。
②状态转移覆盖
所谓状态转移实际上就是if函数的路径代码分支,当记录的状态发生转移时,系统就进行了下一条路径的识别。当发现一次状态转移时,就可知某个部分代码会在另一部分代码之后立即执行,或者或种说法很明显,如果我们发现每个状态转换,我们就有完整的代码覆盖。类似地,如果我们只发现很少状态转换,那么我们可能具有非常低的覆盖。因此,作者将发现这种代码转移的多少用来表示被测试程序的代码被检查了多少。
在上边提到的13个被“卡住”的应用程序中,符号执行的辅助让fuzzer从原先只能发现28.5%的状态转移提升到了能够额外找到56.5%的状态转移。
下图是对于13个被“卡住”的应用程序,系统随时间能偶找到的额外基本块的数量,表示的是随时间,有符号执行辅助的fuzzer能够探索到更深层的代码。
③程序隔区覆盖
系统中的符号轨迹的目标是使得模糊器能够探索二进制中的各个隔区,其可以通过对用户输入进行复杂检查来分离。期望看到通过调用符号跟踪器产生的输入符合在应用程序中找到新的隔间。也就是说,由符号执行引擎产生的输入应该使得模糊器能够到达并探索新的代码区域。
左图表示模糊测试不会使其崩溃的二进制程序中调用的符号执行的次数。即,调用符号执行越多,fuzzer越能检测出漏洞。右图是对于各个应用程序,fuzzer和concolic执行分别找出漏洞的比例,这个图的意义不在于比较fuzzer和concolic执行能找出漏洞的能力的比较(concolic执行只是辅助,实际上能力远不如fuzzer),而在于对于这些应用程序,只要调用了符号执行,都能再找出更多的路径和隔区。
五 创新点与未来研究方向
文章的创新在于:①将fuzzer和混合符号执行结合起来,利用了两者的优势,并规避了两者的缺点。实际上是在fuzzer的大体基础上利用混合符号执行对fuzzer进行辅助。②对符号执行组件做出了预约束控制、探索缓存等优化,在执行性能上有所提升。
经过对论文的研读和论文工作的复现,个人认为fuzzer和混合符号执行两者之间与广度和深度优先的搜索策略有相似之处,fuzzer更像是不在乎规模大小的深度优先算法策略,旨在期望在一个方向上有所突破;而符号执行则更像是广度优先算法,更在乎节点的分支而不是节点对应的具体值,期望先找到更多分支。所以从这一出发点来说,我能想到的是在算法策略上进一步改进整个driller系统。但是不论是深度或是广度搜索,在探索上仍然具有较大的盲目性。据自己了解,人工智能领域如今比较推崇一种叫做启发式的搜索策略,基本思路是在探索路径的控制信息中增加关于被解问题的某些特征,用于指导搜索向最有希望的方向前进。这种搜索策略一般只要知道问题的部分状态空间就能够求解问题,搜索效率较高。