代码分析属性图 CPG 介绍

代码分析工具层出不穷,编译器本身也内置了大量的静态检查,但分析的数据源较为单一。而本文要介绍的,是集众之所长的代码分析属性图 CPG,在漏洞分析上很有帮助。

1、定义

代码属性图(code property graph,简称 CPG) 是一种数据结构,用来通过 DSL(domain-specific language) 查询语句来挖掘代码漏洞。

它的主要思想如下:

  • CPG 将多个程序表示(program representations)整合成一个
  • CPG 数据被存储在图数据库中
  • 通过 DSL 在图数据库中遍历和查询 CPG 数据

2、用处

CPG 整合了 AST(abstract syntax trees)、CFG(control flow graphs)、PDG(program dependence graphs) 到一种数据结构当中。

这种综合的数据表示,使得我们在图遍历当中,可以优雅地组织漏洞扫描的模版。

代码分析属性图 CPG 介绍

可以用来分析缓冲区溢出、整型溢出、格式字符串漏洞、内存泄漏等。

代码分析属性图 CPG 介绍

2.1、局限性

1、纯静态分析,缺乏对运行时信息的组织(比如数据争用)

2、解决的是通用的潜在漏洞,难以挖掘特定场景下的漏洞

3、暂时缺乏对 IR 层面的分析(待扩展)

3、结构

代码分析属性图 CPG 介绍

结合了下面三种不同的表示:

代码分析属性图 CPG 介绍

整合成一种表示 - Code Property Graph:

代码分析属性图 CPG 介绍

CPG 就是属性图,它包含以下组成部分:

  • 节点及其类型。节点代表程序的组成部分,包含有底层语言的函数、变量、控制结构等,还有更抽象的 HTTP 终端等。每个节点都有一个类型,如类型 METHOD 代表是方法,而 LOCAL 代表是一个局部变量。
  • 有向边及标签。节点之间的边代表它们之间的关系,而标签则是关系的说明。比如一个方法 CONTAINS 包含了一个局部变量。
  • 键值对。节点带有键值对,即属性,键取决于节点的类型。比如一个方法至少会有一个方法名和一个签名,而一个局部变量至少有名字和类型。

4、概念

4.1、CPG (Code Property Graph)

4.1.1、property graph

定义基本的有向图数据结构 G = (V, E, λ, μ),即 graph = (点的集合,有向边的集合,边上标签的计算函数,属性)

Σ 代表字母表,λ : E → Σ

K 代表属性键 key 的集合,而 S 代表属性值 value 的集合,μ : (V ∪ E) × K → S

代码分析属性图 CPG 介绍

4.1.2、traversal

遍历函数是 T : P(V) → P(V),P 是 V 的幂集,即 V 中所有子集构成的集合,实际上是把一系列节点映射为另一系列的节点。

代码分析属性图 CPG 介绍

还可以获取有向边的输入:

代码分析属性图 CPG 介绍

4.2、AST (Abstract Syntax Trees)

代码分析属性图 CPG 介绍

抽象语法树 AST,这棵树的编码信息,表达了语句(statements)和表达式(expressions)是如何嵌套组合来生成程序代码的。

AST 是有序的树,它的内部节点代表了运算符操作(如加法或赋值),而叶子节点则关联到操作数(如常量或标识符)。

4.3、CFG (Control Flow Graph)

代码分析属性图 CPG 介绍

控制流图 CFG,描述了代码语句(statements)执行的顺序,以及某一执行路径上的某处的状态。在图中,语句(statements)和判断(predicates)用节点来表示,而这些节点用有向边来连接,表示控制的转换。每条边需要标上 true、false、空 的标签。

CFG 可以用 AST 来生成:首先,结构化的控制语句(如 if、while、for)先用来构建初始的 CFG;然后,添加上非结构化的控制语句(如 goto、break、continue),来逐步修正这个初始的 CFG。

4.4、PDG (Program Dependence Graphs)

代码分析属性图 CPG 介绍

PDG 用来找出会影响某条语句上的变量值的所有语句和判断。PDG 展现了语句和判断之间的依赖关系。一般该图以两种类型的边来构造:数据依赖边(data dependency edges)表示会影响某语句的变量;控制依赖边(control dependency edges)表示判断语句对于变量值的影响。

PDG 的边可以从 CFG 计算得来,通过先判断已定义的变量的集合、每条语句使用到的变量的集合,然后计算每条语句和判断的定义在哪。

5、转换工具 llvm2cpg

llvm2cpg - 将 LLVM bitcode 格式转换成 CPG 图

$ clang -S -emit-llvm -g -O1 main.c -o main.ll 
$ llvm2cpg -output=/tmp/cpg.bin.zip main.ll

下面的 bitcode (IR)内容:

define i32 @sum(i32 %a, i32 %a) {   
    %r = add nsw i32 %a, %b   
    ret i32 %r 
}

可以转换成更高级抽象的形式:

i32 sum(i32 a, i32 b) {   
    i32 r = add(a, b);   
    return r; 
}

5.1、实现细节

当要用 LLVM 来支持 CPG,首先的问题就是:我们要怎么把 bitcode 映射为 CPG?

