FlowDroid获取APK的函数调用图

2 篇文章 0 订阅
2 篇文章 0 订阅

使用FlowDroid这种工具最糟心的就是看不懂的API文档和稀有的编程实例,事实上这个工具的功能很强大,但是工具的开发者并没有告诉用户如何去用。毕竟这个工具源自实验室,科研的产物大多缺乏工业产品式详细的应用指导。这篇博客和下一篇博客要介绍的是使用FlowDroid绘制APK的函数调用图(Call Graph)和每个函数的控制流图关于绘制dot图。这一篇博客先介绍获取函数调用图。

和画图相关的有三个类:DotGraph,DotGraphNode,DotGraphEdge,DotGraphAttribute和DotGraphUtil。关于以.dot格式结尾的文件在此不详述,给一个博客链接大家可以去了解关于dot文件的一切:http://liyanrui.is-programmer.com/posts/6261.html。

总之,dot文件就是一个以一种格式把图描述出来的文件,通过graphviz(一个可以将dot图可视化的工具,可以免费下载并使用)可以将这个文件可视化输出。上述三个类的作用就是将soot中的call graph,controlflow graph等其他种类的图转化成dot的格式,然后以一个.dot文件的形式输出。

DotGraph代表了一个逻辑上的dot图,即一个dot图数据结构,它由图的名称、图中的点集、边集以及图的属性集组成;

DotGraphNode这个结构比较简单,就一个字符串类型的结点名和一些属性;

DotGraphEdge略微复杂,里面除了一个属性集之外有两个DotGraphNode,一个叫start,另一个叫end,设计者想用起始点和终点的方式表示图中的一条边;

DotGraphAttribute最简单,里面两个字段,都是String类型的,一个叫id另一个叫value,有人说这个DotGraphAttribute是干啥的,其实简单的说就是对象的属性,比如一个结点我是画成椭圆的还是矩形的,边框的颜色设置成啥样,底色是什么样的,DotGraphAttribute里面的属性名就是id,属性值就是value。

Dot图的逻辑结构大抵如上所述,不再赘述,感兴趣的同学可以看看soot的源码。这时你发现还有一个DotGraphUtil没有介绍,不要着急,我们最后介绍这个类。

在阅读DotGraph,DotGraphNode,DotGraphEdge这三个类的时候,我们会发现它们里面都有一个名叫render的函数,这个函数是画图用的,在前面我们说过,这三个类其实是dot图的逻辑结构,而真正的dot图是以一定格式表述图的文件,这个render函数的作用就是为了把逻辑结构转化为dot规定的表述形式(语法规则)。在程序的世界里,把逻辑结构按照规定的语法规则转化成dot文件的描述形式,这里我们不妨将这转化好的信息称为“dot标准格式信息”,这时,这些“dot标准格式信息”是以字符串的形式存在于程序里的,我们最终的目的是把这些信息以文件的形式输出。我们知道,文件操作需要有文件输入输出流,拿到输入输出流之后还要使用write和read进行读写,在DotGraph有个plot函数就是主导将dot图输出这件事的,当plot被调用时,它做的第一件事就是打开文件输出流,设置要输出文件的路径;然后plot调用了render函数,这时,DotGraphUtil就派上用场了,render方法调用了DotGraphUtil里面的renderLine方法,如果你看过它的源码,你会发现里面都是公有静态方法,可以直接拿来用的。而且,在DotGraph,DotGraphNode,DotGraphEdge中的每一个render方法最后都调用了DotGraphUtil里面的renderLine方法,而这个方法的作用就是将上面说的“dot标准格式信息”写到输出流中里。

通过上面的叙述,我们已经对soot中输出dot文件的机制大致有所了解,然后我们就可以使用DotGraph这个类帮助我们画图了。我们先把一个APK 文件的call graph画出来,call graph是指一个程序中函数间的调用关系,每一个结点是一个函数,边代表一个函数对另一个函数的调用。

博客http://blog.csdn.net/liu3237/article/details/48827523中的例子是获取到一个安卓应用的call graph之后,使用.gexf的形式输出的,在这里我按照他的输出函数做了一个以.dot格式输出的方法。

