APP下载

基于应用视角的缓冲区溢出检测技术与工具*

2019-07-08司徒凌云王林章李宣东

软件学报 2019年6期
关键词:缓冲区度量漏洞

司徒凌云, 王林章, 李宣东, 刘 杨

1(南京大学 计算机科学与技术系,江苏 南京 210023)

2(计算机软件新技术国家重点实验室(南京大学),江苏 南京 210023)

3(School of Computer Science and Engineering, Nanyang Technological University, Singapore 210023, Singapore)

软件缓冲区溢出漏洞在CWE/SANS排名的25个最危险软件漏洞中位列第三[1],是当今危害最为广泛和严重的安全漏洞之一.缓冲区溢出是指输入数据超过缓冲区可容纳的最大数据量,进而超出部分溢出到临近存储区域的软件漏洞.其产生的根本原因是使用非安全类型编程语言如 C/C++,强调效率优先,而对内存操作不做边界检查.缓冲区溢出可被恶意利用进而控制主机,获得系统权限,执行任意代码,对安全攸关系统造成重大危害.典型的例子最早可追溯到1988年爆发的MORRIS蠕虫[2],其利用BSD操作系统后台程序的缓冲区溢出漏洞进行攻击,数天之内控制了近6 000台网络主机,几乎导致互联网完全瘫痪,造成了近一千万美元的经济损失;再例如2001年7月爆发的Code Red蠕虫[3]攻击了近36万台服务器,造成超过26亿美元的损失.速度最快的属随后于2003年1月爆发的Slammer蠕虫[4],其基于微软SQL Server的缓冲区溢出漏洞进行攻击,在10分钟之内感染了7.5万台主机,最终感染主机60多万台,造成经济损失达50亿美元之巨.

为了抵御缓冲区溢出漏洞的威胁,一方面,编程人员的安全编程技能不断提升;另一方面,各种缓冲区溢出漏洞的检测、防护技术也相继提出,典型的可分为静态方法和动态方法.静态方法[5-8]不需执行程序,基于源码分析,能够有效发现常见的缓冲区溢出漏洞.其优势在于速度快,可处理规模大,并且在一定假设前提下可以证明程序彻底摆脱某种特定类型的缓冲区溢出漏洞;其不足在于误报率和漏报率较高;相对而言,动态方法[5]可获得较高的精度,代价是巨大的额外开销和性能损失;进一步地,部分动、静态结合的技术也相继提出[9,10].

上述方法在一定程度上提高了缓冲区溢出攻击的门槛,缓解了缓冲区溢出漏洞造成的危害.但是面对当今信息社会软件规模不断扩大,软件数量不断增多(包括众多现有的 C/C++编写的系统以及以往的 C/C++遗留代码)的现实,缓冲区溢出漏洞的数目不减反增.图1所示为1989年~2017年,CVE公布的历年缓冲区溢出漏洞数目,由此可知,缓冲区溢出漏洞依旧是威胁软件安全的重大安全漏洞之一.

值得一提的是,缓冲区溢出不仅仅局限于非安全类型编程语言 C/C++,安全类型的编程语言代码 Java,Perl,其底层基础同样面临的缓冲区溢出攻击的威胁[9].

书写安全的代码可能是唯一能够使软件彻底摆脱各种安全漏洞的终极办法,软件工程领域的研究一直致力于帮助编程人员书写安全的代码.遗憾的是,依靠目前的编程范式以及各种缓冲区溢出漏洞的防护措施,要彻底地消除缓冲区溢出漏洞几乎是不可能的.因此,缓冲区溢出漏洞检测技术与工具显得至关重要,它们是检测与修复、预防与保护、度量与评估等多方面工作的基础与核心.

迄今为止,学术界和工业界提出了各种缓冲区溢出漏洞的检测技术与工具.然而面对众多的检测技术与工具,使用者如何有效地进行选择,进而应用到缓冲区溢出漏洞的检测与修复、预防与保护、度量与评估等多个方面,是一个具体而实际的问题.

有必要对现有的软件缓冲区溢出检测技术与工具进行梳理,然而目前对于缓冲区溢出漏洞检测技术与工具的梳理大多基于研究者的研究视角,而非使用者的应用视角.基于研究视角,其重点关注缓冲区溢出检测方法的技术细节,以静态方法、动态方法和混合方法进行分类阐述.这样的梳理对于研究者而言清晰明了,有利于研究者深入了解各种检测技术细节进而进行技术改进.但是对于使用者而言不够直观实用,因为使用者不关心技术与工具的进一步改进,其关心的更多的是如何在有限成本的制约下,有效地选择或者组合出能够最大限度满足自身需求的检测工具进行应用,并达到尽可能好的效果.

该命题的回答可以归结为用户需求的细分与相应检测技术工具的匹配,这需要同时深入了解用户需求和缓冲区溢出检测技术与工具.然而,一方面,用户需求并不单一,是纷繁各异的,不同的行业、不同的场景下,用户需求更是千差万别,同时,用户需求并不独立,多个需求之间可能相互制约、需要进行多需求的平衡;另一方面,缓冲区溢出检测工具也是门类繁多,各有特色,要完成用户需求的细分与相应检测工具的匹配,在繁杂各异的用户需求与多种多样的缓冲区溢出检测技术与工具之间建立一张全面、条理清晰、而又便于用户理解、使用的映射图谱是非常困难的,同时也是非常有价值的.

本文站在使用者的立场,从技术与工具实际应用的视角出发,在概述缓冲区溢出漏洞类型与特征的基础上,从软件生命周期阶段的检测与修复、缓冲区溢出攻击阶段的预防与保护、基于认识与理解途径的度量评估这3个应用视角,对缓冲区溢出缺陷检测技术与工具进行梳理,一定程度上在用户需求与检测技术与工具之间建立了一张映射图谱,为用户实际中有效选择缓冲区溢出检测技术与工具提供了指导,也为进一步的研究工作奠定了基础.

1 缓冲区溢出类型与特征

缓冲区溢出[11,12]是一种软件漏洞,对于强调效率优先的非安全类型编程语言 C/C++而言,当试图将超过缓冲区所能容纳的数据输入到缓冲区时,因其不做边界检查,就会发生溢出.其中,缓冲区是指计算机中存储数据的一段连续内存区域,包括数据段、堆段、栈段.

