引言
读后有收获的同学可以点点关注,非常感谢~ 文中有错误的地方欢迎大家评论区指正。同时也欢迎大家将自己的想法发布在评论区,总而言之,大家畅所欲言~
上期内容已经介绍了CPG的基本概念以及其运行逻辑,建议没有学习的童鞋转战抽丝剥茧CPG系列第一弹:CPG介绍学习~
我们知道,CPG包含的数据量是相当庞大的,CPG几乎把静态分析可用的图都拿过来了,把这些图都封装到一个数据结构超图(super graphic)中,最终构成了一个看起来非常复杂的代码属性图,我们不仅可以从CPG上拿到基本的代码结构信息、控制流信息,甚至,我们可以拿来做数据流分析、污点分析等等,真是妙哉!
本节内容主要讲CPG中的DFG(Data Flow Graphic)
DFG是以节点间的边构建而成的。每个节点都有一组输入数据流(prevDFG
)和输出数据流(nextDFG
)。不同类型的节点构建数据流的方法也是不一样的。
接下来,我们看看不同类型的节点都是怎么构建DFG边的。
1.CallExpression
CallExpression 用来表示方法调用,主要关注以下两个字段:
invokes: List<FunctionDeclaration>
: 存储被调用的方法arguments: List<Expression>
: 方法调用的实参
CPG将被调方法分为两种类型:
- 被调用的方法在源码中已经实现(CPG可以对其进行分析),这种情况
invokes
不为空 - 被调用的方法在源码中没有实现(如:调用第三方库函数),这种情况
invokes
为空
1.1.Known function
对于invokes
列表中的每个方法,会构建两条数据流边:
- 边1:CallExpression的实参流向被调用方法的形参的边
- 边2:被调用方法的声明流向CallExpression的边
此处与我之前写的文章“SAST-数据流分析方法-理论”(加链接)讲的ICFG的构建方法类似,笔者认为这种基础性质的东西还是有必要掌握的,建议对ICFG构建不熟悉的同学点链接再学习一下~
1.1.1.验证边1
测试代码:
package com.test.cpg;
public class TestCallExpression {
public static void main() {
int a, b, c;
a = 6;
b = addOne(a); // CallExpression
c = b - 3;
b = ten(); // CallExpression
c = a * b;
}
static int addOne(int x) {
int y = x + 1;
return y;
}
static int ten() {
return 10;
}
}
CPG翻译后的结果是TranslationResult,在后续的文章CPG参数介绍中会详细介绍这个类型的各个字段,莫慌,现在先跟着笔者的节奏走~
- 验证实参是否流向了被调方法的形参
translationResult.calls // calls方法获取源码中所有的方法调用表达式
可以看到,结果与源码是对应的,好,我们接着往下走,看看真实的DFG边是不是像上文说的那样构建的~
tips:可能细心的同学会发现两个调用表达式的类型不是CallExpression,而是MemberCallExpression,这里给大家讲一下,cpg的CallExpression有两个子类,一个是MemberCallExpression,另一个是ConstructExpression,如下图,见名知意,此处不多解释~
我们现在看第一个CallExpression
translationResult.calls[0].arguments // arguments方法获取调用表达式的所有实参
可以看到,确实有一个实参a
,那我们再来看a
的nextDFG是不是流向了addOne
方法的形参处
事实证明,确实有一条实参argument流向被调方法addOne形参的边。
1.1.2.验证边2
- 验证被调方法声明是否流向CallExpression
translationResult.calls[0].prevDFG // 获取第一个CallExpression的前一个DFG边
可以看到,确实有一条被调方法addOne的声明处流向CallExpression的边
1.2.Unknown function
- 边1:所有实参流向CallExpression的边
- 边2:base(也就是caller)流向CallExpression的边
老样子,上代码!开启debug大法
测试代码:
package com.cpg.dfg;
import org.apache.shiro.util.StringUtils;
public class TestUnknownFunction {
public static void main() {
String str = "hello cpg";
boolean has = StringUtils.hasText("cpg"); // CallExpression (第三方库函数)
}
}
1.2.1.验证边1
- 验证实参是否流向CallExpression
translationResult.calls[0].arguments[0].nextDFG
可以看到,确实有实参流向CallExpression的边
1.2.2.验证边2
- 验证base(caller)是否流向CallExpression
translationResult.calls[0].prevDFG // 获取所有流向CallExpression的节点
可以看到,流向CallExpression的所有DFG中,第二个是实参,第一个就是所谓的base,也就是第三方库的caller。
所以,确实有一条base(caller)流向CallEXpression的边
tips:若读者也要做类似的测试,一定要记得关闭CPG自带的方法推断功能(inferFunctions),否则不会得到以上结果(方法推断功能会将Unknown Function做虚拟化操作,至于如何关闭此处就不详细解释了,防止造成误导,后续的CPG使用文章中会详细介绍)。
OK,目前为止,CallExpression的DFG构建过程已经验证完成,后续的其他类型的节点的验证过程直接贴图了,不再赘述,大家直接看图就OK~
2.CastExpression
CastExpression(类型强转表达式)关注以下字段:
expression: Expression
: 需要进行类型转换的表达式(也就是被转换的对象)
构建一条DFG边:
- expression字段(被转换的对象)流向CastExpression
2.1.验证边
- expression字段 --> CastExpression
测试代码:
package com.cpg.dfg;
public class TestCastExpression {
public static void main(String[] args) {
Object o = getMyObject(1);
if (o instanceof MyObject) {
MyObject myObject = (MyObject) o; //(MyObject) o 是一个 CastExpression
System.out.println(myObject);
}
}
private static Object getMyObject(int a) {
if (a > 0) {
return new MyObject();
}
else {
return new Object();
}
}
static class MyObject {
}
}
该示例中第7行(MyObject) o
就是一个CastExpression,那么他的expression
字段就是o
构建的这条边就是 o --> CastExpression
debug验证结果如下:
3.AssignExpression
AssignExpression
就是赋值表达式,其关注以下两个字段:
lhs: List<Expression>
: 赋值语句的所有左表达式rhs: List<Expression>
: 赋值语句的所有右表达式
3.1.Normal assignment
赋值操作符为等号 operatorCode: =
- 边1:rhs 流向 lhs 的边
- 边2:rhs 流向 AssignExpression的边 (赋值操作的AST父节点不是Block时会加这条边)
如果lhs由多个变量(或元组)组成,CPG会尝试根据索引拆分rhs。如果无法拆分,则整个rhs会流向lhs中的所有变量
如果 lhs
与 rhs
的长度相等:
3.1.1.验证边1
- rhs 流向 lhs 的边
测试代码如下:
package com.cpg.dfg;
public class TestAssignExpression {
public static void main(String[] args) {
// Normal assignment
MyObject o1 = new MyObject();
MyObject o2 = new MyObject();
o1 = o2; // AssignExpression
System.out.println(o1);
// Compound assignment
int a = 1;
a += 1; // AssignExpression
System.out.println(a);
}
static class MyObject{}
}
tips:此处使用CPG提供的访问者模式,自定义Visitor找translateResult中的AssignExpression,要注意的是一定要开启CPG提供的 DFGPass(并同时开启SymbolResolver、EvaluationOrderGraphPass、TypeHierarchyResolver、TypeResolver,因为Pass之间有依赖关系)
可以看到,确实有一条 rhs 流向 lhs 的边
3.1.2.验证边2
- rhs 流向 AssignExpression的边 (赋值操作的AST父节点不是Block时会加这条边)
在某些编程语言中,子表达式中可以存在赋值操作(例如 a + (b=1)
,我们现在只关注CPG分析java代码,所以,此处偷个小懒,就不验证啦,感兴趣的同学可以自行验证(手动狗头)~
3.2.Compound assignment
赋值操作符为其他符号 operatorCode: *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=
- 边1:lhs 和 rhs 流向二元运算表达式
- 边2:二元运算表达式流向 lhs
CPG必须确保前两项操作在最后一项操作之前完成
3.2.1.验证边1
- lhs 和 rhs 流向二元运算表达式
测试代码同3.1.1
- rhs流向二元运算表达式
- lhs流向二元运算表达式(先忽略第二条边,此处验证了有lhs流向二元运算表表达式的边即可)
3.2.2.验证边2
- 二元运算表达式流向 lhs
公主号推荐
笔者运营自己的公主号,会定时更新代码分析、安全漏洞、热点资讯等信息。
本文由mdnice多平台发布
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。