为FlowDroid加上addJavascriptInterface分析(中)

接上篇,上篇主要遇到问题是Source无法对Parameter进行标记,本文两部分,第一部分讲如何加上标记并且完成整个CFG(隔的时间太长了,不大记得这个操作了),第二部分讲通过加入Callback的方式让JSInterface的代码变得可达,并且成功实现预期。

一、为什么直接标记JIdentifyStmt不被认为是Source呢

先看代码,AndroidSourceSinkManager.java

1
2
3
4
5
6
7
8
9
10
11
protected SourceSinkDefinition getSource(Stmt sCallSite, IInfoflowCFG cfg) {
assert cfg != null;
assert cfg instanceof BiDiInterproceduralCFG;

SourceSinkDefinition def = null;
if ((!oneSourceAtATime osaatType == SourceType.MethodCall) && sCallSite.containsInvokeExpr()) {
// This might be a normal source method
final SootMethod callee = sCallSite.getInvokeExpr().getMethod();
def = getSourceDefinition(callee);
if (def != null)
return def;

输入是sCallSite ,就是整个dummyMain 的每条语句,上来就先判断了是否containsInvokeExpr() ,而JIdentifyStmt 是一个声明语句,当然不包含方法调用,所以第一句就被毙了。如果要使用这个分支的话,可以考虑将需要被taint的参数进行一次自己定义的clone的操作,对clone出来的参数标记为source。例如 func(String input) 时候,写为input = input.toString() ,此时将toString 作为产生source的method,就可以“曲线救国”,将参数进行标记。

这里涉及到三种Stmt,JIdentityStmt ——用于对参数赋值,JInvokeStmt ——用于调用但不赋值,JAssignStmt ——用于调用并且赋值。

二、曲线救场

思路就是这个思路,主要修改地方仍然是AndroidEntryPointCreator ,原本我们只是插入js远程调用的语句,测试时候参数本来也全都是String,所以这里额外插入一句,将每个参数都执行一遍toString ,对于自定义的复杂对象,可以使用SootClass.addMethod ,加入一个自定义的方法,只需要返回其本身即可,因为内部我们也会不进行分析,所以这样做还算完备。

构造出来的方法大概长这样

1
2
3
$r0 = @this: JBridge
$r1 = @parameter0: java.lang.String
$r1 = virtualinvoke $r1.<java.lang.String: java.lang.String toString()>()

此时,我们讲toString标记为Source,其返回值$r1 就可以被标记为Source。经过测试,确实能够在log里看到多出来的这个Source。但是,不会被寻路到Sink,什么概念呢,就是Source和Sink看起来是连着的,但是无法被forwardProblem 进行solve。这里思考了比较久,用了其他的几种方法,例如对比我们手动生成的$r1.toString 句子和soot反汇编出来$r1.toString的句子有什么区别;例如进行替换或者微小修改后,看是否能够正常运行;例如使用复制的方式构造stmt替换掉原来的stmt句子。

大量的证据表明,我们的stmt构造方式是没有问题的,没有被寻路寻到,很可能是我们添加stmt的前,callGraph 早就生成了而且没有被更新过,导致创造的语句虽然是Source,却是孤立的节点,没有上下文。

三、将JsInterface视为Callback处理

之后尝试了一些刷新cfg 的方法,后面也没有结果,思路也差不多用完了,可能需要换一条路了。正一筹莫展的时候,yufei说他搞定了,使用的是手动加入callback的思路。

FlowDroid有一个运行参数叫做,"-cs" ,表示CallbackSourceMode ,默认是SourceListOnly ,基本没啥用,改为AllParameters 时会主动标记回调方法的所有参数。举个例子,下面的代码,在默认时是没有检测结果的,设置为AllParameters就会有检测结果。

1
2
3
4
5
@Override
public void onReceive(Context context, Intent intent) {
Log.d("para1", context.toString());
Log.d("para2", intent.toString());
}

(为什么我要这样写呢,因为我发现了FlowDroid的一个bug,para2 那句其实是不会扫描出结果的,文末我会讲一下bug的产生和修复) 反思一下我的失败操作和yufei的成功操作,还是最开始思路的问题,我选了一条很长的调用链,修改的东西太多,本以为是个小坑,但后来发现是大坑,到最后全都填完了,也不知道为什么没有成功。另外,没有注意到CallbackSourceMode 的配置问题,所以也没有注意到getSource 后面的对于callback 判断的代码,导致绕了远路。还有就是对FlowDroid的整体架构没有那么了解,错误预估了难度,以后还是得先看再写。

四、代码实现

ok下面讲一下实现方式,主要涉及2个文件,AbstractCallbackAnalyzer 和DefaultCalbackAnalyzer 。

针对callback的分析,总入口在SetupApplication 里,跳到DefaultCallbackAnalyzer.collectCallbackMethods() ,针对每个class进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (SootClass sc : entryPointClasses) {
// Check whether we're still running
if (isKilled != null)
break;

List<MethodOrMethodContext> methods = new ArrayList<MethodOrMethodContext>(
getLifecycleMethods(sc));

// Check for callbacks registered in the code
analyzeRechableMethods(sc, methods);

// Check for method overrides
analyzeMethodOverrideCallbacks(sc);
}

同样是analyzeReachableMethod ,对每个方法进行分析,只需要关注其中的analyzeMethodForCallbackRegistrations 。

1
2
3
4
5
6
7
8
9
10
while (reachableMethods.hasNext()) {
// Check whether we're still running
if (isKilled != null)
break;

SootMethod method = reachableMethods.next().method();
analyzeMethodForCallbackRegistrations(lifecycleElement, method);
analyzeMethodForDynamicBroadcastReceiver(method);
analyzeMethodForFragmentTransaction(lifecycleElement, method);
}

原本的逻辑是将方法的每个参数确认一遍,是否为Set<String>androidCallbacks 里的成员,这个类是读取AndroidCallback.txt 里的每一行,例如View.OnClickListener 就是其中的一个。

以func(ClickListener obj) 为例,发现obj属于OnClickListener ,对其稍加判断,加入到Set<SootClass>callbackClasses 中,将来做一些额外的处理。这种回调的特点是无法确定是哪个方法设置了这个listener,所以存储的是obj的SootClass。

对于JsInterface的类,有个特点,它的位置特别固定,一定是addJavascriptInterface 的第一个参数的类,所以考虑这里加一层白名单,尽可能优雅地进行改动。代码不难写,先判断方法名,如果匹配了,就直接加到callbackClasses 里。

这时候,信息集中到了callbackClasses 里,寻找引用,发现在AbstractCallbackAnalyzer.analyzeClassInterfaceCallback ,会对MultiMap<SootClass, CallbackDefinition> callbackMethods 进行添加。

以MyOnClickListener 为例,找到其父类的interface,发现是onClick 这个方法,之后用MyOnClickListener 的onClick 方法建立CallbackDefinition ,加入到callbackMethods 中。

类比到JsInterface,并不是使用父类的interface作为入口的,而是使用JavascriptInterface 这个annotation ,所以需要使用一些代码来判断并且添加,如下的代码。

这时候,全部信息都集中到了callbackMethods 里,跟踪其引用,发现在AndroidSourceSinkManager.checkCallbackParameterSource 里,返回MethodSourceSinkDefinition 。这个方法会根据CallbackSourceMode 进行不同的行为,这里直接选择ALL的话,就可以自动将各个remote-js的参数标记为source了。

但我们想在无论哪种模式下,都对这种本来就是source的东西标记下,就加一层判断咯。

1
2
3
4
5
6
7
8
9
10
11
12
13
// If JavascriptInterface, all parameters are sources
boolean hasJsAnnotation = false;
VisibilityAnnotationTag tag = (VisibilityAnnotationTag) def.getParentMethod().getTag("VisibilityAnnotationTag");
if (tag != null) {
for (AnnotationTag annotation : tag.getAnnotations()) {
if (annotation.getType().equals("Landroid/webkit/JavascriptInterface;")) {
hasJsAnnotation = true;
break;
}
}
}
if(hasJsAnnotation)
return MethodSourceSinkDefinition.createParameterSource(paramRef.getIndex(), CallType.Callback);

恩,加上这三处patch,我们的功能就完全实现啦!是不是很简单呢~撒花~✿✿ヽ(゚▽゚)ノ✿✿

五、文末彩蛋——FlowDroid的一个bug

还记得这个testcase吗

1
2
3
4
5
@Override
public void onReceive(Context context, Intent intent) {
Log.d("para1", context.toString());
Log.d("para2", intent.toString());
}

测一下,para2 其实是打印不出来的,而且Intent intent 也没有被标记为source,这个显然不合理嘛。跟了很久,发现是AccessPathBasedSourceSinkManager.createSourceInfo 里的一个check过不去。(千万别看AndroidSourceSinkManager 的方法,那个被Override掉了)

1
2
3
4
5
6
7
8
9
10
11
12
case Callback:
if (sCallSite instanceof IdentityStmt) {
IdentityStmt is = (IdentityStmt) sCallSite;
if (is.getRightOp() instanceof ParameterRef) {
ParameterRef paramRef = (ParameterRef) is.getRightOp();
if (methodDef.getParameters() != null && methodDef.getParameters().length > paramRef.getIndex())
for (AccessPathTuple apt : methodDef.getParameters()[paramRef.getIndex()])
aps.add(getAccessPathFromDef(is.getLeftOp(), apt, manager, false));
}
}
break;

这里判断了methodDef.getParameters.length 和 paramRef.getIndex ,在Callback 这个case里,前者永远是1,而且来自于createParameterSource 方法里的AccessPathTuple.getBlankSourceTuple() ,本身含义是空。而后者表示正在处理的参数是第几个。举个例子,onReceive(Context ctx, Intent intent) ,第一次经过这里时候,ctx 满足条件,打上了source标记;第二次经过时intent 作为第一个参数,1==1 ,过不去,就没有被标记。

对啊,这什么逻辑,显然不合理啊,根据猜测,这里的代码应该是另一处复制来的,本意为了检查当前处理的param 是否越界,但getParameters 的初始化有问题,导致这里是错的。

getParameters 返回的是可达路径,callback 一般是不可达的,所以这里是空路径,直接去掉这个check即可。而且整个project也只有这一处用到了Callback 类型的返回值,所以这样修复是没有任何问题的。

修复后,就可以标记全部的参数、而不是第一个参数啦。

六、总结

还有一点想法需要写,还是这种source的标记问题,onReceive 的第一个参数Context 其实是不需要被标记的,精准的callback标记并没有配置文件和选项。可以考虑对某些class加黑名单,或者checkCallbackParameterSource 时指定参数位置。

下篇时,讲一下如何实现精准的标记Source。