没有理想的人不伤心

软件漏洞分析技术

2024/05/29
2
0

基于源代码的漏洞分析

数据流分析

根据对程序路径的分析精度:

  • 流不敏感的分析(flow insensitive):不考虑语句的先后顺序,往往按照程序语句的物理位置从上往下顺序分析每一语句,忽略程序中存在的分支。这种方法不精确,但是分析简单快速
  • 流敏感(flow sensitive)的分析:考虑程序语句可能的执行顺序,通常需要控制流图,根据分析方向,分为正向分析、逆向分析
  • 路径敏感(path sensitive)的分析:不仅考虑语句的先后顺序,还对程序执行路径条件加以判断。

根据分析程序路径的深度:

  • 过程内分析(intra-procedure analysis):只针对函数内的代码
  • 过程间分析(inter-procedure analysis):需要考虑函数之间的数据流,分为上下文不敏感的分析和上下文敏感的分析。

代码建模:

源代码–(词法分析)–>词素序列–(语法分析)–>抽象语法树

词法分析读入组成源程序的字符流,并将他们组成有意义的词素(lexeme)序列,对每个词素,词法分析器产生一个词法单元,每个词法单元对应一个词素。词法单元描述抽象符号和符号表的对应,例如<id,3>表示一个变量并且变量名在相应的符号表的序号是 3,而<+>表示一个加法运算符号。

语法分析使用由词法分析器生成的各个词法单元的第一个分量来创建抽象语法树。

利用中间代码生成过程解析 AST 可以生成三地址码(three address code,TAC)

或者分析 AST 来获得控制流图CFG,再进行流敏感或路径敏感分析。

那么通过分析 TAC 来获得 CFG 如何呢?

通过分析过程间的调用关系,还可以构造程序的调用图

控制流图一般是过程内的控制流分析,而在过程之间的调用关系,一般需要使用调用图(call graph,CG)或者过程间控制流图(intra-procedure CFG,ICFG)

程序的中间表示 IR 通常有:AST、TAC、SSA(static single assignment,静态单赋值),IR 可用于程序指令的语义分析。

  1. 抽象语法树 AST(abstract syntax tree)

是程序抽象语法结构的树状表现形式。

每个内部节点代表一个运算符,这个节点的子节点代表该运算符的运算分量

AST 描述了程序语句和句中表达式的语法结构,通过描述控制转移语句的语法结构,在一定程度上也描述了程序的过程内代码的控制流结构。

AST 经过语义分析得到带有语义信息的抽象语法树(称为 decorated AST),其在节点的表示中加入一定的程序语义,更有利于分析指令语义。

  1. 三地址码 TAC(three address code)

类似于汇编语言的指令组成,每个指令具有不多于三个的运算分量,每个三地址码赋值指令右侧,最多有一个运算符。

相比于 AST,TAC 更加简单明了的表示了程序语句的指令语义。

  1. 静态单赋值 SSA(static single assignment)

通常指静态单赋值形式的三地址码,除此之外 AST 或源代码也可以表示为 SSA 形式。

SSA 中,所有赋值都是针对具有不同名字的变量,也就是说,若某变量在不同程序点被赋值,那么在不同程序点就应该使用不同的变量名。通常使用下标来区分不同程序点的统一变量。

在控制流的交汇处,SSA 使用 fai 函数将变量的赋值合并起来。

1722686697458-dfcc5317-9a5d-46f8-9f32-ea686d373256.png

通过 SSA 能够很轻易的分析变量在哪里赋值在哪里使用,有利于通过数据流分析来获取变量的状态。缺点是变量数量大大增加。

如下面代码转换为 SSA:

var = malloc(sizeof(type));
free(var);
var = malloc(sizeof(type));
free(var);

# 转换为 SSA:
var1 = malloc(sizeof(type));
free(var1);
var2 = malloc(sizeof(type));
free(var2);

在上述 SSA 中,就不会出现已经释放的变量重新分配空间这样的情况。

  1. 控制流图 CFG

CFG 一般指的是过程内的控制流的有向图,对于存在多个过程的程序,每个过程都用一个 CFG 表示。

涉及过程间调用的 CFG 称为过程间控制流图 ICFG,是 CFG 和调用图 CG 的混合表现形式。

通常情况下,CFG 的节点是由三地址码构成的基本块(basic block,BB)。

  1. 调用图 CG(call graph)

描述程序中过程之间的调用于被调用关系的有向图。

CG 构建的原则:

  • 程序中的每个过程都有一个节点
  • 每个调用点(call site)都有一个节点,调用点指的是程序中调用某个过程的一个程序点
  • 调用点 c 调用了过程 p,那么就存在一条从节点 c 到节点 p 的边

如下面的程序

void func1() {
    func2();//调用点 1
    func3();//调用点 2
}
void func2() {
    func3();//调用点 3
}
void func3() {
    func3();//调用点 4
}

1723108930277-0b54e270-162b-496c-b663-161f2f220c91.png

func1 也调用了 func2,为啥在简化调用图中没有这条边,难道 func2 调用 func3,把这条边优化了?

一般情况下,都是用简化调用图来描述程序调用结构

利用编译器来实现完成代码解析的前端对于漏洞分析系统的设计是一个较好的选择,即使用编译器来生成分析所需的数据结构

解释型语言或脚本语言(如 python、JS、PHP 等)没有相应的编译器实现对程序代码的基本解析,代码直接被解释器解释执行。对于这些语言要完成代码解析的各个部分

对 JAVA 程序的漏洞分析通常分析其字节码,也就是 .class 字节码文件,因为字节码可以很好的反汇编成 Java 虚拟机的指令,而这种指令简洁明了,便于分析;且 Java 程序通常要调用库方法,而记录库方法的文件是 .class 文件,为了分析目标的一致性,所有使用字节码为分析目标

过程内控制流图 CFG 的构建:

  • AST 作为中间表示:在 AST 中直接增加控制流的边,或重组 AST 来构建 CFG
  • TAC 作为中间表示:分析其中控制转移语句构建 CFG。这种情况的构建请参考静态程序分析

过程调用图 CG 的构建:

  • 对于直接过程调用的程序:每个调用的调用目标可以直接确定,如 C 语言编写的某些程序若未使用函数指针,则仅仅通过函数签名的分析就可以静态确定某个调用点调用的函数。
  • 程序使用了过程参数或函数指针:一般需要在运行时才能准确知道其调用目标,通过静态分析只能得到一个近似的估计。因为对于该类程序不同的运行状态,调用目标可能会不同,这时在静态分析中,往往会将该调用点连接到调用图的多个过程
  • 对于面向对象的语言如 Java 来说,间接调用是常用的调用方式,特别是当存在多个子类对父类的方法进行重写(override)时,某个调用点上使用的方法可能时多个不同方法中的任意一个。因此调用方法的确定在早期使用简单的流不敏感的上下文不敏感的分析方法来分析变量的类型,如类层级结构分析(class hierarchy analysis,CHA)、快速类型分析(rapid type analysis)。较为精确的构建方法:进行上下文敏感的指向分析,通过分析程序中变量指向的对象进而得到变量的类型,以此构建调用图,并利用已有的调用图做进一步的上下文敏感的分析。