4.1.1 源代码漏洞挖掘
在现代软件开发环境下,通常将源代码编译或解释为二进制文件,而后作为信息系统的一部分运行,因此源代码中的安全缺陷可能会直接导致软件漏洞的产生。
源代码漏洞挖掘通常是使用静态分析技术。静态分析是指在不允许软件运行的前提下的分析过程,分析对象可以是源代码,也可以是某种形态的中间码。
针对源码的漏洞挖掘主要有三种常见的技术:数据流分析、污点分析和符号执行。
1.数据流分析
(1)基本概念
数据流分析是一种用来获取相关数据沿着程序执行路径流动的信息分析技术,分析对象是数据执行路径上的数据流动或可能的取值。在某些情况下,漏洞分析所关心的主要是数据的流动或数据的实质,如在SQL注入等漏洞检测中,检测系统需要知道的是某个变量的取值是否源自某个非可信的数据源。而在另一些情况下,检测系统需要知道程序变量可能的取值范围,如在缓冲区溢出漏洞的检测中,需要获得内存操作长度的可能取值范围来判断是否存在潜在的缓冲区溢出。很明显,后者需要实施更为深入的数据分析,涉及更为深入的程序语句语义计算。
执行路径表现为代码中的语句序列。
以下为三种数据流分析方法。
1)流不敏感:不考虑语句执行顺利,比如单纯按照代码行号顺序分析。
2)流敏感:基于CFG控制流分析。
3)路径敏感:在流敏感基础上,添加实际路径判断。
在静态层面上,一条程序执行路径可表现为程序代码中的语句序列。数据流分析的精确度在很大程度上取决于它分析的语句序列是否可以准确地表示程序实际运行的执行路径。在漏洞分析中,数据流分析根据对程序路径的分析精度可分为流不敏感(flow insensitive)的分析,流敏感(flow sensitive)的分析,路径敏感(path sensitive)的分析。流不敏感的分析不考虑语句的先后顺序,往往按照程序语句的物理位置从上往下顺序分析每一语句,忽略程序中存在的分支。本质上,流不敏感的分析是一种很不准确的做法,所得到的分析结果精确度不高,但由于分析过程简单且分析速度快,在一些简单的漏洞分析工具中仍然采用了流不敏感的分析方式,如Cqual。流敏感的分析考虑程序语句可能的执行顺序,通常需要利用程序的控制流图(Control Flow Graph,CFG),根据分析的方向可以分为正向分析和逆向分析。路径敏感的分析不仅考虑语句的先后顺序,还对程序执行路径条件加以判断,以确定分析使用的语句序列是否对应一条可实际运行的程序执行路径。成熟的漏洞分析工具中所采用的数据流分析往往采用流敏感或路径敏感的分析方式。
(2)基本原理
采用数据流分析技术检测程序漏洞的原理如图4.1所示。为了对程序进行数据流分析,首先使用词法分析、语法分析、控制流分析以及其他的程序分析技术对代码进行建模,将程序代码转换为抽象语法树(Abstract Syntax Tree,AST)、三地址码(Three Address Code,TAC)等关键的代码中间表示,并获得程序的控制流图、调用图等数据结构。漏洞分析规则描述对程序变量的性质、状态或者取值进行分析的方法,并指出程序存在漏洞情况下的变量的性质、状态或取值。数据流分析在分析变量的性质或状态时,通常使用状态机模型,而在分析变量的取值时,则应用相应的和变量取值相关的分析规则。漏洞分析规则通常是给予对历史漏洞总结或者一些安全编码的规定,数据流分析过程根据漏洞分析规则在程序代码模型上对所关心的变量进行跟踪并分析其性质、状态或者取值,通过静态地检查是否违反安全的编码规定或者符合漏洞存在的条件,进而发现程序中的漏洞。
图4.1 基于数据流的源代码漏洞分析一般原理
1)代码建模
漏洞分析系统在代码建模过程中应用一系列的程序分析技术获得程序代码模型。如果分析的对象是程序的源代码,可以通过词法分析对程序源代码进行初步的解析,生成词素的序列。之后使用语法分析程序的语法结构,将词素序列组合成抽象语法树。如果分析系统需要应用三地址码进行程序的分析,则利用中间代码生成过程解析抽象语法树而生成三地址码。若分析系统使用流敏感或者路径敏感的方式进行漏洞分析,则可以通过分析抽象语法树获得程序的控制流图。构造控制流图的过程是过程内的控制流分析过程。控制流分析还包括分析各个过程之间的调用关系的部分。通过分析过程之间的调用关系,分析系统可以构造程序的调用图。
2)程序代码建模
为满足分析程序代码中语句或者指令的语义的需要,漏洞分析系统通常将程序代码解析为抽象语法树或者三地址码等中间表示形式。树形结构的抽象语法树和线性的三地址码都能简洁地描述程序代码的语义。为了保证分析的精度,数据流分析一般采用流敏感或者路径敏感的方法分析数据的流向,这就使得在分析过程中既需要识别程序语句本身的操作,还需要识别程序的控制流路径。控制流图描述了过程内程序的控制流路径,较为准确的数据流分析通常利用控制流图分析程序执行路径上的某些行为。然而,一般情况下程序是由多个过程(函数或方法)组成,对某个变量的跟踪需要跨越过程的代码,这就需要识别程序中过程之间的调用关系。调用者描述了过程之间的调用关系,是过程间分析需要用到的程序结构。
3)漏洞分析规则
漏洞分析规则是检测程序漏洞的一步。漏洞分析规则描述“当分析到某个程序的某个指令语义时,漏洞分析系统该作出的处理”,例如在分析指针使用错误时,当遇到指针变量的释放操作时,漏洞分析系统记录该指针变量已被释放,而这样的操作由漏洞分析规则所指定。对于分析变量状态的规则,可以使用状态自动机来描述。状态自动机描述变量的状态转换方式,并且给出变量在何种状态下程序是不安全的。对于需要分析变量取值的情况,漏洞分析规则指出的是应该怎样记录变量的取值,以及在怎样的情况下对变量的取值进行何种的检查。例如在检测缓冲区溢出漏洞时,对于声明语句int a[10],记录数组a的大小是10;对于函数调用语句strcpy(x,y),则比较变量x被分配空间的大小和变量y的长度,当前者小于后者时,判断程序存在缓冲区溢出漏洞。
4)静态漏洞分析
数据流分析将程序代码模型作为分析对象,将漏洞分析规则作为检测程序漏洞的依据,数据流分析可以看作一个遍历程序代码进行规则匹配的过程。遍历程序代码可以有多种方式,如流不敏感方式、流敏感方式、路径敏感方式等,但无论使用哪种方式,由于数据流分析需要进行检查规则的匹配,分析程序语句的指令语义是必不可少的过程。数据流分析过程相当于对程序代码中的语句解释执行的过程,其解释执行程序代码的过程也可看作根据检测规则在程序的可执行路径上跟踪变量的状态或者变量取值的过程。
在一般情况下,分析过程对变量的取值使用一定形式的抽象表述,而并不一定需要计算具体的取值。
在数据流分析过程中,如果待分析的程序语句是程序调用语句,需要进行过程间的分析,以分析被调用函数的内部代码。过程间的分析需要利用程序调用图确定被调用的函数。函数的调用是一种特殊形式的控制流。如进行过程间的分析,分析的语句序列不只包括一个函数内的程序语句,还要包含其调用函数的内部的程序语句。过程间的分析由于需要分析更多的程序语句,并且需要分析函数调用这种特殊形式的控制流,相对过程内的分析更加复杂。但由于分析范围的扩大,过程间的分析可能分析到更多的变量的状态和取值,因此可能发现更多的程序漏洞。
5)结果处理
为保证漏洞分析结果的准确性,使用数据流分析方法检测程序漏洞得到分析结果常常需要经过进一步分析处理。为追求分析效率,漏洞分析工具应同时应用多个检测规则对程序代码进行检测,而每个检测规则对程序漏洞危害程度是不同的,因此需要依据漏洞危害程度进行分类。此外,每个分析过程得到的程序漏洞的精确程度也可能不同,这是由于分析中总会使用一些近似分析的情况,如忽略一些路径条件,因此需要对结果的可靠性进行分析。
(3)举例分析
注:kfree是释放指针指向的内存空间。
1)数据流分析过程
首先,针对各函数进行词法语法分析,生成AST,获取内部控制流,如图4.2所示。
图4.2 控制流图
其次,分析程序调用关系,如图4.3所示。
图4.3 指针变量的错误使用示例程序调用图
2)漏洞规则
在分析函数contrived_caller()前,假定函数的参数w、x和p都被分配了空间,变量w和p的状态是start。由于变量x不是指针变量,不用记录它的状态。w和p处于start状态将作为函数contrived_caller()的前置条件。具体的分析过程如下。
在BB1中,认为BB1的前置条件和函数contrived_caller()的前置条件是相同的。变量p被释放,它的状态变为free。变量p、w和x作为函数contrived()的参数,函数contrived()被调用,我们将分析函数contrived()的代码。
首先,将函数contrived()的前置条件记为p,处于free状态,w处于start状态。BB1的前置条件和函数contrived()的前置条件是相同的,BB1未改变变量的状态,它的后置条件和前置条件相同。
然后,我们分析BB2。在BB2中,BB2的前置条件为P处于free状态,w处于start状态。变量w被释放,将w的状态记为free。变量p的值赋给q,将q的状态记为free。基本块的后置条件为p、q和w处于free状态。
对于BB3,变量的状态未发生变化,其前置条件及后置条件和BB2的后置条件是一样的。但是通过分析路径条件,可以发现不存在BB2到BB3,再到BB4这样的路径。关于路径条件的分析,将在符号执行中介绍。
在BB5中,BB5的前置条件为p、q和w处于free状态。对于语句ql=*q,根据分析规则将变量q的状态记为use After Free,并报告程序使用了已经释放的指针。
BB6为函数contrived()的出口,对返回BB1的另一个后继分支BB3继续分析,BB1的后置条件为P处于free状态,w处于start状态。BB3的已经记录了的前置条件为p、w处于free状态,和BB1的后置条件不同,记录BB3的第二个前置条件为p处于free状态,w处于start状态并继续分析。
对于BB4,变量的状态未发生变化。通过分析路径条件,发现BB1到BB3,再到BB5为不可能的路径,因此将继续分析BB4。BB4的前置条件和后置条件都为P处于free状态,w处于start状态。
随后可以将BB4和BB5的后置条件作为函数contrived()的后置条件。此时函数contrived()的后置条件为p、w处于free状态,q处于use After Free状态或者w处于start状态,p处于free状态。在此,返回到函数contrived_caller()继续分析。
对于函数contrived_caller()的BB2,其前置条件为p处于use After Free状态,w处于free状态,或者p处于free状态,w处于start状态。分别根据BB2的两个不同的前置条件进行分析,如果选择前置条件中的前者,通过语句w1=*w,将变量w的状态记为use After Free,并报告程序使用了已经释放的指针。如果选择前置条件中的后者,程序在语句w1=*w处是安全的。
综上,可以发现代码的第10行和第17行可能出现使用已经释放的指针变量指向的内容的情况。
(4)典型工具:Fortify SCA
Fortify是一家专注于安全漏洞分析的技术公司,2010年8月已经被惠普公司收购,成为该公司的一个部门。Fortify所推出的静态代码分析器Fortify SCA(Static Code Analyzer)是一个软件源代码缺陷静态测试工具。它通过分析应用程序可能会执行的所有路径,从源代码层面上识别软件的漏洞,并对识别处的漏洞提供完整的分析报告。
Fortify SCA首先解析目标代码文件或文件夹,将其转化成中间表达形式,然后通过内置的5种分析引擎(数据流引擎、语义引擎、结构引擎、控制流引擎、配置引擎)及由Fortify SCA分析规则库提供的分析规则对中间表达形式进行静态分析,从而将目标程序中可能存在的安全漏洞挖掘出来,并通过审计工作台对检测报告的漏洞进行排序、过滤、组织等处理,向用户报告检测结果。
Fortify SCA由以下4个部分组成。
1)Fortify前端。负责对目标程序设计语言编制的程序代码文件或文件夹进行解析(词法分析、语法分析等),将其转换为Fortify SCA分析引擎能够处理的数据结构。为了提高分析引擎的灵活性和整个系统的可扩展性,Fortify专门设计了一种语言独立的中间表达形式NST(Normal Syntax Tree),不同语言编制的程序代码都将由前端转换为NST的形式。这种处理大大方便了对分析引擎的开发和维护,使得Fortify SCA用同一套分析引擎即可处理不同语言编制的程序代码。Fortify前端主要使用编译中的词法分析、语法分析、语义分析和控制流分析等技术,处理过程类似于编译过程。目前Fortify SCA支持的程序语言主要包括C/C++、C#、Java、JSP、Cold Fusion、PL/SQL、T-SOL、XML、ASP.NET、VB.NET以及其他.NET语言。
2)分析引擎。ForitfySCA主要包含有五大分析引擎。①数据流引擎。使用已获得专利的X-Tier Dataflmv TM数据流分析器跟踪指定的数据,记录并分析程序中的数据传递过程所产生的安全问题。数据流分析是Fortify SCA发现程序漏洞的主要手段,通过较为全面地遍历程序的可能执行路径,比对检测规则查找程序漏洞,数据流分析需要控制流分析的支持,过程间的数据流分析也是Fortify SCA查找程序漏洞的关键部分。数据流引擎使用污点传播跟踪技术,查找隐私数据使用不当等安全问题。②语义引擎。分析程序如何使用不安全的函数或过程,找出与这些函数或过程相应的上下文,查找其中可能存在的安全问题。③结构引擎。从程序代码结构上查找可能存在的漏洞和问题。④控制流引擎。分析程序在特定时间、特定状态下可能执行的操作序列,识别可能存在问题的代码结构(如程序中的死代码)。Fortify SCA控制流分析包括过程间的控制流分析算法,面向整个目标程序代码。⑤配置引擎。分析程序配置和程序代码的联系,查找和程序配置相关的可能存在的安全问题(如敏感信息泄露,配置缺失等)。其中,数据流引擎和控制流引擎是Fortify SCA最主要的两个分析引擎。分析引擎将漏洞分析结果写入FPR(Fortify Project Result)文件,由于部分漏洞的分析过程使用符号执行判断漏洞是否可能存在,分析结果中也包含了漏洞存在可能性方面的信息,后续的审计处理使用FPR文件。
3)分析规则库。Fortify SCA的分析引擎必须根据一定的分析规则来对程序进行静态漏洞分析。分析过程本质上类似于一个模式匹配过程,分析规则库就对应着各种代码安全漏洞模式。Fortify SCA自带的规则库的分类与CWE数据库类似,Fortify SCA产品系统中默认配置了大量的安全漏洞模式,可以满足一般性的漏洞分析需求。此外,用户也可在Fortify SCA中通过规则编辑器自定义分析规则,可按照待分析目标程序代码的特定安全需求进行漏洞分析。
Fortify SCA的工作原理如图4.4所示。
图4.4 Fortify SCA工作原理
Fortify SCA中通过规则编辑器自定义分析规则:
4)审计工作台或控制管理界面。其负责驱动整个分析过程,对从分析引擎得到的检测结果(FPR文件)进行处理,包括对检测结果的排序、过滤、组织等,提供结果查看和输出的界面。
使用Fortify SCA对目标代码做静态分析主要分为两个步骤。
①选择扫描对象,例如Java程序需要classpath和指定目标文件或文件夹,C/C++项目需要makefile等。运行扫描,生成FPR文件。
②通过审计工作台或Fortify管理平台处理FPR文件,查看漏洞分析结果。
Fortify SCA审计工作台提供图形界面,用户可根据提示完成上述操作。对于审查漏洞分析的结果,用户可以使用图形界面通过单击操作找到包含漏洞报告的代码的位置。所有的检测结果可以导出为PDF、HTML、XML和RTF格式的文件。
2.污点分析
(1)基本概念
追踪指定数据在程序中的流动,污点分析是一种跟踪并分析污点信息在程序中流动的技术,最早由Dorothy E.Denning于1976年提出。他的分析对象是污点信息流。污点或者污点信息在字面上的意思是受到污染的信息或者“脏”的信息。在程序分析中,常常将来自程序之外的,并且进入程序的信息当作污点信息,这时污点信息可以指程序接收的外部输入数据,也可以指程序捕捉到的来自外界的信号,如鼠标单击等。此外,根据分析的需要,程序内部使用数据也可作为污点信息,并分析其对应的信息的流向,例如在分析程序是否会将用户的隐私信息泄露到程序外时,我们将程序所使用的用户的隐私信息作为污点信息。
污点分析的过程常常包括以下几个部分:识别污点信息在程序中的产生点并对污点信息进行标记;利用特定的规则跟踪分析污点信息在程序中的传播过程;在一些关键的程序点检查关键的操作是否会受到污点信息的影响。一般情况下,将污点信息的产生点称为Source点,污点信息的检查点称为Sink点。相应的识别程序中Source点和Sink点的分析规则分别称为Source点规则和Sink点规则。Source点规则、Sink点规则以及污点信息的传播规则称为污点分析规则。
如图4.5所示是一个针对源代码的污点分析过程的示例。在这个示例中,将scanf所在的程序点作为Source点,将通过scanf接收的用户输入数据标记为污点信息,并且认为存放它的变量x是被污染的。如果在污点传播规则中规定“如果二元操作的操作数是污染的,那么二元操作的结果也是污染的”,那么对于语句“y=x+k”,由于x是污染的,所以y也被认为是污染的。一个被污染的变量如果被赋值为一个常数,它将被认为是未污染的。例如图4.5中的赋值语句“x=0;”,将x从污染状态转变为未污染。循环语句whlie所在的程序点在这里被认为是一个Sink点,如果污点分析规则规定“循环的次数不能受程序输入的控制”,那么在这里就需要检查变量y是否是被污染的。
图4.5描述的污点分析本质上是一个跟踪输入数据流向的过程。在这种情况下,污点分析的过程和数据流分析技术分析数据流向的过程是相似的。在实际污点分析过程中,常常只关心污点信息通过数据传递的过程,将分析的对象定位于污点数据。以上分析过程也可看作是对程序中部分数据依赖关系的分析,即在程序中找到依赖于污点数据的其他相关数据。然而污点信息不仅可以通过数据依赖传播,还可以通过如图4.5污点分析过程示例中的控制依赖传播。例如在图4.5所示的这段代码中,变量y的取值控制依赖于变量x的取值。如果在污点分析中,变量x是被污染的,考虑到信息在控制依赖上的传播,变量y也应该是污染的。
图4.5 污点分析过程实例
将污点分析用于程序漏洞的挖掘中,即将程序是否存在某种漏洞的问题转化为污点信息是否会被Sink点上的操作所使用的问题。通常,人们将使用污点分析可以检测的程序漏洞称为污点类型的漏洞,如SQL注入漏洞、跨站脚本漏洞以及命令注入漏洞等。
(2)基本原理
使用静态污点分析技术挖掘程序漏洞的系统的工作原理如图4.6所示。为了对程序进行污点分析,首先需要将程序代码转化为污点分析所使用的程序代码模型。程序代码模型包括抽象语法树、三地址码、控制流图、调用图、程序依赖图等结构。代码模型通过词法分析、语法分析、中间代码生成、控制流分析以及程序依赖关系分析等建模过程进行构建。另外,为应用污点分析挖掘程序漏洞,一般将漏洞分析规则用污点分析规则进行表示,包括Source点规则、Sink点规则以及污点信息传播规则。静态漏洞分析过程将程序代码模型以及漏洞分析规则作为输入,利用污点分析技术分析Source点的污点信息是否会影响Sink点处敏感操作所使用的关键数据,进而判断程序是否存在污点类型的程序漏洞,并给出初步的漏洞分析结果。
图4.6 使用污点分析检测程序漏洞的工作原理
(3)举例说明
在漏洞分析中,人们通常使用污点分析技术挖掘一些特定类型的程序漏洞。这些类型的程序漏洞可以表现为来自程序之外的或者程序中重要的数据或信息,在未经足够的验证或数据转化的情况下,被传播到关键的程序点上并且被一些重要的操作所使用。以SQL注入漏洞为例,程序未对来自外部输入的数据进行足够的验证或数据转化,而这些数据会被用来构造SQL操作语句。因此,攻击者可以对这些外部输入数据进行构造并将其输入到程序中,进而构造特定SQL操作语句,以执行某些恶意SQL操作。例如如下所示的代码,代码的意图是对用户的用户名和密码进行验证,通过执行SQL语句查询用户的信息是否存在于数据库中,并给出用户登录成功与否的信息。如果攻击者构造的用户名是“'or 1=1--”,SQL语句的查询结果不为空,攻击者将在未输入正确信息的情况下,得到登录成功的信息。
在使用污点分析技术对上述类型的程序漏洞进行检查时,将所有来自程序之外的数据标记为污染的,通过分析受到污染的数据是否会影响到程序中关键操作所使用的关键数据,以判断程序是否存在这些类型的漏洞。对于如下所示的代码示例,在污点分析时,将来自程序之外数据的变量user和pass标记为污染的。由于变量sqlQuery的取值受到变量user和变量pass的影响,因此也将变量sql Query标记为污染的。在后续代码中,程序将变量sql Query作为参数构造SQL操作语句,并且变量sqlQuery仍然被标记为污染的,据此可以判定程序存在SQL注入漏洞。
(4)典型工具:TAJ
TAJ(Taint Analysis for Java)是由IBM公司的Omer Tripp等人开发并实现在WALA工具上的针对Java语言的Web应用程序污点分析工具。通过使用静态污点分析技术跟踪程序中的信息流,TAJ可以检测可能存在于Java Web应用程序中的跨站脚本漏洞、SQL注入漏洞等污点类型的程序漏洞。TAJ也可用于分析其他污点相关的程序问题,例如Java程序中的服务端的恶意文件执行问题、应用程序的隐私泄露问题以及异常处理不当问题。
TAJ在Java分析工具WALA上实现它的分析过程,它分析的对象是Java的字节码。在解析Java字节码之后,TAJ首先对待分析的Java程序使用经典的指向分析算法,并构建程序的调用图,同时保留指向分析过程中得到的指向关系。TAJ支持多种指向分析算法,为求分析精确和有效,TAJ采用上下文敏感的指向分析。在指向分析的过程中,TAJ考虑到Java中容器对分析结果的影响。此外TAJ还在指向分析的过程中识别程序中的一些关键的API,这些API所在的程序点将被作为污点分析的Source点或Sink点。
TAJ的调用图的构建过程包括对反射机制调用的处理。TAJ通过静态地分析反射机制相关的字符串的取值,确定和反射机制相关的方法的调用。
此外,TAJ在分析程序的控制流结构的过程中,考虑了Java程序中的异常处理。由于利用异常处理,污点信息可以有效地传播,TAJ在污点的分析过程中,考虑到异常处理的参数可能是污染的。TAJ的工作过程如图4.7所示。
图4.7 TAJ的工作过程
在指向分析之后,TAJ利用污点分析检查程序是否存在污点类型的漏洞以及其他的污点相关的安全问题。TAJ的污点分析过程可以看作是一个切片过程。TAJ使用正向的切片算法,从Source点开始,根据数据依赖关系对程序代码进行切片分析,找到和污点相关的程序语句或指令,利用切片结果,判断Source点处的污点信息是否会传播到Sink点。程序切片作为判断程序是否存在污点相关的安全问题的依据。
TAJ的切片分析基于一种混合形式的程序依赖图。这种混合依赖图包含两种形式的边:一种边表示直接的数据依赖关系,如一个变量的使用依赖于该变量的赋值;另一种边可以被看作表示基于摘要的数据依赖关系,例如程序中不同方法所使用的两个局部变量,或者某个对象的实例域和某个局部变量等。构造这样的边需要利用指向分析得到指向关系。
根据混合形式的程序依赖图,TAJ在正向切片算法中同时考虑和变量相关以及和堆相关的数据依赖。这样的分析可以看作是一种流敏感且上下文敏感的对变量之间数据传播的分析,以及一种上下文敏感的对虚拟堆和变量之间的数据传播的分析。在分析变量之间的数据传播时,TAJ根据数据依赖关系,准确地找到相关的语句,但是在分析和虚拟堆相关的数据传播中,切片分析的精度将由前面的指向分析过程决定。
TAJ在切片的分析中忽略了程序中的控制依赖,即它所使用的混合依赖图不包括控制依赖边,它的切片的过程也不将控制依赖相关的语句找出来。TAJ在污点分析中忽略了控制依赖对于污点信息传播的影响。虽然基于控制依赖的隐式信息流可以有效地用于传播污点信息,但是通过隐式信息流对程序中污点类型漏洞的利用常常是极其复杂的,相对的,利用基于数据依赖的显式信息流更容易达到利用漏洞的目的。由于这样的原因,TAJ在分析时仅关注显式信息流。
怎样处理程序中普遍使用的库方法是分析Java程序时需要解决的问题。TAJ主要使用摘要对库方法进行处理,而不去分析方法的实现代码,将其看作原子操作。这个摘要既用于污点分析的切片分析过程,也用于指向分析过程。容器相关的操作无论是对污点分析,还是对指向分析都可能会有影响,TAJ的污点分析考虑通过容器传播的污点信息流,在分析容器相关的操作时,TAJ主要利用相关的方法的摘要。
为适用于对较大规模的程序的分析,TAJ在分析的过程中加入了一些近似分析的方案。当应用程序规模很大,用户可能需要在较短的时间内得到更为主要的分析结果,为应对这样的情况,TAJ使用一个优先级处理策略,和污点分析直接相关的分析将会被优先执行,例如在指向分析的过程中,包含Source点和Sink点的方法将优先得到处理。此外,对于大规模程序,检测系统的存储空间可能受到一定的限制。通常,指向分析需要大量的存储空间存储分析的局部结果,TAJ使用优先级处理策略,优先保留和污点分析相关的局部分析结果。
3.符号执行
(1)基本概念
符号执行是20世纪70年代提出的一种使用符号值代替具体值执行程序的技术,最先用于软件测试。符号是表示取值集合的记号。使用符号执行分析程序时,对于某个表示程序输入的变量,通常使用符号表示它的取值,该符号可以表示程序在此处接收的所有可能的输入。此外,在符号执行的分析过程中针对那些不易或者无法确定取值的变量也常常使用符号表示的方式进行分析。
符号执行的分析过程大致如下:首先将程序中的一些需要关注但又不能直接确定取值的变量用符号表示其取值,然后通过逐步分析程序可能的执行流程,将程序中变量的取值表示为符号和常量的计算表达式。程序的正常执行和符号执行的主要区别是:正常执行时,程序中的变量可以被看作被赋予了具体的值,而符号执行时,变量的值既可以是具体的值,也可以是符号和常量的运算表达式。
每一个符号执行的路径都是一个“true”和“false”组成的序列,其中第i个“true”(或“false”)表示在该路径的执行中遇到的第i个条件语句。一个程序所有的执行路径可以用执行树(Execution Tree)表示,例如,在下列代码中有三条执行路径,这两条路径组成了如图4.8所示的执行树。这些路径可以分别被输入{x=0,y=1},{x=2,y=1}和{x=30,y=15}触发。下面的一个简单例子可以大致应用符号执行对程序进行分析,其执行树如图4.8所示。
图4.8 执行树
符号执行中维护了符号状态σ和符号路径约束PC,其中σ表示变量到符号表达式的映射,PC是符号表示的不含量词的一阶表达式。在符号执行的初始化阶段,σ被初始化为空映射,而PC被初始化为true,这两者在符号执行的过程中不断变化。在对程序的某一路径分支进行符号执行的终点,把PC输入约束求解器以获得求解。如果程序把生成的具体值作为输入执行,它将会和符号执行运行在同一路径,并且以同一种方式结束。例如,在上面示例代码中,程序开始时,σ为空,PC为true,执行完第16和17行之后,σ=x→x 0,y→y 0。这其中x 0和y 0是初始化的不受约束的符号值,当执行到第6行的时候,σ=x→x 0,y→y 0,z→2y 0。在执行到每一个条件语句if(e)then{}else{}的时候,PC被更新为PC=PC∧σ(e)(then分支)或PC′=PC∧σ(e)(else分支)。对于某些指定的具体值,如果能够使PC成立,那么继续执行then分支,反之亦然。如果符号执行实例遇到了程序终止或者错误(如程序崩溃或违反断言),目前符号执行的实例终止,并利用已有的约束求解器生成满足目前路径约束的值。
(2)基本原理
使用符号执行进行程序漏洞分析的工作原理如图4.9所示。与数据流分析类似,使用符号执行技术进行漏洞分析的系统,常常使用抽象语法树、三地址码、控制流图、调用图等结构作为程序代码的模型。而代码建模过程通常包括词法分析、语法分析、中间代码生成和控制流分析等过程。基于符号执行技术分析变量取值的特点,漏洞分析规则常常包括符号的标记规则以及一些在特定情况下变量取值的约束。静态漏洞分析过程将程序代码模型以及漏洞分析规则作为输入,通过使用符号执行以及约束求解等分析技术查找程序中可能存在的漏洞,并将初步的分析结果传递给处理分析结果过程。初步的分析结果经过进一步的处理形成最终的漏洞报告。
图4.9 使用符号执行检测程序漏洞的工作原理
以下介绍两种符号执行的方法:正向与反向。
在使用符号执行技术时,常常使用沿着程序路径分析的方式,将变量表示为符号和常量组成的计算表达式,同时分析路径条件或者漏洞存在的条件等约束。对于实际检测程序漏洞,通常关注程序中可能引起程序异常的操作,例如,数组的赋值或者C语言中的strcpy()函数调用等类似操作所在的程序点称为漏洞检查点。此外,还可以从漏洞检查点出发,逆向分析程序是否存在一条使漏洞的存在条件可满足的程序路径。在这里将第一种检测程序漏洞的方法称为正向的符号执行,而后者称为逆向的符号分析。
1)正向的符号执行
正向的符号执行从某个分析的起始点开始,通过正向遍历程序的路径的方式进行程序漏洞的分析。其分析的起始点可以是程序入口点、程序中某个过程的起始点或者某个特定的程序点。在应用正向的符号执行检测程序漏洞时,通过分析路径上的程序语句,不断地将变量的取值表示为符号和常量的表达式,将路径条件表示为符号的约束,同时对符号在程序路径上需要满足的取值约束进行求解,判断路径是否可行,并且在特定的程序点上检查变量的取值是否一定符合程序安全的规定或者可能满足漏洞存在的条件。
2)逆向的符号分析
符号执行通常是沿着程序路径对程序进行分析,遇到可能引起程序漏洞的程序点,则分析该程序点处是否存在安全问题。然而,这样的分析过程缺乏漏洞分析的针对性。而逆向的符号执行分析可以弥补这种不足,有的放矢地进行漏洞分析。在应用逆向的符号分析方法检测程序漏洞时,通过直接在关键的程序点上分析所关心的变量是否可以满足存在程序漏洞的约束条件,并且通过逆向分析不断地获取路径条件对所关心变量的取值约束,并计算所关心的变量在当前的约束下是否还满足漏洞的约束条件,进而判断程序漏洞是否真实存在。分析过程常常在发现所关心变量的取值不再满足漏洞存在的条件或者分析到达程序的入口点时终止。
(3)举例说明
对于形如array[x]的数组声明形式,分析过程记录数组的长度为x,其中x为常量。对于形如array[i]的数组元素访问形式,分析规则规定数组下标的取值在0到数组长度之间。
首先对以上代码片段进行基本解析,对代码中出现的宏进行数值替换,将代码第5行数组声明中的“ISDN_MAX_DRIVERS”替换为32,将代码第18行for循环语句中的“ISDN_MAX_CHANNELS”替换为64。
该代码段包含了两个程序声明的函数“get_drv_by_nr()”和“get_slot_by_minor()”。其中,函数“get_slot_by_minor()”在第19行调用了函数“get_drv_by_nr()”,函数“get_drv_by_nr()”在第11行调用了函数“spin_lock_irqsave()”。程序的部分调用图如图4.10所示。
图4.10 程序部分调用图
1)正向分析法
如果使用自底向上的分析调用图的方法,那么对于图4.10所示的调用图情况,首先分析函数spin_lock_irqsave(),这里由于没有列出它的实现代码,默认为对其的分析已经完成。之后需要分析函数get_drv_by_nr(),分析过程如下。
首先将函数get_drv_by_nr()的参数di作为符号处理,这里用符号a不对其取值。
代码第7行和第8行声明了两个变量,但未对其赋值,这里不对其进行处理。
第9行if条件语句对变量di加以限制,在时执行后面的代码,
是退出函数。这里记录
时,函数返回空。然后遍历语句的false分支。
第11行函数调用使用摘要对其分析,这里的分析略去。
第12行数组访问操作,是程序的检查点,根据分析规则,将a的取值范围限定在0到数组drivers的长度之间。数组drivers的长度是32,所以有。结合路径条件,有符号a的取值约束为
。化简得到
。这时生成摘要
,程序是安全的。
当函数spin_jock_irqsave()分析完成后需要将符号a替换为参数di。这里摘要为时,函数返回空,
时,程序是安全的。
之后分析到函数gel_slot_by_minor()时,在第18行循环变量di的范围是0~64,这里记录。第19行函数spin_jock_irqsave()被调用,通过分析其摘要,参数di在
时程序是安全的,
时程序存在漏洞,而此时有
,利用约束求解器求解约束
64∧di≥32,di为32时,满足约束条件,这就说明程序存在漏洞。
2)反向分析法
这里从第12行开始分析,根据规则有变量di的约束,而
程序存在漏洞。
第11行和di无关,不对其进行分析。
第9行,补充路径条件di≥0,此时的约束为(di≥32∨di 0)∧di≥0,将其化简为di≥32时程序存在漏洞。
第8、7行和di无关,不对其进行分析。此时到达函数spin_jock_irqsave()的入口点,分析调用它的函数spin_jock_irqsave(),从第19行开始分析,此时函数spin_lock_irqsave()中di的约束为di≥32时,程序存在漏洞。
通过分析第18行,得到约束,利用约束求解器求解约束
,发现约束可满足,这时认为程序存在漏洞。
(4)典型工具Clang
Clang是一个开源工具,在苹果公司的赞助下进行开发,构建在LLVM编译器框架下。Clang能够分析和编译C、C++、Objective C、Objective C++等语言,该工具的源代码发布于BSD协议下。本质上,Clang不仅是一个静态分析工具,还是这些语言的一个轻量级编译器。与LLVM原来使用的GCC相比,Clang具有很多之前编译器所不具有的特性。
1)用户特性。第一,高速而低内存消耗,在某些平台上,Clang的编译速度显著快过GCC,而Clang的语法树占用的内存只有GCC的五分之一;第二,更清楚的诊断信息描述,Clang可以很好地收集表达式和语句信息,它不仅可以给出行号信息,还能高亮显示出现问题子表达式;第三,与GCC的兼容性,GCC支持一系列的扩展,Clang从实际出发也支持这些扩展。
2)应用特性。第一,基于库形式的架构,Clang是LLVM的子项目,自然而然地继承了LLVM的架构,在这种设计架构下,编译器前端的不同部分被分成独立的支持库,在提供了良好接口的前提下,可以使得新开发人员迅速开展工作;第二,支持不同的客户端,不同的客户端有着不同的需求,如代码生成不需要语法树,而重构需要一棵完整的语法树,简洁而清晰的API可以使得客户端决定这些高层策略;第三,与IDE集成,为了高性能的表现,需要增量编译、模糊语法分析等技术,所以IDE除了代码生成之外还需要相关信息,Clang会收集并生成这些一般编译器会丢掉的信息。
3)内部特性。Clang是一个实际产品级别的编译器,它实现了针对C、C++、Objective C、Objective C++的统一语法分析器,并顺应其变化。
作为GCC的替代品,它有着更适合进行静态分析工具开发的属性;第一,Clang的抽象语法树和设计让任何熟悉C语言工作机制或者编译器工作机制的开发人员容易理解;第二,Clang从开始就设计成API,源代码分析工具可以重用这些接口;第三,Clang可以将抽象语法树(Abstract Syntax Tree,AST)书写到磁盘上,再由另一个程序读入(这对程序的全局分析很有用),而GCC的PCH机制相比则很有限;第四,Clang可以提供非常清晰明确的诊断信息,而GCC的警告的表达力不够,可能会造成混淆。
1)Clang驱动器设计
为了实现与GCC的良好兼容,Clang 2.5实现了一个全新的驱动器,驱动器结构如图4.11所示。
图4.11 Clang静态驱动器结构
①参数解析。在这个阶段,命令行参数被分解成参数实例,因而驱动器需要理解所有的可用参数。参数实例是很轻量级的,仅仅包含足够的信息来确定对应的选项和选项值。参数实例一般不包含参数值,而是将所有的参数保存在一个参数队列中,其中包含原始参数字符串,这样每个参数实例只包含自身这个队列中的索引。在这个阶段后,命令行参数被分解成良好定义的选项对象,而之后的阶段不需要再处理任何字符串。
②编译任务构建。当参数解析完毕,编译序列所需要的后续任务树被建立。这一步确定输入文件、类型以及需要进行什么样的工作,并为每项任务建立一系列操作。最终得到一系列顶层操作,每个操作对应一个输出。经过这一步,编译流程被分成一组用来生成中间表示或最终输出的操作。
③工具绑定。这个阶段将操作树转化成一组实际子过程。从概念上说,驱动器将操作树指派给工具。一旦一个操作被绑定到某个工具,驱动器与工具进行交互从而确定各个工具之间如何连接以及工具是否支持集成预处理器之类的东西。驱动器与工具链交互从而实现工具绑定,每条工具链包含所有工具在特定体系结构、平台、操作系统下工作所需要的信息。因为与不同平台下的工具进行交互的需求,驱动器可能需要在一个编译中访问多条工具链。
④参数翻译。当一个工具被用来进行特定的操作(绑定),工具必须构造编译器中执行的实际工作。主要工作就是将GCC命令行形式的参数翻译成工具所需的形式。其中Arg List类提供了几种简单的方法来支持参数翻译。这个阶段得到的结果是一组执行工作(执行路径和参数字符串)。
⑤执行。Clang驱动器最后执行编译器任务流程。尽管有如一些选项“-time”“-pipe”会影响这个流程,但是这一过程一般来说是比较简单的。
通过这个驱动器,Clang可以根据参数来选择使用GCC或Clang-cc(Clang自己的编译器实体)。确定了所使用的编译器和相应参数之后,驱动器会fork出一个子进程,在子进程中通过exec函数族系统调用运行相应的编译器(GCC/Clang-cc)。
2)Clang静态分析器
Clang上的静态分析器是基于Clang的C/C++漏洞查找工具,现有的Clang静态分析器已经完成了过程内分析(intra-procedural analysis)和路径诊断(path diagnostics)两个大模块。其中,已实现的过程内分析功能包括源代码级别的控制流图、流敏感的数据流解析器、路径敏感数据流分析引擎、死存储检查和接口检查。而路径诊断信息模块已经提供路径诊断客户端(提供开发新bug报告的抽象接口、HTML诊断报告等)、缺陷报告器(为前一个模块服务)。
如图4.12所示,Clang的静态分析是按照如下思路实施的:根据参数构建消费者,然后在语法树分析的过程中使用这些消费者进行各种实际分析。
图4.12 Clang静态分析流程
具体的流程如下面几个部分所述。
①构建消费者。当传递参数“-analyze”给Clang-cc时,编译器会根据后续的参数建立相应消费者。例如当后续参数为“-warn-dead-stores”时,会产生一个AnalysisConsumer对象,并添加一个Action WarnDeadStores函数指针到对象中名为Function Actions的容器中;如果同时再传递一个后续参数“-warn-uninit-values”,同样只产生一个AnalysisConsumer对象,但是会再添加一个Action Warn Uninit Vals函数指针到Function Actions容器中。这些后续参数都以宏的形式在Analyses.def文件中进行定义。
②构建语法树上下文。在消费者构建完后,需要构建语法树的上下文信息。创建Void、Bool、Int等内建类型,放置到全局的类型列表中,接着为所有的内建标识符赋予唯一的ID,最后创建翻译单元定义体。
③解析语法树。在初始化工作完成后,建立词法分析器和语法分析器。然后,在语法分析器的初始化过程中用词法分析器进行词法分析,生成标记流。接着语法分析器从每个顶层定义体开始分析标记流(接口为Parse Top Level Ded),根据定义体的类型调用相应的处理机制,并从顶层定义体逐层向下进行分析。每当分析完一个顶层定义体,ParseTop LevelDed()函数返回了分析过的定义体,调用消费者的Handle Top Level Decl接口,进行所需要的分析。