当发生缓冲区溢出时,溢出的数据流入到缓冲区临近的内存区域,进而会覆盖、修改临近内存区域中的值.缓冲区溢出攻击就是利用这一特点进行的.攻击者精心构造输入内容,造成缓冲区溢出,进而使溢出部分修改附近内存中诸如返回地址、函数指针、栈帧基址、指针变量等关键类型的值,使其指向攻击者希望程序后续执行的位置,从而改变程序控制流,最终执行攻击代码(攻击代码可能位于构造的输入内容之中,也可能是系统库中的函数),实现其攻击目的.

基于上述的理解,给出缓冲区溢出漏洞的定义,并对典型缓冲区溢出攻击进行说明.

定义 1(缓冲区溢出漏洞).缓冲区溢出漏洞是一种软件缺陷,指输入数据的长度超过了缓冲区能够容纳的长度,超出的部分数据溢出到临近的内存区域的一种异常.

典型的缓冲区溢出攻击是攻击者基于缓冲区溢出漏洞,通过构造输入,造成缓冲区溢出,使溢出数据修改了临近内存区域的关键值(如 Return Address,Heap Metadata等),进而劫持控制流,执行注入的或者重用已有代码(如系统库函数等)构成的攻击代码的一种软件安全攻击.

1.1 缓冲区溢出漏洞类型

缓冲区溢出漏洞按照不同的标准有不同的分类:按照缓冲区所在内存区域的位置可分为栈溢出、堆溢出和数据段溢出;按照导致溢出的内存操作函数分为字符串操作(如strcpy函数等)导致的溢出和格式化输出(如sprintf函数等)导致的溢出等;按照溢出数据修改的关键值类型分为修改返回地址的溢出、修改函数指针的溢出、修改指针变量的溢出等.

下面简要介绍几种典型缓冲区溢出漏洞类型,主要包括栈溢出、Return-into-Libc溢出、off-by-one溢出、堆溢出、数据段溢出、格式化字符串溢出和整数溢出.

1.1.1 栈溢出

栈溢出是被利用最广泛的溢出漏洞.每一次函数调用,栈中会存放该函数对应的栈帧,帧中包含函数参数、函数返回地址、栈帧基址等信息.例如,函数func的栈帧如图2所示.

Stack Smashing是基于栈溢出的典型攻击,1996年,AlephOne在文献[13,14]中进行了详细的论述,其基本过程如图3所示.首先,攻击者通过精心构造包含恶意代码的输入内容传入函数(如Pointer指向的数组);然后,函数内部内存操作函数(例如strcpy等)将输入内容拷贝到缓冲区,进而造成溢出,溢出部分数据会修改临近缓冲区的关键值,即函数的返回地址;最后,当函数执行结束返回时,程序执行跳转到被修改过的返回地址所指向的地址,即缓冲区中恶意攻击代码所在的位置,进而执行攻击代码.

此外,如果攻击者不是将攻击代码注入到缓冲区中,而是重用已有代码,系统库函数(如system(·),exec(·)等)作为攻击代码,那么这样的溢出攻击叫做Return-into-Libc溢出攻击.ROP(return oriented programming)[15,16]通过RET地址重用Gadgets代码(即已在内存中的指令序列).JOP(jump oriented programming)[17,18]通过Call/Jmp指令重用 Gadgets代码构成攻击代码.函数返回地址是最重要的攻击目标之一,除此之外,指针变量、函数指针、栈帧基址等都是重要的攻击目标,即,可以通过覆盖修改指针变量(如上例中的 Pointer)的值和函数指针(func pointer)指向攻击代码[19].Off-by-one溢出则指的是输入内容恰好超出缓冲区一位数据的溢出,其通常产生于试图将一个数组中的所有元素逐个复制到缓冲区中的循环中,如图4所示.

该程序意在将 input中的数据逐个复制到长度为 128的 buffer数组中,但由于 for循环中i<128写成了i<=128,所以该程序会复制129个数值到buffer中,进而造成off-by-one溢出.

1.1.2 堆溢出

堆是由程序运行时运用malloc(·)和free(·)等函数动态分配、释放的内存块组成,每一个内存块都包含自身内存大小和指向下一个内存块的指针等信息.虽然堆中没有函数返回地址,但是攻击者可以通过修改堆中的函数指针或者指针变量,进而达到修改程序控制流,执行攻击代码的目的.典型的,如图5所示[20].

图 5(a)展示的是一个典型的在堆中动态分配和释放的内存块情况,chunk1是一个已分配的内存块,包含其之前存储的块的大小和它本身的大小信息,User data部分即提供程序写入数据的buffer区域.chunk3是一个临近 chunk1且已被释放的内存块,chunk2和 chunk4是位于堆中其他任意位置的已被释放的内存块.chunk2,chunk3,chunk4在一个双向链表结构中,chunk2是链中的第1个内存块,其前向指针指向 chunk3,后向指针指向了链中前一个内存块.chunk3的前向指针指向chunk4,后向指针指向了chunk2.chunk4是链中最后一个内存块,其前向指针指向了链中下一个内存块,后向指针指向chunk3.图5(b)则展示了一个攻击,当chunk1中的User data部分溢出,攻击者将覆盖重写chunk3的管理信息,chunk3的前向指针被修改指向栈中函数f0返回地址的前12个字节位置,后向指针被修改指向可以跳转到后几个字节然后执行攻击代码的代码位置(code to jump over dummy).当chunk1后续被释放,就会和chunk3合并成了一个大的空闲内存块.由于Chunk3不再是一个独立的空闲内存块,必须首先从空闲结点链表中移除 chunk3.其过程如下:chunk3→fd→bk=chunk3→bk,chunk3→bk→fd=chunk3→fd.即fd指向位置12个字节之后的内存位置的值(即Returnaddressf0的地址)会被bk指向位置的值(即Codeto jump over dummy地址)重写,bk指向位置8字节之后的内存位置的值(dummy内的地址)会被fd指向位置的值(即 Localvariablef0的地址)重写.因此,在图 5(b)中的返回地址会被一个指向跳转代码的指针重写,进而越过存储fd的地址区域,进而执行注入的攻击代码(InjectedCode).

1.1.3 数据段溢出

数据段溢出[21]与堆段溢出类似,数据段中存储的是初始化和未初始化的全局/静态变量.如图6所示.

