程序分析-Joern工具工作流程分析

Joern工具是一个代码解析工具,主要针对C语言,Joern有2个版本:

  • 第一个版本将解析的 c, cpp, hpp 等文件解析为图结构后导出 edge.csvnode.csv。用法简单些,不过感觉bug有点多,同时很久不更新的,官方将后续更新都迁移到第二个版本上。

  • 第二个导出 dot 文件。不过第二个实在太难用了,还要手写scala脚本,导出的图结点全是AST结点。

我这里分析了第一个版本0.3.1分支的代码,主要目的是基于这个开发自己的工具,因为导出csv对我的工作实在是不方便。

Joern基于Antlr实现,Antlr写代码分析工具的大概套路就是写g4文件,写完之后可以生成java代码(Antlr也支持python等语言,不过java最好用),Joern 0.3.1的g4文件位置如下。

Joern的工作流程大致如下:

转换
控制流分析
语句支配分析
symbol使用定义分析
数据依赖分析
AntlrAST
AST
CFG
CDG
UDG
DDG

Joern首先将Antlr的AST转化为自定义的AST结构,然后基于自定义的AST结构生成CFG以及CDG(控制依赖图)、DDG(数据依赖图)

Joern的代码目录结构如下:

antlr/C
ast
cdg
cfg
databaseNodes
ddg
fileWalker
graphutils
misc
neo4j
outputModules
parsing
tests
tools
udg
  • antlr/C 下为Antlr生成的的Listener和Visitor。

  • ast 为Joern自定义的AST结点。

  • cdg 定义支配分析,控制依赖代码以及控制依赖图的定义。

  • cfg 包含CFG的定义以及自定义AST向CFG转换的代码。

  • ddg 包含数据依赖分析相关代码。

  • parsing 下为AntlrAST向Joern自定义AST转换的代码。

  • udg 包主要计算每个结点使用定义的symbol,为 ddg 的辅助工具。

  • misc 包含一些自定义工具类。

顺带一提,在老版本的joern中变量定义没有考虑函数指针的情况

declarator: ptrs? identifier type_suffix?;

而新版本的joern中加入了函数指针和模板特性

declarator: ptrs? identifier template_args? type_suffix? |
            ptrs? '(' func_ptrs identifier ')' type_suffix;

一.自定义AST

自定义AST主要用2类结点:

  • Statement:语句类结点

  • Expression:表达式

AntlrAST转化为自定义AST可参考FunctionContentBuilder

1.1.Statement

Statement对应的文法在Function.g4中

statement: opening_curly
         | closing_curly
         | block_starter
         | jump_statement
         | label 
         | simple_decl
         | expr_statement
         | water
        ;