5.1.1、指令语义

我们可以把一些 LLVM 的指令转换成 CPG 的运算符。比如:

  • addfadd -> <operator>.addition
  • bitcast -> <operator>.cast
  • fcmp eqicmp eq -> <operator>.equals
  • uremsremfrem -> <operator>.modulo
  • getelementptr -> 组合 <operator>.pointerShift<operator>.indexAccess, 和 <operator>.memberAccess 依赖于 GEP 操作数的类型

但有一种指令我们无法映射为 CPG 的,那就 phi 指令,在 CPG 中没有 phi 节点的概念,所以我们需要用 reg2mem 的运算方法把 phi 指令去掉。

5.1.2、冗余

Clang 默认会生成大量的冗余指令。

define i32 @sum(i32 %0, i32 %1) {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  %5 = load i32, i32* %3, align 4
  %6 = load i32, i32* %4, align 4
  %7 = add nsw i32 %5, %6
  ret i32 %7
}

而不是更加精简的版本:

define i32 @sum(i32 %0, i32 %1) {
  %3 = add nsw i32 %1, %0
  ret i32 %3
}

一般情况下,这不是什么问题,但它增加了数据流追踪的复杂度,而且很没必要地增加了图的大小。一个可以解决的方法是,先对 bitcode 跑一下优化。但最终 LLVM 还是把这种选择权交给用户来决定。

5.1.3、类型等价判断

在我写的一篇译文里有相关更具体的介绍和解决方案《【译】LLVM 类型相等判断》,或者可以直接看原文。下面简单地介绍下问题:

如果有相同结构相同名字的两个模块,被加载到同一上下文,LLVM 会重命名其中一个来避免命名冲突。

; Module1
%struct.Point = type { i32, i32 }

; Module 2
%struct.Point = type { i32, i32 }

当加载到同一个上下文时:

%struct.Point = type { i32, i32 }
%struct.Point.1 = type { i32, i32 }

我们需要对这些类型进行去重,以保证最终生成的图中,只会有一个 Point 的节点。这就需要我们判断两个类型是否是等价的。

5.2、总结

用 LLVM bitcode 的方式有以下的优点和局限性:

  • LLVM 语言的接口比 C 和 C++ 更轻量
  • 很多抽象层的细节,在 IR 层不会有体现
  • 程序需要被编译,因此会限制了程序的语言扩展范围

6、相关产品

现在,CPG 已经被以下几个工具支持了:

  • Ocular- 支持 Java, Scala, C#, Go, Python, 和 JavaScript 语言
  • Joern- Ocular 的开源部分,支持 C 和 C++
  • Plume- 开源工具,支持 Java Bytecode

6.1、Joern

Joern 有以下几个重要特性:

  • Fuzzy Parsing of C/C++. 模糊解析,用于在编译环境和部分文件的缺失的情况下,进行解析。
  • Code Property Graphs. 模糊解析的结果会被存储到 CPG 格式的数据库当中。
  • Search Queries. 提供了基于 Gremlin-Scala 扩展的查询语言。
  • Extendable via CPG passes. 支持自定义扩展来添加或消费数据信息。

Joern 可以为 C/C++ 代码创建以下几种中间表示:

  • Abstract Syntax Trees (AST)
  • Control Flow Graphs (CFG)
  • Control Dependence Graphs (CDG)
  • Data Dependence Graphs (DDG)
  • Program Dependence graphs (PDG)
  • Code Property Graphs (CPG14)

6.1.1、Joern 使用例子

有了 Joern 和 llvm2cpg,就可以愉快地玩耍了:

  1. 转换程序为 LLVM Bitcode
  2. 生成 CPG
  3. 加载 CPG 到 Joern 当中,然后进行分析
$ cat main.c
extern int MAX;
extern int source();
extern void sink(int);
void foo() {
  int x = source();
  if (x < MAX) {
    int y = 2 * x;
    sink(y);
  }
}
$ clang -S -emit-llvm -g -O1 main.c -o main.ll
$ llvm2cpg -output=/tmp/cpg.bin.zip main.ll

现在我们已经把 CPG 保存到 /tmp/cpg.bin.zip ,之后就能加载到 Joern,然后尝试分析是否有从 source 函数到 sink 函数的流:

$ joern
joern> importCpg("/tmp/cpg.bin.zip")
joern> run.ossdataflow
joern> def source = cpg.call("source")
joern> def sink = cpg.call("sink").argument
joern> sink.reachableByFlows(source).p
List[String] = List(
  """_____________________________________________________
| tracked               | lineNumber| method| file   |
|====================================================|
| source                | 5         | foo   | main.c |
| <operator>.assignment | 5         | foo   | main.c |
| <operator>.lessThan   | 6         | foo   | main.c |
| <operator>.shiftLeft  | 7         | foo   | main.c |
| <operator>.shiftLeft  | 7         | foo   | main.c |
| <operator>.assignment | 7         | foo   | main.c |
| sink                  | 8         | foo   | main.c |
"""
)

参考

上一篇:MLIR中间表示与编译


下一篇:iOS编译简析