我建立了一个工具类,其中的一个函数可以将call graph以dot的形式输出,同时,为了配合输出,我设立了两个静态变量,第一个一个是DotGraph类的对象,用它来存储构造好的dot图结构;第二个是一个map集合,key是一个表示结点名称的字符串,value代表这个结点是否已被访问过(true代表访问过,false代表没有被访问过)。在绘制call graph的函数中,传入参数有两个:一个CallGraph类对象和一个SootMethod类对象。第一个参数是解析过程完成后,我们通过soot提供的接口Scene.v().getCallGraph()获取到的CallGraph类对象,我们的目标就是根据这个CallGraph类对象提供的信息构造一个DotGraph;第二个参数是当前我们要处理的结点,其实就是源程序中的一个方法(在最开始调用该方法时,这个当前访问参数应该是整个应用的入口函数:DummyMain),在soot中使用SootMethod这个类来代表。整体来看,这个函数做了一件事:在一个CallGraph中访问其中的一个结点,然后递归的访问其前驱和后继。当我们访问CallGraph中的一个结点时,首先要获取其签名或者函数名称;然后将其加入之前提到过的那个静态map集合,表示该结点已经被访问过;然后调用DotGraph类的相关方法以这个SootMethod的信息(函数签名或者函数名)构造DotGraph结点。结点构造完之后,就该为结点添加相应的边。画边分为两个部分:一块是对当前访问的SootMethod的前驱结点,即它的调用者进行处理,另一块是对当前访问的SootMethod的后继结点,即它调用的SootMethod进行处理。这两块的处理策略并不相同,下面分开介绍。对于当前SootMethod前驱的处理策略是,获取到前驱(caller)方法集之后遍历他们,然后依次将它们作为参数之一递归调用本函数。对于当前SootMethod后继的处理策略是,获取到后继(callee)方法集之后遍历他们,构造从当前访问的SootMethod到该callee的边(使用DotGraph类中的方法即可),然后判断当前callee是否已被访问,若未被访问过则以其为参数递归调用本函数。

直接上代码,思路可以看代码里面的注释(英文写的不太标准请海涵)。

public class DotGraphUtil {
	
	//a collection store the visited point in a graph when iterate the graph
	public static Map<String, Boolean> visitedNodes = new HashMap<String, Boolean>();
	//an DotGraph object
	public static DotGraph dotGraph = new DotGraph("defult-dotgraph");
	
	public DotGraphUtil(){
		
	}
	
	/**
	 * output dot graph as a dot file
	 * @param fileName
	 * @param storeDir
	 * @param dotGraph
	 */
	public static void exportDotGraph(String fileName, String storeDir, DotGraph dotGraph){
		String outPath = storeDir + "/" + fileName + ".dot";
		dotGraph.plot(outPath);
	}

	/**
	 * represent call graph in the form of a dot graph
	 * @param callGraph: call graph of an APK
	 * @param curMethod: entry point of an APK (DummyMain)
	 */
	public static void CGToDotWithMethodsSignature(CallGraph callGraph, SootMethod curMethod){
		//if this is the first to call this function, method is start point
		String sigOfCurNode = curMethod.getSignature();
		//iterate the call graph starting from the startPoint, after visited a node, put it into the visitedNodes collection
		visitedNodes.put(sigOfCurNode, true);
		//draw the current node into call graph
		dotGraph.drawNode(sigOfCurNode);
		
		//after draw a node into datGraph, we can set some attribute for the node
		
		//get the callers of current node, that is to say, find out methods who have edge point into current method
		Iterator<MethodOrMethodContext> callersOfMethod = new Targets(callGraph.edgesInto(curMethod));
		if(callersOfMethod != null){
			while(callersOfMethod.hasNext()){
				SootMethod predecessor = (SootMethod) callersOfMethod.next();
				if(predecessor == null){
					System.out.println("This method is not existed!");
				}
				//if this predecessor of current node has not been visited, visit it! 
				if(!visitedNodes.containsKey(predecessor.getSignature())){
					CGToDotWithMethodsSignature(callGraph, predecessor);
				}
			}
		}
		
		//get the callees of current node, that is to say, get the methods called by curMethod
		Iterator<MethodOrMethodContext> calleesOfMethod = new Targets(callGraph.edgesOutOf(curMethod));
		if(calleesOfMethod != null){
			while(calleesOfMethod.hasNext()){
				SootMethod successor = (SootMethod) calleesOfMethod.next();
				if(successor == null){
					System.out.println("This method is not existed!");
				}
				//draw this callee into DotGraph
				dotGraph.drawNode(successor.getSignature());
				//add an edge from currentNode to this successor
				dotGraph.drawEdge(sigOfCurNode, successor.getSignature());
				//if this successor of currentNode has not been visited, visit it!
				if(!visitedNodes.containsKey(successor.getSignature())){
					CGToDotWithMethodsSignature(callGraph, successor);
				}
			}
		}
	}
	