其中

  • block starter指 if, else, while, try 等等,这类语句都会包含语句块(CompoundStatement{ xxx }

  • jump statement指 break, continue, goto, return

  • opening_curly指 {, closing_curly指 } (这里有点奇怪,Joern中 { 也算一条语句)

  • expr statement指表达式语句,也就是expr + ;

  • label指 switch 语句中的 case xxx 表达式

  • simple decl指变量定义语句

joern中需要注意 ifdo 语句

If-Else示例

{
	if (cond1) {
	}
	else if (cond2){
	}
	else{
	}
}

Antlr AST为(文法中没有定义 CompoundStatement

在这里插入图片描述
转化后自定义AST为

在这里插入图片描述

生成自定义AST的代码参考FunctionContentBuilderNestingReconstructornew XXX 新建AST结点,xx.addChild 添加AST边。

Do-While示例

do{
	xxx
}
while (cond1);

Antlr AST:

在这里插入图片描述
自定义AST:

在这里插入图片描述

Condition 类继承自 ExpressionHolder 继承自 Expression ,其中while已经被隐藏了,而同结构 while 循环对应的自定义AST如下:

在这里插入图片描述

可以看到父节点类型为 WhileStatement,而子结点一样但是顺序变了,因为执行 addChild 的顺序发生了改变。

For示例

对于 for 语句

for (initExpr; cond1; Expr){
	xxx
}

AntlrAST为

在这里插入图片描述

自定义AST为

在这里插入图片描述

Switch示例

switch(cond1){       
   case id1: break;      
}

Antlr AST:

在这里插入图片描述

自定义AST:

在这里插入图片描述

1.2.Expression

Expression类的继承关系如下

Expression
BinaryExpression
PostfixExpression
ExpressionHolder
Identifier
ArrayIndexing
CastExpression
CastTarget
ConditionalExpression
IncDec
Sizeof
SizeofExpr
SizeofOperand
UnaryExpression
UnaryOp
UnaryOperator
AssignmentExpression
AndExpression
AdditiveExpression
BitAndExpression
EqualityExpression
ExclusiveOrExpression
InclusiveOrExpression
MultiplicativeExpression
OrExpression
RelationalExpression
ShiftExpression
Argument
ArgumentList
Callee
InitializerList
CallExpression
IncDecOp
MemberAccess
PrimaryExpression
PtrMemberAccess

类别太多这里我就不一一列举了,先列举几个:

  • BinaryExpression为双目运算表达式

  • UnaryExpression为单目运算表达式

  • ConditionalExpression为三目运算表达式(cond? expr1: expr2 ),不过计算CFG的时候三目表达式都在一个CFG结点上。

  • PrimaryExpression为运算数,可以为变量名、字符串常量或者数值型常量。

  • Identifier为标识符,大部分情况下代表变量名,有时候也包括函数名等等。

  • InitializerList为初始化列表,比如 char a[3] = {'a', 'b', 'c'};InitializerList{'a', 'b', 'c'},中间3个元素每个对应一个 PrimaryExpression

二.CFG的建立

在构建完自定义AST之后就开始构建CFG,构建CFG的代码放在cfg包下,老版本Joern中构建CFG需要考虑 blockStarterjump 类结点,新版本加入了 trycatch,这里我暂时无视 try 语句的控制流图:

blockStarter 包括:

  • 分支语句 if, else, switch

  • 循环语句 for, while, do-while

CFG的构建代码参考 CCFGFactory

  • CFG结点类型主要包含了 ASTNodeContainerCFGEntryNodeCFGExitNodeCFGErrorNodeASTNodeContainer 为普通CFG结点类型,封装了 ASTNode,继承自 CFGNode

  • CFG默认包含了3种标签:empty, true, false。但是在 switch 语句中边标签为 case 值或者 defalut

joern中还包括了些 fix 函数,因为在开始构建控制流时,joern将这4种跳转语句视为普通的代码语句,因此它们会与后继语句有CFG边相连,而实际上这些语句都会跳转到别处,所以存在错误控制流,这些fix函数就是为了修复这些控制流而存在。

2.1.普通连续结点的CFG

首先看 newInstance(ASTNode… nodes) 方法,这个方法输入的是连续的 ASTNode ,每个 ASTNode 都是一个CFG结点,这个方法的逻辑很简单,就是为这些 ASTNode 生成一个局部小CFG,包含了 EntryNodeExitNode,所有结点顺序连接起来(通过CFG边)。

2.2.函数定义语句

代码为 newInstance(ASTNode… nodes),可以看到joern

  • 首先构造形参列表的CFG,每个形参为1个CFG结点。

  • 然后构造函数主体的CFG,最后将参数CFG和函数主体CFG相连。

void fun(int a, char b){
	// functionBody
}
Entry
int a
char b
functionBody
Exit

2.3.If

代码参考 newInstance(IfStatement ifStatement)。对于

if (cond) {
	block1
}
else {
	block2
}

else if (cond) { } 则当成 else { if (cond) {} } 处理。

生成的CFG如下:

True
False
Entry
cond
block1
block2
Exit

2.4.Switch

解析 Switch 对应代码 newInstance(SwitchStatement switchStatement),我贴上一个补充注释的

// switch语句
    public static CFG newInstance(SwitchStatement switchStatement) {
        try {
            CCFG switchBlock = new CCFG();
            // 处理condition
            CFGNode conditionContainer = new ASTNodeContainer(switchStatement.getCondition());
            switchBlock.addVertex(conditionContainer);
            switchBlock.addEdge(switchBlock.getEntryNode(), conditionContainer);

            CCFG switchBody = convert(switchStatement.getStatement());
            switchBlock.addCFG(switchBody);

            boolean defaultLabel = false;
            // 获取每个case: xxx
            for (Entry<String, CFGNode> block : switchBody.getLabels().entrySet()) {
                // 如果内容是default
                if (block.getKey().equals("default"))
                    defaultLabel = true;
                switchBlock.addEdge(conditionContainer, block.getValue(), block.getKey());
            }
            for (CFGEdge edge : switchBody.ingoingEdges(switchBody.getExitNode()))
                switchBlock.addEdge(edge.getSource(), switchBlock.getExitNode());
            // 不存在defalut标签的话,添加一条condition到switch结尾的CFG边
            if (!defaultLabel)
                switchBlock.addEdge(conditionContainer, switchBlock.getExitNode());

            // switch中break直接与switch end相连
            fixBreakStatements(switchBlock, switchBlock.getExitNode());
            return switchBlock;
        }
        catch (Exception e) {
            // e.printStackTrace();
            return newErrorInstance();
        }
    }

对于如下代码:

switch(cond){
	case 1:
		break;
	case 2:
		break;
}

CFG为:

1
2
Entry
cond
case1
break
case2
break
Exit

2.5.While

代码为 newInstance(WhileStatement whileStatement)

对于代码

while (cond) {
	whileBody
}

CFG为

True
False
Entry
cond
whileBody
Exit

2.6.do-while

代码为 newInstance(DoStatement doStatement)

do{
    doBody
}
while(cond);
True
False
Entry
doBody
cond
Exit

2.7.For

代码为 newInstance(ForStatement forStatement)

代码

for (init; cond; expr) {
	forBody
}

CFG为

False
True
Entry
init
cond
Exit
forBody
expr

如果 expr 为空,即 for (init; cond; )

那么CFG为

False
True
Entry
init
cond
Exit
forBody

暂时还没有对ForRange语句的支持。

三.控制依赖分析

在我之前的博客中简单说了控制依赖和数据依赖。控制依赖主要表现形式如下(if, switch, while, for 通用):

statement1;
if (cond) {
	statement2;
	statement3;
}
statement4;

那么控制依赖图如下:

cond
statement2
statement3

即condition表达式会对其相连块语句中的每一个statement产生控制依赖。控制依赖边的源点通常是condition表达式。在这里也简单说下普通CFG结点通常出度为1,但是condition结点的出度通常大于1

控制依赖分析相关的代码在cdg包下,控制依赖图的创建可分为3个步骤,参考CDGCreator.createDominatorTreeCreator.create方法(前者调用后者):

假设当前有CFG(简化):

3
4
5
6
7
Entry
1
2
Exit

那么逆向CFG就是:

3
4
5
6
7
Entry
2
1
Exit

DominatorTree 有3个成员变量:

  • dominators:保存post-dominate关系,上述示例中dominators的值为 { 1 --> 7, 2 --> 6, 3 --> 6, 4 --> 6, 5 --> 7, 6 --> 7 },key post-dominate value(1 post-dominate 7)

  • dominanceFrontiers:保存控制依赖关系,key 依赖 value,上述示例中值为 { 2 --> {1}, 3 --> {2}, 4 --> {2}, 5 --> {1}, 6 --> {1} } 。value的长度通常为1,一个语句不太可能同时依赖多个condition。

  • postorderEnumeration:保存逆向CFG中结点后序遍历的顺序,上述示例中结点后续遍历顺序为(无视Exit和Entry)1, 2, 3, 4, 6, 5, 7postorderEnumeration 的值为 {1 --> 1, 2 --> 2, 3 --> 3, 4 --> 4, 5 --> 6, 6 --> 5, 7 --> 7},key的值在遍历时第value个被访问。

  • 同时还用到了内部变量 orderedVertices,它的值就是结点后序遍历列表,示例值为 1, 2, 3, 4, 6, 5, 7

上述示例的CDG为

3
4
5
6
1
2

3.1.创建支配树

初始化支配树主要是计算 postorderEnumerationorderedVertices,然后逆序遍历 orderedVertices(以 7, 5, 6, 4, 3, 2, 1 的顺序访问结点,本质是顺序访问逆向CFG),对于列表中的每个结点,当该结点有多个前驱时求出前驱的公共支配结点(比如 1 有前驱 25,那么求出 25 的公共支配结点,2 --> 6 --> 7, 5 --> 7,因此 7 是公共支配结点),并设置当前结点支配公共支配结点(1 --> 7)。

3.2.控制依赖分析

顺序访问 orderedVertices 中的所有控制依赖源点(前驱数大于1,即顺序访问 1, 2)。求出当前结点与前驱结点支配路径的交点(1 的前驱为 2, 51 --> 7, 2 --> 6 --> 7, 5 -->77 就是交点),然后所有路径上除了交点以外的结点都会控制依赖于当前结点 (路径交集涉及 1, 2, 5, 6, 7,除去 1 本身和交点 7,其它结点 2, 5, 6 都会控制依赖于 1)。

四.Use-Def分析

4.1.目标

相关代码在udg包下,这段代码主要是为了计算每个语句使用了以及定义了哪些变量,还不是语句间的数据依赖关系。

举个例子,对于语句 x += y,这部分会分析出 x 被定义以及使用(+= 用到了 x),而 y 只被使用。也就是这部分只是逐语句分析。

UseDefGraph的定义如下:

public class UseDefGraph
{

	// A UseDefGraph is a table indexed
	// by identifiers. Each table-entry
	// is a list of the UseOrDefRecords
	// of the identifier.

	MultiHashMap<String, UseOrDefRecord> useOrDefRecordTable = new MultiHashMap<String, UseOrDefRecord>();

	public MultiHashMap<String, UseOrDefRecord> getUseDefDict()
	{
		return useOrDefRecordTable;
	}

	public List<UseOrDefRecord> getUsesAndDefsForSymbol(String symbol)
	{
		return useOrDefRecordTable.get(symbol);
	}

	public void addDefinition(String identifier, ASTNode astNode)
	{
		add(identifier, astNode, true);
	}

	public void addUse(String identifier, ASTNode astNode)
	{
		add(identifier, astNode, false);
	}

	private void add(String identifier, ASTNode astNode, boolean isDef)
	{
		UseOrDefRecord record = new UseOrDefRecord(astNode, isDef);
		useOrDefRecordTable.add(identifier, record);
	}

}

MultiHashMap 是joern中自定义实现的数据类型,MultiHashMap<K, V> 等同于 Map<K, LinkList<V>>useOrDefRecordTable 的 key 是每个符号,value记录了该语句中这个符号的使用和定义情况。符号是指语句中出现的变量。

UseOrDefRecord是一个二元组,为 < ASTNode, boolean > 类型,它表示一个符号在该ASTNode下是被定义或者被使用。

4.1.1.示例

x += y 来举例,其AST和分析完毕时 useOrDefRecordTable 的结果如下:

ExpressionStatement
AssignmentExpr
Identifier
Identifier
{
	"x": [
		[ExpressionStatement, true],
		[AssignmentExpr, true],
		[ExpressionStatement, false],
		[AssignmentExpr, false]
	],
	"y": [
		[ExpressionStatement, false],
		[AssignmentExpr, false]
	]
}

其中只有 ExpressionStatement 属于CFG结点,因此暂时忽略 AssignmentExpr,这个结果表示在该语句中,符号 x 既被使用又被定义,而 y 只被使用。

4.1.2.结构体示例

结构体的def-use分析可以参考下面示例:

int ddg_test_struct(){
	struct my_struct foo; 
	foo.bar = 10; 
	copy_somehwere(foo); 
}

AST如下:

CompoundStatement
IdentifierDeclStatement
ExpressionStatement
ExpressionStatement
IdentifierDecl
IdentifierDecl: struct my_struct
Identifier
AssignmentExpr
MemberAccess
PrimaryExpression
Identifier
Identifier
CallExpression
Callee
ArgumentList
Identifier
Argument
Identifier

分析结果如下:

{
	"foo": [
		// struct my_struct foo;
		[IdentifierDeclStatement, true],
		[IdentifierDecl, true],
		// foo.bar = 10; 
		[ExpressionStatement, false],
		[MemberAccess, false],
		[ExpressionStatement, true],
		[AssignmentExpr, true],
		// copy_somehwere(foo);
		[ExpressionStatement, false],
		[Argument, false]
	],
	"foo . bar": [
		// foo.bar = 10; 
		[ExpressionStatement, true],
		[AssignmentExpr, true]
	]
}

从结果来看:

  • struct my_struct foo; 定义了符号 foo

  • foo.bar = 10; 使用了符号 foo,并定义了符号 foofoo . bar

  • copy_somehwere(foo); 使用了符号 foo

4.1.3.数组和指针示例

void test()
{
	char* dst = (char*)malloc(sizeof(char)*100);
	char source[100];
	memset(source, 'A', 100);
	source[99] = '\0';
	memcpy(dst, source, 100);
	*dst = 'A';
}

AST为:

CompoundStatement
IdentifierDeclStatement
IdentifierDeclStatement
ExpressionStatement
ExpressionStatement
ExpressionStatement
ExpressionStatement
IdentifierDecl
IdentifierDecl: base -> char , complete -> char*
Identifier: dst
AssignmentExpr
Identifier: dst
CastExpression
CastTarget: char *
CallExpression
IdentifierDecl
IdentiferDecl: base -> char, complete -> char 100
Identifier
PrimaryExpression: 100
CallExpression
Callee
ArgumentList
Argument
Identifier: source
Argument
PrimaryExpression: 'A'
Argument
PrimaryExpression: 100
AssignmentExpr
ArrayIndexing
Identifier: source
PrimaryExpression: 99
PrimaryExpression: '\0'
CallExpression
Callee
ArgumentList
Identifier: memcpy
Argument
Argument
Argument
Identifier: dst
Identifier: source
PrimaryExpression: 100
AssignmentExpr
UnaryOp
PrimaryExpression: 'A'
UnaryOperator: *
Identifier: dst

解析结果为:

{
	"* dst": [
		[ExpressionStatement, true],
		[true]
	],
	"malloc": [
		[IdentifierDeclStatement, false],
		[AssignmentExpr, false]
	],
	"dst": [
		[IdentifierDeclStatement, true],
		[AssignmentExpr, true],
		[ExpressionStatement, false],
		[Argument, false],
		[ExpressionStatement, false],
		[UnaryOp, false]
	],
	"* source": [
		[ExpressionStatement, true],
		[AssignmentExpr, true],
	],
	"source": [
		[IdentifierDeclStatement, true],
		[IdentifierDecl, true],
		[ExpressionStatement, false],
		[Argument, false],
		[ExpressionStatement, false],
		[ArrayIndexing, false],
		[ExpressionStatement, false],
		[Argument, false]
	]
}

可以看到:

  • char* dst = (char*)malloc(sizeof(char)*100); 定义了符号 dst,使用了符号 malloc(感觉这里 malloc 是被错误解析出来的)。

  • char source[100]; 定义了符号 source

  • memset(source, 'A', 100); 使用了符号 source

  • source[99] = '\0'; 定义了符号 * source,使用了符号 source

  • memcpy(dst, source, 100); 使用了符号 dstsource

  • *dst = 'A'; 定义了符号 * dst,使用了符号 dst

可以看到对于数组指针类型的变量,在使用时除了本身变量名作为符号,还会加上一个 * + 变量名 的符号。

4.2.算法

构建 UseDefGraph 参考CFGToUDGConverter.convert

for (CFGNode cfgNode : statements)
{
		// skip empty blocks
		if (cfgNode instanceof ASTNodeContainer)
		{
			ASTNode statementNode = ((ASTNodeContainer) cfgNode)
						.getASTNode();
			ASTNodeASTProvider provider = new ASTNodeASTProvider();
			provider.setNode(statementNode);
			Collection<UseOrDef> usesAndDefs = astAnalyzer
						.analyzeAST(provider);
			addToUseDefGraph(useDefGraph, usesAndDefs, statementNode);
		}
}

显然,算法是每个语句独立解析,最后将结果汇聚到一起。解析语句主要是遍历该语句对应的AST子树,这里用到了ASTDefUseAnalyzer类和ASTNodeASTProvider类。前者主要遍历某个语句对应的AST求语句类符号(symbol)的定义和使用关系,后者是 ASTNode 的封装。

ASTDefUseAnalyzer 遍历AST的时候用到了后序遍历,特殊处理的AST结点类型参考ASTDefUseAnalyzer.createUseDefEnvironment

private UseDefEnvironment createUseDefEnvironment(ASTProvider astProvider)
	{

		String nodeType = astProvider.getTypeAsString();

		switch (nodeType)
		{
			case "AssignmentExpr":
				return new AssignmentEnvironment();
			case "IncDecOp":
				return new IncDecEnvironment();
			case "IdentifierDecl":
			case "Parameter":
				return new DeclEnvironment();

			case "CallExpression":
				return createCallEnvironment(astProvider);

			case "Argument":
				return createArgumentEnvironment(astProvider);

			case "PtrMemberAccess":
				return new PtrMemberAccessEnvironment();

			case "MemberAccess":
				return new MemberAccessEnvironment();

			case "Condition":
			case "ReturnStatement":
				return new UseEnvironment();

			case "ArrayIndexing":
				return new ArrayIndexingEnvironment();

			case "UnaryOp":
				return new UnaryOpEnvironment();
				
			case "Identifier":
				return new IdentifierEnvironment();

			default:
				return new UseDefEnvironment();
		}
	}

4.2.1.AssignmentExpr, IdentifierDecl, Identifier

char* dst = (char*)malloc(sizeof(char)*100); 为例,其AST为:

IdentifierDecl
IdentifierDecl
IdentifierDecl: base -> char , complete -> char*
Identifier: dst
AssignmentExpr
Identifier: dst
CastExpression
CastTarget: char *
CallExpression
Callee
Identifier: malloc
ArgumentList
Argument
MultiplicativeExpression
SizeofExpr
Sizeof: sizeof
SizeofOperand: char
PrimaryExpression: 100

Joern提取出来的symbol有2个 dstmalloc

先关注 AssignmentExpr 之前的部分:

Joern中定义了一个UseDefEnvironment类用来处理单个AST结点,成员变量 symbols 包含了该结点及其子结点下包含了 symbolUseDefEnvironment 有多种子类型,不同的子类型处理不同类型的结点。UseDefEnvironment 有2个重要的方法 isDefisUse 判断AST结点中 symbol是定义还是使用。

处理上述结点时算法主要从 Identifier 类结点中提取symbol。

AssignmentEnvironmentisUseisDef 定义如下:

@Override
	public boolean isUse(ASTProvider child)
	{
		int childNum = child.getChildNumber();
		// 如果是第一个symbol
		if (childNum == 0)
		{
			// 如果operator不为空 并且 operator不是 =,也就是 x = y没有使用x,而x += y即使用也重新定义
			String operatorCode = astProvider.getOperatorCode();
			if (operatorCode != null && !operatorCode.equals("="))
				return true;
			else
				return false;
		}

		return true;
	}
	// Assignment Expr中第一个symbol为重新定义,后面的均不是
	@Override
	public boolean isDef(ASTProvider child)
	{
		int childNum = child.getChildNumber();

		if (childNum == 0)
			return true;

		return false;
	}
  • isUse :赋值语句有多种 x = y 重新定义了 x 但是没有使用 x,而 x += y 既使用了 x 又重新定义了 xy 不属于赋值语句第一个子结点,因此直接算使用。

  • isDef:赋值语句中只要symbol处于第一个子结点下那都属于重新定义。

IdentifierDeclParameter(函数定义参数)都对应DeclEnvironment

public boolean isUse(ASTProvider child)
{
	return false;
}

public boolean isDef(ASTProvider child)
{
	String type = child.getTypeAsString();
	int childNum = child.getChildNumber();
	return (childNum == 1 && type.equals("Identifier"));
}
  • isUse 恒等于 false 表示 IdentifierDecl 不存在使用只有定义。

  • isDef 则是考虑到这么一个情况

    • 对于 char a;,那么其AST子结点Identifier 的子结点数量为1(调试的时候发现的),此时该语句定义了 a,对于 char a = 0;,语句前半部分 char a 对应的AST子结点 Identifier 子结点数量0,因此没有定义 a.

    • 而后半部分属于 AssignmentExpr,其下有子结点 Identifier: a(这个 Identifier 是被 AssignmentEnvironment 而不是 DeclEnvironment 处理的,)。

    • 因此 char a; 中symbol aDeclEnvironment.isDef 标记为定义,而 char a=0;aAssignmentEnvironment.isDef 标记为定义。

4.2.2.Argument

ArgumentEnvironment相关代码如下:

public class ArgumentEnvironment extends EmitDefAndUseEnvironment
{

	boolean isTainted = false;

	public void addChildSymbols(LinkedList<String> childSymbols,
			ASTProvider child)
	{
		if (isDef(child)){
			// For tainted arguments, add "* symbol" instead of symbol
			// to defined symbols. Make an exception if symbol starts with '& '
			
			LinkedList<String> derefChildSymbols = new LinkedList<String>();
			for(String symbol : childSymbols){
				
				if(!symbol.startsWith("& ")){
					derefChildSymbols.add("* " + symbol);
					// !patch to see if we can detect macro-sources!
					derefChildSymbols.add(symbol);
				}else
					derefChildSymbols.add(symbol.substring(2));	
			}
			
			defSymbols.addAll(derefChildSymbols);
		}
		if (isUse(child))
			useSymbols.addAll(childSymbols);
	}
	
	public boolean isUse(ASTProvider child)
	{
		return true;
	}

	public boolean isDef(ASTProvider child)
	{
		return isTainted;
	}

	public void setIsTainted()
	{
		isTainted = true;
	}
}
  • isUse 表示函数调用实参无论何时都会使用symbol。

  • isDef:返回参数是否受污点影响。

  • addChildSymbols 中的代码表示

    • foo(x)xTaintedtrue,那么 * xx 都会被定义,同时 x 被使用。

    • foo(&x)(直接传地址,引用只在函数定义形参中出现) 中当 xTaintedtrue 时,x 会被重新定义但是 & x 会被使用。

4.2.3.UnaryOp

包括指针引用情况,以 *p = *(a + 1) 为例,AST为:

AssignmentExpr
UnaryOp
UnaryOp
UnaryOperator: *
Identifier: p
UnaryOperator: *
AdditiveExpression
Identifier: a
PrimaryExpression: 1

UnaryOpEnvironment 中的相关代码如下:

public void addChildSymbols(LinkedList<String> childSymbols,
			ASTProvider child)
	{
		
		String codeStr = astProvider.getEscapedCodeStr();
		// 如果以取地址符开头,比如 &p = xx,那么symbol = p, add的就是 & p
		if(codeStr != null && codeStr.startsWith("&")){
			for(String symbol : childSymbols){
				symbols.add("& " + symbol);
			}
			return;
		}
		// 如果是取指向的,那么直接将p添加进符号表
		if(codeStr == null|| !codeStr.startsWith("*")){
			symbols.addAll(childSymbols);
			return;
		}
			
		LinkedList<String> retval = new LinkedList<String>();
			
		// emit all symbols as '* symbol'
		
		LinkedList<String> derefedChildren = new LinkedList<String>();
		for(String c : childSymbols){
			derefedChildren.add("* " + c);
		}
		
		retval.addAll(derefedChildren);

		// emit entire code string
		retval.add(codeStr);
	
		useSymbols.addAll(childSymbols);
		symbols.addAll(retval);
		
	}

针对上述代码生成的分析结果为(false 表示使用,true 表示定义):

{
	["p", false],
	["a", false],
	["* ( a + 1 )", false],
	["* p", true],
	["* a", false]
}

直接从 identifier 中提取出来的symbol只有 p, a,而 derefedChildren.add("* " + c); 添加了symbol * p, * aretval.add(codeStr); 添加了symbol *(a+1)

4.2.4.MemberAccess和PtrMemberAccess

a.f1 = b.f2; 举例,AST如下:

AssignmentExpr
MemberAccess
MemberAccess
Identifier: a
Identifier: f1
Identifier: b
Identifier: f2

MemberAccess相关代码如下:

public LinkedList<String> upstreamSymbols()
	{

		LinkedList<String> retval = new LinkedList<String>();
		
		// emit all symbols
		retval.addAll(symbols);

		// emit entire code string
		String codeStr = astProvider.getEscapedCodeStr();
		retval.add(codeStr);

		return retval;
	}

	public void addChildSymbols(LinkedList<String> childSymbols,
			ASTProvider child)
	{
		int childNum = child.getChildNumber();
		// Only add the left child but never the right child
		if (childNum == 0)
			super.addChildSymbols(childSymbols, child);
	}
  • addChildSymbols 函数的功能是将第一个子结点的symbol加入symbol列表,比如 a.f1a 会被添加 f1 不会。

  • upstreamSymbols 会将子结点的symbol和子身对应的代码片段添加进symbol列表,比如 a.f1aa . f1 会被添加进列表。

解析结果如下:

{
    ["a", false],
    ["a", true],
    ["a . f1", true],
    ["b", false],
    ["b . f2", false]
}

MemberAccessEnvironment.useOrDefsFromSymbols 函数解析出 ["a", false]

PtrMemberAccessEnvironmentMemberAccessEnvironment 类似,区别在于 upstreamSymbols

public LinkedList<String> upstreamSymbols()
	{

		LinkedList<String> retval = new LinkedList<String>();
			
		// emit all symbols as '* symbol'
		// 给变量添加 * 符号
		LinkedList<String> derefedChildren = new LinkedList<String>();
		for(String c : symbols){
			derefedChildren.add("* " + c);
		}
		
		retval.addAll(derefedChildren);

		// emit entire code string
		// 与MemberAccessEnvironment一样
		String codeStr = astProvider.getEscapedCodeStr();
		retval.add(codeStr);

		return retval;
	}

那么对于 a->f1 = b.f2;,解析结果为:

解析结果如下:

{
    ["a", false],
    ["* a", true],
    ["a -> f1", true],
    ["b", false],
    ["b . f2", false]
}

4.2.5.ArrayIndexing

ArrayIndexingEnvironment的相关代码如下:

public class ArrayIndexingEnvironment extends EmitUseEnvironment
{

	public void addChildSymbols(LinkedList<String> childSymbols,
			ASTProvider child)
	{
		
		LinkedList<String> derefedChildren = new LinkedList<String>();
		for(String c : childSymbols){
			derefedChildren.add("* " + c);
		}
		symbols.addAll(derefedChildren);
		useSymbols.addAll(childSymbols);
	}
}

也就是说数组名会作为使用symbol添加,而 * + 数组名会进一步被处理,以 a[i] = b[j]; 为例,其AST为:

AssignmentExpr
ArrayIndexing
ArrayIndexing
Identifier: a
Identifier: i
Identifier: b
Identifier: j

解析结果为:

{
	[a, false],
	[b, false],
	[* a, true],
	[* b, false],
	[i, false],
	[j, false],
	[* i, true],
	[* j, false]
}

当然,有的结果感觉不太对,比如 [* i, true][* j, false]i, j 都是整型变量不是数组,原因可能是 ArrayIndexingEnvironment 添加符号时没有考虑 childNumber,后续可以添加一个判断,只对第一个子结点进行符号处理。

五.数据依赖分析

5.1.算法

数据依赖的分析过程是一个Reaching Definition问题,详细可以参考南大软件分析课程-数据流分析。在这里,gen[B] 表示语句(结点)B 处定义的符号。迭代过程如下(遍历CFG完成):

在这里插入图片描述

数据依赖图相关代码在ddg包下,数据依赖图的定义为DDG类,其主要包含数据依赖边DefUseRelation类。

public class DefUseRelation
{
	public Object src;
	public Object dst;
	public String symbol;

	public DefUseRelation(Object aSrc, Object aDst, String aSymbol)
	{
		src = aSrc;
		dst = aDst;
		symbol = aSymbol;
	}

	@Override
	public boolean equals(Object other)
	{
		DefUseRelation otherRel = (DefUseRelation) other;

		return (src == otherRel.src) && (dst == otherRel.dst)
				&& (symbol.equals(otherRel.symbol));
	}

	@Override
	public int hashCode()
	{
		return symbol.hashCode();
	}

}

srcdst 均为CFG结点,这样一条边表示在 src 中定义的符号在 dst 中被使用。在给定 CFGUseDefGraph 计算 DDG 之前会计算一个中间类型DefUseCFGDefUseCFG 包含了以下成员变量。

  • statements:对应所有的CFG结点,包括Entry和Exit。

  • symbolsUsed 是一个 MultiHashMap:key对应某个CFG结点(语句),value对应该语句使用的symbol列表。

  • symbolsDefined 也是一个 MultiHashMap:key对应某个CFG结点(语句),value对应该语句定义的symbol列表。

  • parentBlocks 也是一个 MultiHashMap:key对应某个CFG结点(语句),value对应其CFG中的前驱结点。

  • childBlocks 也是一个 MultiHashMap:key对应某个CFG结点(语句),value对应其CFG中的后继结点。

  • symbolId 貌似没用到。

Reaching Definition分析由DDGCreator.calculateReachingDefs完成,DDGCreatorin, out, gen 均由 HashMap<Object, HashSet<Object>> 构成,key为一个语句(CFG结点),value对应该语句定义的symbol集合。

Reaching Definition分析完成之后,算法会调用DDGCreator.createDDGFromReachingDef 构建数据依赖关系,此时 In[S] 中保存了能活跃到语句 S 中的符号定义集合 inForBlock 。算法会逐个访问CFG结点,对于每个CFG结点,算法会找出该CFG结点中使用的符号集合 usedSymbol,并将2个集合对应元素连上数据依赖边

private DDG createDDGFromReachingDefs()
{
		DDG ddg = new DDG();
		for (Object statement : cfg.getStatements())
		{
			HashSet<Object> inForBlock = in.getListForKey(statement);
			if (inForBlock == null)
				continue;
			List<Object> usedSymbols = cfg.getSymbolsUsed().getListForKey(
					statement);
			if (usedSymbols == null)
				continue;
			// inForBlock保存活跃到statement的symbol定义
			for (Object d : inForBlock)
			{
				Definition def = (Definition) d;
				// usedSymbol保存statement使用的symbol
				if (usedSymbols.contains(def.identifier))
					ddg.add(def.statement, statement, def.identifier);
			}
		}
		return ddg;
}

5.2.示例

以下面代码为例

void test()
{
	char* dst = (char*)malloc(sizeof(char)*100); // s1
	char source[100]; // s2
	memset(source, 'A', 100); // s3
	source[99] = '\0'; // s4
	memcpy(dst, source, 100); // s5
	*dst = 'A'; // s6
}
  • gen值如下:

    • gen[s1] = { (dst, s1) }
    • gen[s2] = { (source, s2) }
    • gen[s4] = { (*source, s4) }
    • gen[s6] = { (*dst, s6) }
  • in 值如下:

    • in[s1] = { }
    • in[s2] = { (dst, s1) }
    • in[s3] = { (source, s2), (dst, s1) }
    • in[s4] = { (source, s2), (dst, s1) }
    • in[s5] = { (*source, s4), (source, s2), (dst, s1) }
    • in[s6] = { (*source, s4), (source, s2), (dst, s1) }
  • 数据依赖图如下,实际上symbol *source*dst 并没有用上。

dst
dst
source
source
source
s1
s5
s6
s2
s3
s4
  • 13
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值