Modeling and Discovering Vulnerabilities with Code Property Graphs
之前提到过许多代码漏洞检测方法都会用到图表示和图神经网络,但对这些图的来源开始缺少探索,今天来探索下。
一.背景
CPG(代码属性图)是2014年提出来了,不过现在很多对漏洞的研究依旧是基于它的。CPG是结合AST(抽象语法树),CFG(控制流图),PDG(程序依赖图)3种代码表示的一种联合表示方法。并且在这篇文章里,作者试图通过图的遍历(graph traversals)来挖掘漏洞。挖掘的漏洞包括缓冲区溢出(buffer overflows),整数溢出(integer overflows),内存泄漏(memory disclosures),格式化字符串漏洞(format string vulnerabilities)。
作者认为图表示可以允许安全人员写出更复杂的漏洞模式(更复杂的规则匹配),来针对不同种类的漏洞进行检测,作者的贡献如下
- code property graph(CPG):一种结合了AST,CFG,PDG 3种程序表示的综合图表示。
- Traversals for vulnerability types:基于CPG可以写出图的遍历算法来挖掘程序中潜在的漏洞,CPG的诞生促成了更有效率的漏洞模式的产生。
- Efficient implementation:将CPG导入到图数据库之后,遍历变得更有效率。
二.Representations Of Code
为了解释程序的属性,在程序分析和编译器设计领域已经发展了各种不同的代码表示方法。虽然这些表示主要是为了分析和优化代码而设计的,但它们也适合于描述这篇文章所探讨的代码。不过作者集中关注3种表示:
AST,CFG和PDG。
以如下代码举例
void foo()
{
int x = source();
if (x < MAX)
{
int y = 2 * x;
sink(y);
}
}
这个代码包括4个statement。分别是
int x = source();
if (x < MAX)
int y = 2 * x;
sink(y);
2.1 AST
AST全称抽象语法树(abstract syntax tree)。通常由代码解析器(code parser)或者编译器(compiler)产生。AST也是很多其它表示(code representation)得基础。
AST的非终端结点(inner node)表示operator(运算或者赋值操作)。而终端结点(leaf node)表示operand(常量或者identifier)。AST可能会适用于code transformation这样的任务,也在检测相似代码片段上取得一定效果。但是AST缺乏控制流和数据依赖信息。上面代码的AST如下
2.2 CFG
控制流表示了每个statement的执行顺序以及需要满足的condition(if分支)。CFG的每个结点表示1个statement。结点之间通过有向边连接。而AST中的边是无向边。
通常,AST经过2个步骤可转换为CFG:
- 用structured control statements(if, while, for等)建立初步的CFG
- 再用unstructured control statements(goto,break等)来修正。
CFG可以用在许多安全应用上,比如检测已知恶意代码的变种以及指导fuzz testing tools。并且已成为了程序理解和逆向工程的基础。尽管如此,CFG缺乏数据流信息。
上述代码的CFG如下
可以看到前面的1行statement这里为1个结点
2.3 PDG
程序依赖图(PDG)最初是用在程序切片(program slicing)任务当中的。
PDG有2个部分:
- 数据依赖(data dependency edges):表示变量之间的依赖,比如下面的图中
if (x < MAX)
和int y = 2 * x;
数据依赖于int x = source();
,依赖变量是x
。而sink(y);
数据依赖于int y = 2*x;
依赖变量为y
- 控制依赖(control dependency edges):表示对条件之间的依赖,比如下图中
C
t
r
u
e
C_{true}
Ctrue 表示条件为真执行。即
if
代码块中所有的statement 会控制依赖于if
条件中的statement。这点要和CFG区分开来。
下图是上面代码的PDG
三.Property Graphs And Traversals
每种程序表示(AST,CFG,PDG)都从不同的角度表示程序。而CPG就是要结合这几种表示。作者在这里引入属性图(property graph)的概念,这在许多图数据库(ArangoDB,Neo4J,OrientDB)中是结构化数据的基础表示。
关于属性图的介绍。
3.1 属性图
属性图 G G G 定义如下:
- G = ( V , E , λ , μ ) G = (V,E, \lambda, \mu) G=(V,E,λ,μ) 是一个有向图,并且每条边都有label。
- V V V 是结点集合。
- E ⊆ ( V × V ) E \subseteq (V \times V) E⊆(V×V) (乘号是笛卡尔积)是一个有向边集合。
- λ : E → Σ \lambda : E→ Σ λ:E→Σ 是一个给边打标签的函数。 Σ Σ Σ 是边的标签集合。
- μ : ( V ∪ E ) × K → S \mu: (V \cup E) \times K→S μ:(V∪E)×K→S。给结点和边分配属性会用到 μ \mu μ 函数。 K K K 是属性key集合, S S S 是属性value集合。
一个简单的属性图示例如下:
其中每个结点的属性key均为
k
k
k, 而属性值有
x
,
w
x, w
x,w 2种。(
ε
\varepsilon
ε 不算)。挖掘属性图信息的主要步骤是graph traversals。
3.2 属性图的遍历
对于给定的结点集合
X
X
X (
V
V
V 的子集), 这里用到以下函数:
F
I
L
T
E
R
p
(
X
)
=
{
v
∈
X
:
p
(
v
)
}
FILTER_p(X)=\{v \in X:p(v)\}
FILTERp(X)={v∈X:p(v)}
p
(
v
)
p(v)
p(v) 是一个bool
函数,判断结点
v
v
v 是否满足一定条件,所以这是个简单的过滤函数。
对于属性图遍历有如下操作:
O
U
T
(
X
)
=
∪
v
∈
X
{
u
:
(
v
,
u
)
∈
E
}
OUT(X) = \underset{v \in X}{\cup} \{u:(v,u)∈E\}
OUT(X)=v∈X∪{u:(v,u)∈E}
O U T l ( X ) = ∪ v ∈ X { u : ( v , u ) ∈ E a n d λ ( ( v , u ) ) = l } OUT_l(X) = \underset{v \in X}{\cup} \{u:(v,u)∈E \; and \; \lambda((v, u)) = l \} OUTl(X)=v∈X∪{u:(v,u)∈Eandλ((v,u))=l}
O U T l k , s ( X ) = ∪ v ∈ X { u : ( v , u ) ∈ E a n d λ ( ( v , u ) ) = l a n d μ ( ( v , u ) , k ) = s } OUT_l^{k, s}(X) = \underset{v \in X}{\cup} \{u:(v,u)∈E \; and \; \lambda((v, u)) = l \; and \; \mu((v,u),k)=s \} OUTlk,s(X)=v∈X∪{u:(v,u)∈Eandλ((v,u))=landμ((v,u),k)=s}
O U T ( X ) OUT(X) OUT(X) 返回返回 X X X 中结点所有的邻居结点, O U T l ( X ) OUT_l(X) OUTl(X) 返回 X X X 中通过类别 l l l 的边的可达结点。 O U T l k , s ( X ) OUT_l^{k, s}(X) OUTlk,s(X) 返回 X X X 中通过类别 l l l 的边并且属性 k : s k:s k:s 的可达结点。
四.Code Property Graphs
CPG定义 G = ( V , E , λ , μ ) G=(V, E , \lambda, \mu) G=(V,E,λ,μ)。
- V V V 表示结点集合,包括了AST叶子结点和statement。
- E E E 为边集合,包括CFG,AST,PDG的边。
- λ \lambda λ 为边分类函数,AST边可能只有一个类。CFG边可能有 ε \varepsilon ε, t r u e true true, f a l s e false false 几类。PDG可能有 D x D_x Dx, D y D_y Dy, C t r u e C_{true} Ctrue 等类。
上述代码的CPG示例
可以看到
- 与AST对比以下,整个函数的AST被切分成4个statement的AST,并用CFG和PDG的边串起。
- 与CFG相比,每个statement用它的AST来表示而不仅仅是token序列,并且多了PDG的边。
- 与CFG同理,每个statement用它的AST表示而不是token序列,并多了CFG的边。
五.Traversals For Well-Known Types Of Vulnerablities
5.1 example
作者用了下面的c代码举例
[...]
if (channelp)
{
/*set signal name (without SIG prefix)*/
uint32_t namelen =_libssh2_ntohu32(data+9+sizeof("exit-signal"));
channelp->exit_signal = LIBSSH2_ALLOC(session, namelen + 1);
[...]
memcpy(channelp->exit_signal,data + 13 + sizeof("exit_signal"),namelen);
channelp->exit_signal[namelen] = ’\0’;
[...]
}
[...]
channelp->exit_signal = LIBSSH2_ALLOC(session, namelen + 1);
中 namelen
是用户可控参数,所以会导致漏洞。
作者从以下几个方面分析漏洞:
- Sensitive operations:敏感操作包括调用受保护的函数( protected functionality),缓冲区复制数据,而示例中每个statement中的算术运算(比如加法)需要多加关注,这需要访问AST。
- Type usage:同时变量类型呀需要多加关注,如果
namelen
是16位而不是32位变量就不会出现漏洞。这也需要检查AST。 - Attacker control:检测哪些data source处于用户控制之下很重要,这个示例中,
_libssh2_ntohu32
的返回值就是用户可控的。PDG中的数据依赖可以对此进行建模。 - Sanitization:许多程序由于缺乏数据校验而导致漏洞,在示例中,如果对
namelen
进行校验,确保它的值在合适范围内,那么漏洞不会发生。CFG则可以在此派上用场。
作者定义了如下3种漏洞,不过论文对这些漏洞的描述过于抽象,我就简单说下
5.2 Syntax-Only Vulnerability
在CPG中,statement内部的问题可以通过AST解决。而statement之间的相互作用就需要CFG和PDG了。
AST层面主要有如下问题:
-
Insecure arguments
不安全的参数,主要出自函数调用参数,比如格式化字符串漏洞(printf
函数)。其中格式化字符串一个必备条件就是传递的第一个参数不是常量(%s
等等)。 -
Integer overflows
整数溢出常常发生在内存分配(malloc
)的算术运算中(+
,*
),比如LIBSSH2_ALLOC(session, namelen + 1);
的第二个参数。所以在遍历AST时重点访问malloc
类函数调用中的这些算术运算结点。 -
Integer type issues
问题主要出现在赋值操作中,左边的数据类型宽度要小于右边的宽度(比如左边short
,右边int
),这在遍历AST的时候可能分别需要遍历赋值运算符的左右子树。
5.3 Control-Flow Vulnerability
引入CFG可以对更多的漏洞类型建模,比如
-
Resource leaks
当资源被分配(allocate)但并没有被释放的时候,会导致系统爆内存,进而使得无法被外部访问。在CFG中,从1个分配内存空间的函数调用(malloc
)开始,找不到释放这个指针的函数(free
)。当然,分配内存的函数必须要返回一个指针。 -
Failure to release locks
-
Use-after-free vulnerabilities
内存被释放后没有置为NULL
,导致可能被再次利用。
5.4 Taint-Style Vulnerability
-
Buffer overflow vulnerabilities
缓冲区溢出漏洞大多是没有对输入数据进行校验导致的。在许多linux内核代码中,当系统从get_user
读取外部输入数据作为第3个参数传递给copy_from_user
或者memcpy
函数时会触发该漏洞。所以遍历的时候需要检查get_user
的第一个参数和copy_from_user
(memcpy
)的第3个参数。 -
Code injection vulnerabilities
在C语言中,注入类漏洞通常在CPG中存在从recv
第2个参数到system
第1个参数的路径,并且中间没有检查是否校验字符串有没有分号。 -
Missing permission checks
没有对用户可控数据进行检查,确保他们有足够的权限。