上述程序中,如果str的长度超过buffer容量就会造成溢出,覆盖函数指针fptr,这样就可以改变程序的执行流程,使其跳转并执行攻击代码.

1.1.4 格式化字符串溢出

格式化字符串溢出[11,22,23]主要由格式化字符函数如fprintf,sprintf,snprintf,syslog等引起.对于格式化字符串系函数,如果不按照规定给定正确的输入、输出格式以及相应变量,就可能发生溢出.典型的如函数sprintf(char*str,constchar*format,...)是将格式化的数据写入str所指的数组中,并添加‘',如果格式化的数据长度超出了数组的容量就会溢出.再比如sscanf(constchar*s,constchar*format,...)从s中读进数据,按照format的格式将数据写入到其他参数中,如果格式化后得到的字符串长度大于相应的参数字符数组大小就会溢出[84].此外,对于格式化字符串,利用%格式符可以把已经输出的字符串长度写到指定的内存单元,换言之,攻击者可以通过%n来改写函数的返回地址等信息.

1.1.5 整数溢出

该例子中,参数str被拷贝到一个有限长度的缓冲区中,虽然对缓冲区的写入有边界保护,但是当str的长度超出了短整型的范围,它很可能被截取为一个小于 80的值,这样边界保护不再有效,一个超出缓冲区大小的数据就会被写入到缓冲区中,从而造成缓冲区溢出.

1.2 缓冲区溢出攻击特征

缓冲区溢出可发生在栈、堆、数据段区域,其主要目标是通过溢出数据修改返回地址、函数指针等关键值,进而修改程序控制流,最终执行攻击代码.缓冲区溢出攻击的主要特征[26]可归纳为如图8所示.

至此,每一类具体的缓冲区溢出攻击可以概括为上述若干特征的集合.典型的如 Stack Smashing={len:buff,con:addr,con:inst;mod:radd,jmp:stack,exe:stack}.

2 多应用视角下的缓冲区溢出检测技术与工具

从工具实际应用的视角来看,使用者更关心的问题是如何在有限成本的制约下,有效地选择一个或多个能够最大限度满足自身需求的检测工具进行应用,并达到尽可能好的效果.解决这一问题需要进行用户需求的细分与相应检测工具的匹配,在各异的用户需求与多样的缓冲区溢出检测技术与工具之间建立一张全面、条理清晰、便于用户理解和使用的映射图谱.

虽然使用者具体的细分需求(成本、场景、精度、广度、功能等)纷繁各异,但是其使用工具的目的大致可以归纳为3点.

(1) 使用工具对缓冲区溢出漏洞进行检测,进而修复软件漏洞;

(2) 使用工具对缓冲区溢出攻击进行预防,进而保护软件安全;

(3) 使用工具对缓冲区溢出漏洞摆脱程度进行度量,进而评估软件可信性.

基于上述归纳,本节试图从检测与修复、预防与保护、度量与评估这 3个应用视角出发,对现有的缓冲区溢出检测技术与工具进行梳理,在用户细分需求与相应的检测技术工具之间建立一个映射图谱.

进一步,每个应用视角下的具体阐述也试图解决使用者实际中普遍关心的问题,即:

(1) 在软件制品的不同阶段,可分别选用哪些工具进行缓冲区溢出漏洞的检测与修复?

(2) 在缓冲区溢出攻击的不同阶段,可分别选用哪些工具进行缓冲区溢出攻击的预防与保护?

(3) 在基于认识与理解途径的不同度量评估需求下,如何选择工具度量评估缓冲区溢出漏洞摆脱程度?

以下内容的阐述一定程度上回答了上述问题,具体内容可归纳为:(1) 软件生命周期阶段的检测与修复;(2) 缓冲区溢出攻击阶段的预防与保护;(3) 基于认识与理解途径的度量与评估.

2.1 软件生命周期阶段的检测与修复

使用者选用检测技术与工具的目的之一是为了对软件进行缓冲区溢出漏洞的检测,进而修复漏洞.然而,面对软件生命周期不同阶段产生的不同形态的软件制品,该选用哪些检测技术与工具进行分析与检测?

软件生命周期阶段可如图9所示.

每一个阶段,都有相应的检测技术与工具可供用户选择进而对软件制品进行缓冲区溢出漏洞检测.

· 在源码阶段,用户可以使用诸如词法分析、语义分析、约束分析、符号执行、定理证明、模型检验等各种静态分析技术与工具,不需要运行程序,直接基于源码进行缓冲区溢出漏洞检测;

· 在编译阶段,可供选择的工具主要是对现有编译器进行修改、扩展,通过增加边界信息、边界检查代码、或者增加安全类型检查来预防缓冲区溢出漏洞;

· 在目标代码阶段,可供选择的工具更多的是基于二进制码进行缓冲区溢出漏洞分析,主流方式可概括为3种:逆向工程、补丁对比、错误注入;

· 链接加载阶段,可供选择的工具主要是通过对不安全的库函数进行增强或替换,对系统内核或环境进行修改来阻止由不安全的库函数或者非正常的函数调用而产生安全缺陷;

· 最后,在测试运行阶段,更多的是综合使用如模糊测试、混合执行、动态污染分析等各种动态检测技术与工具,进行缓冲区溢出漏洞检测.

2.1.1 源码阶段

· 词法分析

基于词法分析技术的缓冲区溢出检测工具,其基本思想是:通过扫描源代码,与现有缓冲区溢出漏洞库中归纳的漏洞特征、规则等信息进行模式匹配,如果发现与漏洞库中不安全代码模式符合,则给出警告.词法分析技术因其不考虑语义信息,所以分析速度快,可处理程序规模大,但同时误报率较高.典型的工具有 ITS4[27],Flawfinder[28],RATS[29]以及商用工具Fortify[30]等.其中,ITS4由Cigital公司于2000年发布,其能对C/C++程序的每一个函数进行扫描分析,与维护的缓冲区溢出漏洞库进行匹配,并依据危险等级给出提示报告,其漏洞库随着新漏洞的发现而不断更新.类似的,Flawfinder是2001年发布的 C/C++安全审查工具,Flawfinder同样内置了一个漏洞数据库,如格式化字符串溢出漏洞等.RATS相较于前者则支持更多的语言,提供对 C,C++,Perl,PHP以及Python语言的漏洞扫描.文献[31]对前3个工具的性能进行了分析比较,结论指出,ITS4的综合性能最好.类似的,文献[32]对Splint,Fortify,Checkmarx[33]进行了性能以及误报率、漏报率的比较分析.