	public static void CGToDotWithMethodsName(CallGraph callGraph, SootMethod curMethod){
		//if this is the first to call this function, method is start point
		String nameOfCurNode = curMethod.getName();
		//iterate the call graph starting from the startPoint, after visited a node, put it into the visitedNodes collection
		visitedNodes.put(nameOfCurNode, true);
		//draw the current node into call graph
		dotGraph.drawNode(nameOfCurNode);
		
		//after draw a node into datGraph, we can set some attribute for the node
		
		//get the callers of current node, that is to say, find out methods who have edge point into current method
		Iterator<MethodOrMethodContext> callersOfMethod = new Targets(callGraph.edgesInto(curMethod));
		if(callersOfMethod != null){
			while(callersOfMethod.hasNext()){
				SootMethod predecessor = (SootMethod) callersOfMethod.next();
				if(predecessor == null){
					System.out.println("This method is not existed!");
				}
				//if this predecessor of current node has not been visited, visit it! 
				if(!visitedNodes.containsKey(predecessor.getName())){
					CGToDotWithMethodsName(callGraph, predecessor);
				}
			}
		}
		
		//get the callees of current node, that is to say, get the methods called by curMethod
		Iterator<MethodOrMethodContext> calleesOfMethod = new Targets(callGraph.edgesOutOf(curMethod));
		if(calleesOfMethod != null){
			while(calleesOfMethod.hasNext()){
				SootMethod successor = (SootMethod) calleesOfMethod.next();
				if(successor == null){
					System.out.println("This method is not existed!");
				}
				//draw this callee into DotGraph
				dotGraph.drawNode(successor.getName());
				//add an edge from currentNode to this successor
				dotGraph.drawEdge(nameOfCurNode, successor.getName());
				//if this successor of currentNode has not been visited, visit it!
				if(!visitedNodes.containsKey(successor.getName())){
					CGToDotWithMethodsName(callGraph, successor);
				}
			}
		}
	}
}


上面的例子中有两个构造dot图的函数,第一个是将每个SootMethod的签名作为每一个dot图结点的表述的,一般的函数签名可能会比较长,这样dot图显示出来可能会比复杂,不好看,因此我又写了第二个函数,它可以只通过函数名称构造dot图,原理都一样,不赘述。这样可以缩减dot结点中的内容长度,使图的整体看起来更加简单易读。

我们来看看在mac下的分析结果。

这是使用函数签名构造的dot图,因为我分析的apk逻辑可能有点复杂,再加上签名本身就长,所以从全局看这个图就很小,我们可以放大看看局部。

看到这个图你大概知道方法签名是什么了,同时你还看到了好多没见过的init和finalize之类的函数,这个我们在后面的博客会简单介绍。其实也不用知道具体是什么,你大概能猜出来。

最后我们看看用方法名构造的call graph吧。

这样看起来规模就小很多,因为内容变短了,每个结点里就一个函数名,没别的。

这篇博客我们已经能够成功的获取apk的函数调用图,下一篇博客我会介绍获取call graph中所有方法的控制流图。

评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值