张婉莹,曹晓梅,陈 伟
(南京邮电大学 计算机学院,南京 210023)
近年来,综合了模糊测试和Concolic执行的白盒模糊测试技术引起研究人员的广泛关注,其是一种漏洞检测技术,可以自动生成测试用例来尽量覆盖程序中的可执行路径,进而发现程序内的漏洞[1]。研究人员基于白盒模糊测试技术设计了许多漏洞检测工具,如KLEE[2]、SAGE[3]、Angr[4]和Driller[5]等。实验结果表明,这些工具能够有效提高漏洞检测的效率。
从理论上而言,白盒模糊测试可以实现对目标程序中所有可执行路径的覆盖,但在实际的漏洞检测过程中,无法达到100%的路径覆盖率,这主要是由于白盒模糊测试无法提供被测程序运行所需的实际环境以及约束求解器难以求解复杂约束等问题。目前,大量应用软件与环境密切相关,即在运行过程中需要与操作系统、网络以及设备之间进行频繁的交互,从而导致白盒模糊测试的漏洞检测结果不仅取决于用户输入,还依赖于其运行的环境[6]。如果在白盒模糊测试中不能提供某些路径执行所需要的环境,则与之相关的路径可能无法被探索到,从而导致某些漏洞被遗漏,降低了检测结果的准确性,这就是环境交互问题。例如,CH4[7]等著名病毒都是在指定的时间发作,即只有满足了漏洞触发的时间条件,才能检测到该漏洞,而这对于白盒模糊测试而言是一个难度较高的挑战。
针对环境交互问题,比较成熟的解决方案是斯坦福大学CADAR等人开发的KLEE和瑞士洛桑联邦理工学院CHIPOUNOV等人开发的S2E[8]。在漏洞检测之前,KLEE先对被测程序中调用的库函数进行建模,在对程序进行漏洞检测的过程中,如果遇到调用库函数的路径,则将其重定向到对应的模型,获得所有合法的输入值,从而驱动相关路径的执行[9]。由于建模需要耗费大量的人力和物力,因此KLEE只对库函数进行建模,当遇到其他类型的外部函数时,无法驱动路径的执行,因此,其路径覆盖率偏低。S2E是基于虚拟机插桩的漏洞检测工具,被检测的目标程序和整个运行环境都运行在S2E上。S2E采用2种漏洞检测模式,即SC-UE(Strictly Consistent Unit-level Execution)模式和SC-SE(Strictly Consistent System-level Execution)[10]模式。如果在漏洞检测的过程中采用SC-UE模式,则需要与外部环境进行交互的函数不会被插桩,与之相关的路径将无法被执行,导致路径覆盖率偏低,部分漏洞可能被遗漏。如果采用SC-SE模式,需要与外部环境进行交互的函数将会被插桩,并且可以根据真实的运行环境获得正确的测试用例,以驱动相关路径的执行,获得较高的路径覆盖率并检测到更多的漏洞,但是这会带来比较严重的路径爆炸问题。
近年来,国内外学者针对上述问题进行了较多研究。文献[11]提出了一种基于抽象内存的针对外部函数调用的建模方法,该方法对每个外部函数建立抽象内存模型,以保存其路径约束条件并模拟外部函数的语义信息,从而获取相应的测试用例,以保证待测程序能够按照目标路径执行,最终提高路径覆盖率。但是,该技术难以对需要精确理解上下文的外部函数调用进行建模。
本文提出一种基于外部函数探测和校正的隐藏路径搜索(Hidden Path Search Based on External Function,HPSBEF)方案。该方案利用约束求解和最弱前置条件推理获得外部函数的输出值,并将其记录在已经建立好的链表中,在执行新路径的过程中根据链表信息对外部函数的输出进行动态修正,以驱动新路径的执行并提高路径覆盖率。为验证HPSBEF方案的有效性和执行效率,将该方案与KLEE、S2E以及文献[11]提出的FMM方案作对比,从路径覆盖率、漏洞检测能力和时间开销3个方面进行分析。
HPSBEF方案在白盒模糊测试的基础上实现了外部函数探测与校正,其框架如图1所示,主要由外部函数预处理器、外部函数分析器和外部函数更新器三部分组成。
图1 HPSBEF方案框架
外部函数预处理器针对每个被检测的目标程序,创建与外部函数相关的链表结构,以记录目标程序中所有调用的外部函数及其参数信息,包括位置信息、输出参数类型和长度等。预处理器实现了外部函数的预处理,是检测外部函数的前提和基础。外部函数分析器检测函数调用指令,并与外部函数预处理器建立的外部函数链表进行匹配,若发现外部函数,则根据链表中记录的详细参数信息进行外部函数输出值的修正,以达到访问隐藏路径的目的。分析器实现外部函数的探测、标记与校正,是检测外部函数的关键阶段。外部函数更新器负责更新链表中的信息,即外部函数的参数在下次路径执行时是否需要修正以及修正为何值。白盒模糊测试在每次路径执行结束后,都会按照一定的规则对约束条件进行取反,并由约束求解器求解产生新的测试用例[12],外部函数更新器将新的测试用例中与外部函数相关的信息更新到已有的链表中,以作为下次路径执行时外部函数进行修正的参考。
1.2.1 外部函数预处理
外部函数预处理器的主要作用是外部函数及其参数信息的收集与整理,这是进行后续探测与修正的前提和基础。如果在预处理阶段有外部函数被遗漏,或者其参数信息的收集存在误差,则某些隐藏路径将不能被访问,从而导致某些漏洞被遗漏。
外部函数预处理器的执行主要包含2个步骤:针对每个被检测的目标程序建立外部函数链表结构,包含函数地址、函数名、输出参数个数、类型长度、校验标识以及校验值,如图2所示;找出目标程序中包含的所有外部函数及其输出属性的参数信息,并记录在链表中。
AddressFunction NameParameter NameParameter SizeFlagReplacement
图2 外部函数链表结构
Fig.2 Structure of external function chain
本文以Windows系统下的二进制程序为例对外部函数的预处理过程进行说明。Windows SDK中对于每个API函数参数的输入输出特性都有明确的定义,一般包括5种情况:[in],[out],[in,out],[in,optional],[out,optional]。对于可执行PE文件而言,所有调用的外部函数信息都存储在输入地址表(Import Address Table,IAT)中[13],多数Windows下的常见外部函数都由Kernel32.dll导出,每一个从dll中被引入的外部函数在PE文件的IAT中都有相应的位置。PE文件在装载过程中先搜索数据结构IMAGE_IMPORT_DESCRIPTOR中的OriginalFristThunk,若存在,则迭代搜索输入名称表(Import Name Table,INT)中的每个指针,这些指针指向了IMAGE_IMPORT_BY_NAME中的每个被载入函数的地址,然后装载器将值填充到FristThunk指向的IAT表中,整个过程如图3所示。因此,在PE文件被系统加载之后,提取外部函数及其在IAT表中的地址并且参照SDK中关于函数参数输入输出特性的定义,便可建立相应的外部函数链表结构。
图3 PE文件加载后的IAT表
1.2.2 外部函数的探测、标记与校正
执行外部函数分析器是检测外部函数的关键阶段,其主要作用是在漏洞检测的过程中根据外部函数的链表结构对路径中是否存在外部函数进行探测,若存在外部函数,则对其输出参数所在的内存区域进行标记,并根据外部函数链表中的校验标识以及校验值选择性地更改输出参数的具体数值,以驱动隐藏路径的执行。
首先,外部函数分析器探测路径中是否存在外部函数。Intel手册定义了x86平台下的3种调用指令[14],分别对应机器码E8、9A和FF。在程序执行过程中,外部函数分析器获取指令指针寄存器EIP中存储的下一指令的机器码,通过与E8、9A和FF 3种调用指令机器码进行比较,判断是否为函数调用指令。如果下一条指令为函数调用指令,则进一步获取调用函数中的地址,并通过与外部函数链表中记载的地址进行比较,判断是否为外部函数,如果是外部函数,则进行下一步,否则程序继续执行。
其次,在确定了外部函数之后,外部函数分析器即对其输出参数所在内存区域进行标记。由于Windows系统常采用cdecl和stdcall 2种调用约定,因此参数按照从右往左的顺序进行传递[15],所以最后一个入栈的参数就是函数的第一个参数。如果指令指针寄存器EIP指向的call指令调用的函数被判定为外部函数,则根据当前的指令地址可以获得该外部函数每个参数的入栈地址,然后根据外部函数链表中存储的该外部函数的输出参数序号和参数类型大小这2个参数,逆序推导出该函数中所有输出参数所在的内存区域。
最后,当外部函数输出参数被标记之后,还需要根据链表中存储的校验标识Flag判定该外部函数的输出是否需要被更改,如果需要更改,则将其更改为Replacement中存储的数据。如果该外部函数的输出参数是结构体变量,则输出参数值修正需要具体到结构体中的每一个成员变量。
本文外部函数探测、标记与校正的过程如算法1所示。语句1将指令指针寄存器EIP指向的指令机器码转换成汇编代码,语句2判断当前指令是否为函数调用call指令,如果不是call指令,调用语句13,将控制权交回目标程序,正常执行该指令;反之,则调用语句3查询外部函数链表,并调用语句4判定该函数是否为外部函数。如果链表中该外部函数存在,则调用语句5循环查找其输出参数,直到所有输出参数查询完毕,即IEF→PN=end时查询结束,其中,PN代表输出参数的名称。然后,算法利用语句6计算输出参数的内存区域,可由函数参数的入栈地址和参数大小相加得出,即APN+IEF→PS,其中,APN代表参数的入栈地址,PS代表参数的大小。算法在获得参数的内存区域后则利用语句7将语句6的计算结果进行标记;反之,则调用语句9,将控制权交回目标程序,执行函数调用。语句10根据链表中记载的该外部函数的校验标识,判断其输出值是否需要校正,如果需要,则调用语句11将输出参数值修改为链表中存储的数据,即ModifyOutput(Memory,IEF→Replacement)。
算法1外部函数探测、标记与校正算法
输入MEIP
输出校正后的外部函数
MEIP:machine code recorded in the register EIP
1.Iop←HextoAsm(MEIP)
2.if Iop∈Icallthen
3.IEF←Looktable(Iop,TEF);
4.if IEF≠∅ then
5.while (IEF→PN≠end) do
6.Memory←APN+IEF→PS;
7.MarkOutput(Memory);
8.else
9.Execue(Iop);
10.if IEF≠∅∩IEF→Flag==1 then
11.ModifyOutput(Memory,IEF→Replacement);
12.end if
13.Execue(Iop);
14.end if
由上述过程可知,该算法的时间复杂度由外部函数及其输出参数的数量决定,因此,若外部函数的数量为nEF,每个外部函数的输出参数数量最大为k,则该算法在最坏情况下的时间复杂度可表示为O(knEF)。算法的空间复杂度由存储外部函数的链表结构决定,可表示为O(6knEF)。
1.2.3 外部函数的更新
在白盒模糊测试中每条路径执行完成之后,会按照一定的路径搜索策略对约束条件取反,并由约束求解器翻译成最弱前置条件[16]进行求解,产生新的输入用例集合,以在下次漏洞检测时驱动新的路径执行。因此,外部函数更新器需要根据约束求解器的求解结果更新链表中的信息,以便在下次程序执行时作为外部函数校正的参考和依据。
由于实验部分是在KLEE、S2E的基础上实现本文方案,因此路径搜索策略采用KLEE、S2E提供的方法。KLEE和S2E均提供4种搜索方法:深度优先搜索,随机状态搜索,随机路径搜索以及NURS搜索,搜索方法的选择取决于参数的设定[17],本文采用深度优先搜索策略。
外部函数更新器需要将新生成的输入集合中与外部函数相关的信息更新到相应的链表项中,即该外部函数在下次执行时,输出参数值是否需要校正以及需要校正的具体数值为多少。
为验证HPSBEF方案的实际效果并降低路径搜索策略与约束求解能力等因素对实验结果的影响,本文分别在KLEE和S2E的基础上实现HPSBEF方案以及文献[11]提出的FMM方案,即在处理外部函数调用时,将KLEE、S2E原有的方案替换为HPSBEF以及FMM方案,其余都不变。其中,HPSBEF方案以H-KLEE和H-S2E作为标识,FMM方案以M-KLEE和M-S2E作为标识。
实验选用开源软件库[18]中包含较多外部函数的10个应用程序作为漏洞检测对象,程序的名称、路径数量和外部函数数量如表1所示。本次实验分别使用H-KLEE、M-KLEE、KLEE、H-S2E、M-S2E和S2E这6种工具对表2中的被测程序进行检测,并从路径覆盖率、漏洞发现能力以及时间开销3个方面进行对比分析。
表1 目标程序的路径数量与外部函数数量
Table 1 Number of target program paths and external functions
程序名称路径数量外部函数数量doublecmd882123metafile2eps70184processcecker1 125168pdftotext72790miniweb53568ipsacn974111kkplayer1 385104hardseed81572mpush1 09688vnote78693
实验分别使用6种工具对被测程序进行检测,统计各自的路径覆盖率,对比结果如图4所示。
图4 路径覆盖率结果对比
由图4可以看出,H-KLEE和M-KLEE、KLEE以及H-S2E和M-S2E、S2E相比,路径覆盖率均有所提升。其中,H-KLEE比M-KLEE平均增加了4.2%,H-KLEE比KLEE平均增加了8.1%,H-S2E比M-S2E平均增加了5.3%,H-S2E比S2E平均增加了5.9%,结果表明,本文方案可以探测到更多路径。此外,实验过程中的路径覆盖率没有达到白盒模糊测试在理论上的100%覆盖率,分析发现主要原因如下:约束求解器不支持浮点类型和指针运算[19],不能理解某些包含浮点类型或指针运算的复杂函数的运算规则,在实验过程中,当STP求解耗尽内存或者求解时间超过5 min时,将主动终止求解过程,这也是众多测试过程中部分路径无法访问的主要原因之一。
实验过程中对6种工具所检测出的漏洞数量进行统计,结果如表2、表3 所示。
表2 H-KLEE、M-KLEE和KLEE检测出的漏洞数量
Table 2 Number of vulnerabilities detected by H-KLEE,M-KLEE and KLEE
被测程序H-KLEEM-KLEEKLEEdoublecmd212019metafile2eps111010processcecker151413pdftotext191716miniweb131211ipsacn1098kkplayer121010hardseed987mpush1098vnote887
表3 H-S2E、M-S2E和S2E检测出的漏洞数量
Table 3 Number of vulnerabilities detected by H-S2E,M-S2E and S2E
被测程序H-S2EM-S2ES2Edoublecmd212020metafile2eps111010processcecker151314pdftotext191816miniweb131313ipsacn1099kkplayer121111hardseed987mpush1099vnote888
由表2可知,H-KLEE和M-KLEE、KLEE相比,检测出的漏洞数量有所增加,其中,H-KLEE比M-KLEE平均增加了9.4%,比KLEE平均增加了17.4%;由表3可知,H-S2E与M-S2E 、S2E相比,漏洞数量也有所增加,H-S2E比M-S2E平均增加了7.6%,比S2E平均增加了16.4%。因此,本文方案的漏洞检测能力较高。
实验过程中对6种工具的平均漏洞检测消耗时间进行统计,结果如图5所示。其中,H-KLEE的平均时间为45 028 s,M-KLEE的平均时间为45 343 s,KLEE的平均时间为45 811 s,H-KLEE所用时间最少;H-S2E的平均时间为45 078 s,M-S2E的平均时间为45 879 s,S2E的平均时间为46 526 s,H-S2E所用时间最少。因此,本文方案的时间开销明显降低。
图5 各方案平均消耗时间对比
Fig.5 Comparison of average consumption time of each scheme
本文针对白盒模糊测试中环境交互问题引起的路径覆盖率偏低、某些潜在漏洞可能被遗漏的问题,提出一种基于外部函数探测和校正的隐藏路径搜索方案HPSBEF。该方案阐述了外部函数探测和校正的实现过程,并针对虚拟机级别插桩和用户态级别插桩2种方式分别进行了验证。实验结果表明,HPSBEF方案能有效提升路径覆盖率和漏洞检测能力,同时降低时间开销。目前的白盒模糊测试技术经常遭遇如路径爆炸、复杂约束求解等问题,下一步将针对这些问题进行研究,以使该技术更好地应用于漏洞检测领域。