· 语义分析

基于语义分析的检测工具在语法分析的基础上增加语义分析,在发现潜在的缓冲区溢出漏洞方面具备更强的能力.代表性的工具是 Splint,其为 Lint,LCLint[34]的改进版本,其根据用户提供的语义注释信息来检测程序中的安全漏洞.系统内建立了一组使用高危函数的安全条件(如对strcpy要求目标函数区空间大于源缓冲区).类似的语义分析工具还有 Prefast[35],Prefix[36],Marple[37]等.文献[38]构造了一组 benchmark 量化评估了如Splint,Prefast,Clang,UNO等典型静态分析工具的性能与准确率,结果显示,Prefast在文中规定的测试集上对缓冲区溢出漏洞的检测准确率最高达41.8%.

· 约束分析

美国加州大学伯克利分校的Wagner设计的BOON[39]是基于约束分析的缓冲区溢出检测工具的典型代表,其将字符串看成一种抽象数据类型,将缓冲区建模为一个表示大小和当前长度的整数对,对缓冲区的操作建模为对缓冲区范围的修改,进而将缓冲区的约束产生问题转化为整数范围的约束求解问题.通过对建立的程序约束条件求解,来发现可能的缓冲区溢出缺陷.但BOON实现的检测精度较低,实验表明,BOON的误报率达90%.

· 符号执行

符号执行技术试图用新的方式捕获程序的执行语义,是对普通执行的一般化,其用符号代替实际输入,执行过程中收集相关约束,到程序出口或发现错误时,根据收集的约束条件求解,进而产生测试用例.基于符号执行技术进行缓冲区溢出检测的工具有ARMOR,ARCHER[40]等,其中,ARCHER是基于符号执行和路径敏感分析技术开发的用于分析软件中数组越界漏洞的静态分析工具.ARCHER则通过遍历分析所有语句来发现缺陷,对于内存访问语句,遍历模块先调用求解器检查该语句是否可能越界,然后更新求解器的状态.基于符号执行的方法具有较高的准确性,但由于求解器使用的开销较高,同时求解能力有限,所以基于符号执行的方法普遍存在可处理程序规模受限的问题.文献[41]构建了一组测试集量化分析了ARCHER,Splint,UNO,BOON等静态分析工具,结果显示:ARCHER表现较好,检测率达 90.72%,几乎没有误报;UNO检测率为 51.89%,同样没有误报;Splint检测率为56.36%,误报率为12.03%;BOON表现最差,检测率仅为0.69%.

· 定理证明

定理证明技术的基本思想是:将源程序的行为语义和安全性质转换为逻辑公式,再将这些逻辑公式输入到定理证明器,进而将软件缺陷的检测问题转换为逻辑公式的证明问题.如果定理证明器证明这些逻辑公式是有效的,则说明源程序不存在缺陷.其优势在于定理证明可以处理无限状态;其不足在于语句转换过程复杂,难以全面实现自动化.典型的,基于定理证明技术的缓冲区溢出检测工具有ESC[42]和CSSV[43]等.ESC是可用于检测Modula-3和Java两种语言代码中的数组越界漏洞:首先输入用户注释过的程序,然后利用验证条件产生器产生关于源程序行为正确的条件,最后将产生的逻辑公式输入到定理证明器 Simplify完成验证.如果验证失败,则说明源程序中存在错误,定理证明器给出相应的反例.CSSV则是一种验证 C代码中是否存在字符串溢出漏洞的工具:首先将C源程序和安全性质转换为带注释的程序,然后利用指针分析算法GOLF进行过程间别名分析,进而将源程序转换为整数程序,最后用前向完整性整数分析算法和后向完整性整数分析算法检测整数程序,进而报告发现的漏洞.

· 模型检验

模型检验的基本思想是:使用有穷状态迁移系统对程序的行为进行建模,使用时序逻辑或者模态逻辑公式对待检验的安全性质进行刻画,程序的每条执行路径及其相应的状态对应于迁移图中的一条状态迁移轨迹.通过穷举状态迁移图中每一条状态迁移轨迹,判定该轨迹上的所有状态是否满足待检验性质:如果所有路径上的所有节点都满足性质,则程序满足了待检验性质;否则,模型检验器给出使性质为假的系统状态迁移轨迹作为反例.运用模型检验方法进行缓冲区溢出检测的典型工具有UNO[44],MPOS[45].其中,UNO是一个用于发现C程序中数组访问越界漏洞的模型检验工具;MOPS是Berkeley大学开发的可用来验证程序操作序列相关的安全性质的模型检验工具.类似的工具还有 ESPx[46],BufSTAT[47]等.基于模型检验的方法相较于定理证明可以实现自动化,但是只能处理有限状态程序,同时面临状态空间爆炸问题.

2.1.2 编译阶段

众多缓冲区溢出缺陷的检测工具是通过修改、扩展现有的编译器实现.主流的可分为两类:一类是通过增加边界信息以及边界检查代码,代表性的工具有 StackGuard[48],ProPolice[49],Purify[50],CERD[51],Insure++[52],TinyCC[53]等;另一类是增加安全类型检查,代表性的工作如 CCured[54],其通过静态分析将指针分为 SAFE,SEQ和WILD这3种类型,再对3类指针进行分类插装检查,如SAFE型的指针检查是否为空、SEQ指针检查其地址范围等.

2.1.3 目标代码阶段

在目标代码阶段面对的往往都是二进制码,相应的基于二进制码进行缓冲区溢出检测[19]的工具按照使用方法可分为3类.

· 第一是逆向工程方法,即:通过反汇编技术将二进制码转化为中间表示或者汇编代码,再基于转化后的表示进行相应的缺陷检测.典型的工具 IntScope[55],其将可执行文件转化为中间语言表示,再运用符号执行检测潜在的整数溢出漏洞.类似的工具还有WinSafe[56];

· 第二是补丁对比方法,即:通过二进制文件对比揭示二进制文件中的信息差异,进而用于缓冲区溢出漏洞挖掘.简单的,可通过比较patch前后可执行文件的变化,进而确定溢出点.典型的如Sabin提出的基于指令相似性的图形化比较[57]和 Flak提出的结构化的二进制比较[58].前者可以发现文件中一些非结构化的变化,如缓冲区大小的改变;后者则更注重二进制可执行文件在结构上的变化;

· 第三是错误注入方法,即:运用各种类型的不规则输入对程序进行探测,以此来触发潜在的安全漏洞,测试成功的标志是程序的异常.Fuzzing通过向被测程序提供半有效性的输入(即可以被应用程序所接受并且具有一定破坏性的随机输入),检查应用程序是否能正确处理可能的错误输入.通过监控应用程序的执行情况,发现程序中潜在的诸如缓冲区溢出等漏洞.典型的针对缓冲区溢出漏洞的检测工具有AFL[59],Dowser[60],SwordFuzzer[61],TaintScope[62]等.Fuzzing易实施,并能够发现确切潜在的缓冲区溢出漏洞,其主要问题是如何产生高效的测试输入尽可能覆盖目标程序的所有代码.而 Symbolic execution能够有效的产生高覆盖度的测试用例,典型的可用于二进制的符号执行框架有 Angr[63],S2E[64]和BAP[65]等.

2.1.4 链接加载阶段

一方面,链接加载的库函数中的不安全函数是导致缓冲区溢出威胁的重要推手;另一方面,系统运行的软、硬件环境对缓冲区溢出攻击的实施有着重要影响.所以,在该阶段可供使用者选择用于防护缓冲区溢出漏洞的工具可分为3类.

(1) 不安全的库函数增强、替换.典型的,如FormatGuard是glibc的一种增强,其可以在不需要显著降低程序运行性能的情况下有效检测针对格式化字符串溢出缺陷的攻击.类似的,LibSafe[66]将一些已知的易受堆栈溢出攻击和格式化字符串漏洞攻击的库函数诸如strcpy(·),printf(·)等进行了修改封装.这些修改后的函数一方面实现了原有功能;另一方面还可以确保缓冲区溢出范围被限制在当前栈的栈帧之内,继而返回地址和堆栈指针的内容无法被修改,可以有效地阻止堆栈溢出攻击和格式化字符串溢出攻击.LibSafeXP[67]是对 LibSafe的拓展,而 LibVerify[68]使用和 StackGuard类似的动态方法来进行增强保护;

(2) 操作系统内核补丁实现堆栈不可执行,如OpenBSD,PaX[69],DEP[70]等;

(3) 硬件支持不可执行内存,如 Intel AMD “NX”[71]等.

2.1.5 测试运行阶段

在测试运行阶段,基于各种动态技术诸如模糊测试、动态污染分析、混合执行方法开发的工具可供使用者选择、组合、应用于测试运行时检测缓冲区溢出漏洞.

· 模糊测试

模糊测试是一种使用大量半有效输入对目标程序进行测试,通过探测、监视程序运行过程中的异常来挖掘软件系统漏洞的方法[72,73].模糊测试按照测试用例的产生方式可以分为基于生成的(generation based)模糊测试与基于变异的(mutation based)模糊测试,典型基于生成的模糊测试工具如 SPIKE[74],Sulley[75],Peach[76]等,通过构建协议语法与Session模型生成测试用例,因为其生成的测试用例满足语法格式,可以较快地通过语法检查部分进入程序语义逻辑层面进行探索,故更适用于测试接收高结构化输入的程序;相应的,典型的基于变异的模糊测试工具如 AFL,LibFuzzer[77]等,基于程序插桩反馈在给定seed基础上应用不同变异算子,进而产生测试用例,通过覆盖度反馈指导不断产生可以覆盖更多路径的测试用例,更适用于测试接受输入简洁、无结构化格式要求的程序.典型基于模糊测试的检测缓冲区溢出漏洞的工具有 AFL,LibFuzzer,Dowser,SwordFuzzer,STOBO[78],FLS[79]等.

· 动态污染分析

动态污点分析是在程序执行的过程中,依据执行流程标记污染数据、跟踪数据污染信息的传递,检测污染数据的非法使用,从而达到跟踪攻击路径、获取漏洞信息的目的.污点分析的过程一般可以分为3个步骤:标记污染数据、跟踪污染数据的传递、污染数据非法使用的判定.动态污点分析能有效提高缺陷检测的精度,在阻止攻击的同时获得程序的漏洞所在,应用性较强,代表性的工具有TaintScope[62],Dytan[80]等.

· 混合执行

混合执行[81]是混合两种执行方式,在具体执行的同时,对所执行到的代码进行符号执行,具体执行的特性决定了每次混合执行获取的路径都是可行路径,因此避免了误报,这是混合执行相对于静态符号执行的主要优势之一.一次混合执行结束时,其路径约束是一组约束的合取,对其中某个或某几个约束取反的同时,保持其余部分不变,则可以得到路径约束的一个变体,利用求解器对变换后的新约束求解,若有解,则意味着它对应于另一条可行路径.在此过程中,基于新的启发式算法探索程序的路径空间.典型的混合执行工具有 DART[82],CUTE[83],SAGE[84],EXE[85],KLEE[86],Angr,S2E和BAP等,其中:DART和CUTE的路径空间搜索与路径选择策略是一次混合执行之后,对取得的路径约束的最后一个约束进行取反而得到新的路径;而 SAGE则按照不同组合对多个约束取反,从而获得新的路径;KLEE则对每条路径已执行部分进行打分,通过贪心算法,从一组路径集合中选择最重要的优先处理.此外,文献[73]对包括 EXE,DART,KLEE,BAP,S2E,Angr在内的主流符号执行以及混合执行技术在内存消耗、环境交互、循环处理、状态空间搜索与路径选择、约束求解等方面进行了细致的对比分析与讨论.相应的,专门的基于混合执行检测缓冲区溢出漏洞的工具有BIOL[87]等.

综上所示,基于软件生命周期的检测与修复工具见表 1,一定程度上回答了用户实际关心的问题,即,软件制品的不同阶段可分别选用哪些工具进行缓冲区溢出缺陷的检测与修复.

Table 1 Detection and repair tools of software lifecycle stages表1 软件生命周期阶段的检测与修复工具

2.2 缓冲区溢出攻击阶段的预防与保护

使用者使用检测技术与工具第 2个目的是为了预防缓冲区溢出攻击,进而保护软件产品.然而,问题是面对不同特征的缓冲区溢出攻击,用户可以从哪些阶段进行预防,不同阶段又可以选用哪些工具达到预防与保护的目的?

典型的缓冲区溢出攻击阶段如图10所示.

每一个攻击阶段,用户都可以选用相应的技术与工具构建预防和保护措施.典型的措施有输入检测、边界检查、关键值完整性检查、控制流监控和堆栈不可执行.

2.2.1 输入数据阶段

正如文献[88]中所言:“所有的输入都是恶意的,除非被证明”,外部的恶意输入是所有攻击产生的源头,OSWAP更是于2017年将“不安全的攻击保护(包括不恰当的输入验证)”列为Top 10安全风险之一.故在输入数据阶段,相应的预防保护是对输入数据进行检查.具体技术与工具可分为两类.

· 一类是基于攻击代码特征的模式匹配,就缓冲区溢出攻击而言,如果其恶意代码是在输入数据中,那么输入数据必然包含一些典型特征(如包含与关键值类型相同的值、用于填充的 NOP指令、Shellcode等),因此,运用模式匹配算法将输入数据与已知的恶意攻击代码特征进行匹配,如果发现恶意输入,给出警告,就可以一定程度地在源头有效控制恶意攻击的发生,典型的工具如Polygraph[89];

· 另一类是标记并跟踪分析输入的污染数据,其指导思想是:恶意输入的污染数据如果在后续敏感操作中被使用,就有可能造成威胁.所以通过标记输入对其进行污染传播分析,如果污染数据在后续的敏感操作中被使用,就给出警告.典型的工具有Tainted Pointer[90].

2.2.2 缓冲区溢出阶段

缓冲区溢出是缓冲区溢出攻击发生的前提,最直接的对应于缓冲区溢出阶段的保护策略是增加边界检查.所谓边界检查指的是对每一个数据的操作进行检查,使其必须在缓冲区的边界之内进行.

缓冲区溢出检测工具按照边界检查实现的方式可分为3类.

(1) 基于编译器的修改增加相应的边界信息和边界检查代码,典型的工具如Bounds Checker[91],CERD[51],MOBD[92]等;

(2) 基于虚拟机的修改增加安全类型检查,典型的工具如CCured[54];

(3) 通过硬件实现支持,如IA-32/I432[93].

理论上,如果对每一个内存操作都进行边界检查,就可以彻底避免缓冲区溢出攻击的发生,但是需要付出巨大的额外开销.如果对C/C++的每一个内存操作都进行边界检查,也就损失了C/C++效率优先的优势.

2.2.3 修改关键值阶段

缓冲区溢出的直接目的是运用溢出数据覆盖、修改诸如返回地址、函数指针等关键类型的值.那么,在关键值使用之前对其进行完整性检查,有助于发现缓冲区溢出漏洞,预防缓冲区溢出攻击.

典型的,以返回地址为例,对其进行完整性检查的方法有3种.

(1) 基于 Canary检测的保护,即在返回地址前增加一个 Canary字节数据,将对返回地址的完整性检查转化为对 Canary值的完整性检查.因如果缓冲区溢出数据覆盖修改了返回地址,必然覆盖修改 Canary,故在使用返回地址之前对Canary进行检查,如果发现Canary值已经被修改,则中止程序给出警告.典型的StackGuard[48]对编译器GCC添加补丁,使得在函数入口能自动在栈中生成Canary标记,在函数调用结束时,检测 Canary标记是否改变来发现并且阻止缓冲区溢出攻击.ProPolice[49]与 StackGuard类似,不同的是 ProPolice将函数中用到的局部变量重新排列,使得函数的缓冲区紧邻 Canary,栈中的其他关键值(如函数指针等)就不会受到缓冲区溢出的影响;

(2) 基于加密的保护,即,将加密技术运用于保护关键值(返回地址等)的完整性.用预先定义的密钥在返回地址存储到内存前进行加密,使用时再进行解密.典型的如 PointGuard[94],其通过改进 GCC编译器实现,基本思想是:在程序装入内存时,先将指针型数据加密;当程序访问这些数据时,再在寄存器中进行解密;

(3) 基于备份的保护,即:将返回地址复制一份备份,存储在其他区域,当使用时,通过与备份比对或者用备份替换来保证返回地址的完整性,典型的工具如 StackGhost[95],StackShield[96],RAD[97]等.StackShield基于对 GCC的修改,在函数调用将返回地址压栈时,用一个全局数组存储备份返回地址.当函数调用结束时,用备份的返回地址替换栈中的返回地址,以此达到保护返回地址的目的.StackGhost思想类似,具体实施是基于SPARC处理器体系结构对系统内核进行修改,从而实现对返回地址的保护.

基于关键值完整性检查的保护策略可以有效地对特定类型的关键值进行保护,从而提高了相应缓冲区溢出攻击的门槛.其不足在于:因为是针对特定类型的关键值,所以其只能检测特定类型的缓冲区溢出漏洞.

2.2.4 改变控制流阶段

攻击者修改关键值的目地是为了改变程序控制流,程序执行进行非正常的跳转或者非正常的系统调用,是出现缓冲区溢出攻击的重要特征.因此,保障程序控制流的完整性[17],通过阻止非正常的程序执行跳转和非正常的系统调用,使程序运行中的控制转移,始终处于原有的控制流图限定范围内,可以有效地预防和阻止缓冲区溢出攻击.

控制流完整性(CFI)的具体做法是:通过分析程序的控制流图获取间接转移指令(包括间接跳转、简洁调用以及函数返回指令)目标的白名单,并在运行过程中核对间接转移指令的目标是否在白名单中.CFI从实现角度可分为细粒度和粗粒度两种:细粒度 CFI严格控制每一个间接转移指令的转移目标,这种精细的检查提升了安全性,但是通常会引入巨大开销;粗粒度CFI则是将一组类似或相近类型的目标归到一起检查,以降低开销,但会导致安全性的下降.基于CFI保护的典型工作包括CCFIR[98],其策略是区分间接调用指令和函数返回指令,阻止未经验证的返回指令跳转到敏感操作函数上,一定程度上避免了 CFI插桩开销大的问题.BinCFInally[99]则提出致力于将CFI应用于已有的商用应用上.PathArmor[100]则是一个可用于现实应用程序的高效可靠的上下文敏感CFI方案.此外,KCoFI将CFI做到了操作系统内核之中,使之免受经典的控制流劫持、return2user和代码段修改攻击[101].文献[102]则对主流的控制流完整性保护技术做了详细的性能分析、比较与讨论.此外,保障控制流完整性的工具有基于API-Hook方法的AIFD[103],其通过跟踪返回地址相关的API调用,与收集的合法API调用模式进行比较而发现潜在的缓冲区溢出漏洞.McAfee[104]有着类似的缓冲区溢出保护机制,例如:若堆栈中的shellcode调用了getProcaddress等函数,那么McAfee就会中止当前进程并报警.

2.2.5 执行攻击代码阶段

缓冲区溢出攻击的最终目的是执行攻击代码,如果最终使得攻击代码无法执行,那么就可以在最后阶段成功阻止攻击,避免危害.基于该指导思想的保护策略是使堆栈不可执行来阻止攻击代码存储在堆栈中的缓冲区溢出攻击.

典型的实现方式有两种:其一是通过操作系统内核补丁实现,典型的工具如 PaX[69],DEP[70];其二是通过处理器体系结构支持实现,如 SPARC,AMD “NX”[71]等.

不可执行内存的保护方法可以有效地阻止攻击代码存储在堆栈中的攻击,但是对于攻击代码不在堆栈中的攻击,如Return-into-Libc,ROP,JOP攻击就无效.针对Return-into-Libc,ROP,JOP攻击,一般配合使用堆栈不可执行如DEP[24]和随机化地址空间ASLR(address space layout randomization)机制[18],可以起到很好的防护效果.

综上所示,基于缓冲区溢出攻击阶段的预防与保护工具见表 2,一定程度上回答了用户实际关心的问题,即:在缓冲区溢出攻击的不同阶段,可分别选用哪些工具进行缓冲区溢出攻击的预防与保护.

Table 2 Prevention and protection tools of buffer overflow attack stages表2 缓冲区溢出攻击阶段的预防与保护工具

2.3 基于认识与理解途径的度量与评估

使用者使用检测技术与工具的第三个目的是对软件摆脱缓冲区溢出漏洞的程度进行度量,进而评估软件的安全性.如图 15所示:对软件摆脱缓冲区溢出漏洞的程度进行度量与评估,是通过基于用户需求建立度量规约,选择与用户需求相适应的缓冲区溢出漏洞检测工具对软件进行分析,并基于分析所得对软件形成的直接认识与理解,包括检测出的漏洞数目、各项度量指标值、度量规约的完备度等基础上进行的.度量与评估的对象是软件系统,但同时也体现着对缓冲区溢出漏洞检测工具能力的度量.

度量是定量的,指的是基于工具获得客观证据;评估是定性的,指的是基于度量结果(即获得的客观证据)形成主观判断.直接认识与理解的维度可分为对模型认识与理解的程度、对代码认识与理解的程度.直接认识与理解的途径可分为静态分析、动态测试、形式化验证.

用户根据需求选择度量工具进行缓冲区溢出漏洞摆脱程度度量与评估的基本流程可如图12所示.

首先,根据用户需求建立度量规约.度量规约是由一组对应于各个途径下检测工具的评估项构成,其根据用户的需求建立,体现用户需求和可投入成本,同时也是度量结果可信性比较的依据.度量规约本身根据其完备级别有着可信级别的划分.实际中,需要充分针对具体应用场景,对多种需求进行平衡,进而建立相应的度量规约.通常的,对缓冲区溢出漏洞而言,用户选用相应工具应用到漏洞摆脱程度度量与评估时的度量规约一般涉及为以下几个方面.

(1) 成本:指的是用户可投入的经济成本,表现为具体选择时,需要考虑工具是免费开源还是商用付费,对于商用付费类型,试用、购买、租用等不同方式的价格等因素;

(2) 效率:指的是用户针对特定类型或规模的软件系统进行度量时可允许的时间限制,表现为具体选择时,需要一方面关注工具检测算法的复杂度,另一方面关注工具处理规模与耗时的比值;

(3) 广度:指的是需要检测哪些类型的缓冲区溢出漏洞,表现为具体选择时,关注于工具所能检测的缓冲区溢出漏洞的类型以及运行平台;

(4) 精度:指的是用户需要对工具检测软件产品缓冲区溢出漏洞的准确程度的要求,表现为选用工具是更多的关注指误报率与漏报率;

(5) 功能:指的是用户考虑检测工具功能模块的要求,具体选择是关心的可能是工具是否同时包括检测和对应的修复功能,从而可以降低人工确认修复的负荷和成本等;

(6) 途径:指的用户对技术手段和认识与理解途径的要求,表现为选择度量工具时,关注于工具使用的技术途径,如静态分析、动态测试、形式化验证或者多种途径结合;

(7) 其他:如用户需要对软件制品生命周期的哪些阶段进行检测,或者对预防攻击的哪些阶段进行检测保护等.

然后,根据度量规约选择度量工具.度量工具检测的项目内容对应于度量规约中的评估项,为此,度量规约同时决定了度量工具选择的标准,如度量规约中的“广度”规定了选择的度量工具能够检测的缓冲区溢出漏洞类型,“途径”需求规定了选择的度量工具基于的认识与理解途径,包括静态分析途径、动态测试途径、形式化验证途径.

(1) 静态分析途径

静态分析途径指的是基于静态分析工具对软件代码制品进行扫描,进而发现代码中的语法、语义错误.一般采用驼鸟策略,基于分析结果形成警告信息.其优点是可直接检查程序,而不用驱动程序运行,但是其警告信息需要进一步确认,成本较高.静态分析途径下主要的分析技术包括有词法分析、语义分析、约束分析、符号执行等,静态分析更多地在软件测试运行前的阶段使用.

(2) 动态测试途径

动态测试途径指的是动态运行程序以发现错误,其优点在于检测可以达到较高的精度,但是性能和规模受限.动态测试途径下主要的分析技术包括模糊测试、污染分析、混合执行等.

(3) 形式化验证途径

形式化验证途径指的是通过建立系统的形式化模型,进而验证模型是否满足给定性质.但是其构建的数学模型状态空间复杂度较高,此外,状态空间爆炸是其面临的主要问题.形式化验证途径下主要的分析技术包括定理证明和模型检验等.

表3、表4分别给出了缓冲区溢出漏洞类型以及不同认识与理解途径下对应的缓冲区溢出漏洞检测工具.

Table 3 Types of buffer overflow vulnerabilities and corresponding detection tools表3 缓冲区溢出漏洞类型与对应缓冲区溢出漏洞检测工具

Table 4 Buffer overflow detection tools based on knowledge and understanding表4 认识与理解途径下的缓冲区溢出漏洞程度漏洞检测工具

然后,根据度量工具确定度量指标.其中,度量指标是相关于分析、测试和验证技术与工具的,其值反映认识与理解(即分析、测试、验证)的充分度.例如,静态分析中的相关指标可以是不确定性分析结果的比例,动态测试的相关指标可以是各种模型覆盖度、代码覆盖度,形式化验证的相关指标可以是状态空间被搜索的程度.接着,运行度量工具获得度量结果,度量结果指的是运行度量工具对软件制品进行检测获得的对应于度量规约的客观证据,如检测的漏洞数目、对应的度量指标值.

最后,根据度量结果评估软件缓冲区溢出漏洞摆脱程度.评估是主观判断,基于度量结果,在综合考虑度量规约的完备度、检测出的漏洞、度量指标反映的认识与理解的充分度的基础上,结合不同领域、行业、机构、项目以及个人经验设定度量规约中个度量项的可信阈值,在不彻底消除漏洞的情况下给出判定.

综上所示,一定程度上回答了用户实际关心的问题,即:在如何基于认识与理解途径,在不同需求下选择缓冲区溢出漏洞检测工具获得对软件的认识与理解,进而度量与评估软件缓冲区溢出漏洞摆脱程度.

3 总结与展望

站在使用者的立场,在概述缓冲区溢出漏洞类型与特征的基础上,从软件生命周期阶段的检测与修复、缓冲区溢出攻击阶段的预防与保护、基于认识与理解途径的度量与评估这3个应用视角,对缓冲区溢出漏洞检测技术与工具进行梳理,一定程度上在用户需求与检测技术工具之间建立一张映射图谱,为用户实际中有效选择缓冲区溢出检测技术与工具提供了指导,为进一步的研究工作奠定了基础.

目前,缓冲区溢出漏洞检测技术与工具的研究中,存在一些后续研究工作可以关注的方面.

(1) 误报率与漏报率是衡量单个缓冲区溢出检测工具能力的重要指标,也是对比多个类似检测工具能力的有效依据.然而目前相关文献中,工具能力的判定更多的是基于个例的测试样本,看能否发现未知的漏洞,很少给出具体的误报率与漏报率的数值.其根源在于衡量工具自身检测能力,或者对比多个工具检测能力时,缺乏一个统一的完备的标准测试集(benchmark)作为权威的测试对象,以便产生令人信服的误报率与漏报率的数值报告;

(2) 就单一工具缓冲区溢出检测方法与技术改进而言,静态方法处理规模大,速度快,但是误报率与漏报率高;动态方法检测结果精确,但是额外开销与性能损失大.动、静态方法的结合是进一步改进检测效率与精度的有效方式.目前,混合执行近 10年来得到了学术界大量关注,在遍历程序执行路径、探索程序执行状态空间方面有着突出优势.其次,模型检验方法通过建模、性质规约,通过穷举模型状态空间判定性质是否满足,在一定程度上可以证明系统彻底摆脱某种类型的缓冲区溢出漏洞.结合模型检验与混合执行的方法对系统动态语义进行精确建模,提炼缓冲区溢出漏洞性质进行规约,运用类似于混合执行的方法探索状态空间,可以进一步降低缓冲区溢出检测的误报率与漏报率;

(3) 误报率的产生受两方面因素影响:其一是检测技术对程序动态语义建模的准确程度,其二是对缓冲区溢出漏洞特征提炼的精确程度.漏报率的产生,更多地是由对程序执行状态空间探索的充分度决定.然而目前,大部分检测工具都是基于某一个关键特征(如缓冲区数据溢出、关键值被修改等),在某一个特定阶段(源码阶段、运行时阶段等),关注于单一工具的检测技术的改进提高,缺乏基于对多用户需求与应用场景要求深入分析、理解、平衡基础上进行工具匹配组合,用于多特征、多阶段检测,检测结果融合比对分析,进而发挥工具组合效益的研究.

所以,结合上述的3点可以关注改进的地方,未来的研究内容如下.

(1) 缓冲区溢出漏洞最小完备标准测试集的收集整理.收集、整理一个用于测试缓冲区溢出检测工具自身检测能力,用于对比测试类似工具检测能力的最小完备标准测试集(benchmark).基于该权威测试集,测试工具可以运用该测试机进行自测,给出准确的误报率与漏报率信息;

(2) 基于模型检验与混合执行的检测技术改进,降低误报率、漏报率.检测方面,结合模型检验与混合执行的方法,对程序动态语义进行尽可能的精确建模,基于对特定类型(如格式化字符串溢出或整数溢出)漏洞特征深入提炼的基础上进行性质规约,借鉴混合执行思路,将缓冲区溢出检测问题转化为状态可达性求解问题,降低误报率与漏报率;

(3) 基于应用场景的多用户需求平衡与多工具组合应用研究.针对具体应用场景要求,对多用户需求进行深入细致分析、理解、平衡,选择与需求相匹配的工具组合应用,进行多阶段、多特征的缓冲区溢出缺陷检测,进而发挥工具组合效益.基于应用场景的多用户需求平衡与多工具组合应用进行检测,对多工具检测结果进行对比、分析、确认、分级.所有的特征都满足肯定就是漏洞,满足一部分的(其中,满足关键特征的可能性就大,一般特征的就可能性就小)可能是漏洞,都不满足肯定不是漏洞.

猜你喜欢

缓冲区度量漏洞
漏洞
鲍文慧《度量空间之一》
基于selenium的SQL注入漏洞检测方法
突出知识本质 关注知识结构提升思维能力
度 量
侦探推理游戏(二)
三参数射影平坦芬斯勒度量的构造
缓冲区溢出漏洞攻击及其对策探析
漏洞在哪儿
初涉缓冲区