为FlowDroid加上addJavascriptInterface分析(下)(未完待续)

其实我们最初的目的已经实现了,本文讲如何实现对callback的精准标记Source,最后讲一下对整个FlowDroid代码的理解和评价。

一、CallbackSourceMode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static enum CallbackSourceMode {
/**
* Callback parameters are never treated as sources
*/
NoParametersAsSources,
/**
* All callback parameters are sources
*/
AllParametersAsSources,
/**
* Only parameters from callback methods explicitly defined as sources are
* treated as sources
*/
SourceListOnly
}

这个属性只出现在checkCallbackParamSource 里,用于判断某些parameter是否是source。在NoParametersAsSources 和AllParametersAsSources 都好理解,这个SourceListOnly困扰了我很久,直到写完本文都不知道这是个啥,而且这个值是默认值,经过阅读代码和写测试,结论是SourceListOnly 是一个废掉的功能,而且会导致NullPointerException 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SourceSinkDefinition sourceSinkDef = this.sourceMethods.get(def.getParentMethod());
if (sourceSinkDef instanceof MethodSourceSinkDefinition) {
MethodSourceSinkDefinition methodDef = (MethodSourceSinkDefinition) sourceSinkDef;
if (sourceSinkConfig.getCallbackSourceMode() == CallbackSourceMode.SourceListOnly
&& sourceSinkDef != null) {
// Check the parameter index
if (methodDef.getParameters().length > paramRef.getIndex()) {
Set<AccessPathTuple> apTuples = methodDef.getParameters()[paramRef.getIndex()];
if (apTuples != null && !apTuples.isEmpty()) {
for (AccessPathTuple curTuple : apTuples)
if (curTuple.getSourceSinkType().isSource())
return sourceSinkDef;
}
}
}
}

先在sourceMethods 里查询,key是getParentMethod ,就是这个参数所在的方法名。而sourceMethods是在解析SourcesAndSinks.txt 时构建的。以onClick为例,必须要将OnClickListener.onClick 加入到配置文件中,才可以作为索引被查到。 之后做一些简单的判断,如果方法的参数总数大于参数的下标,直接return掉,否则返回对应的SourceSinkDefinition 。恩,看起来比较合理,但问题也在这里,methodDef 是查表查到的,很久之前就被初始化,methodDef.getParameters 返回是null 。 那么是否是初始化过程出现问题了呢?在SetupApplication.runInfoflow() 中,调用PermissionMethodParser.fromFile() ,解析配置文件并且对sourceMethods进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (AndroidMethod am : methods.values()) {
SourceSinkDefinition singleMethod = new MethodSourceSinkDefinition(am);

if (am.getSourceSinkType().isSource())
sourceList.add(singleMethod);
if (am.getSourceSinkType().isSink())
sinkList.add(singleMethod);
if (am.getSourceSinkType() == SourceSinkType.Neither)
neitherList.add(singleMethod);
}

public MethodSourceSinkDefinition(SootMethodAndClass am) {
this(am, null, null, null, CallType.MethodCall);
}

直接使用了构造方法,这个构造方法的getParameters 本来就是空,长期都没有人对其进行赋值,所以nullptr很好理解,这也佐证了我的观点,这个功能就是个辣鸡功能啊,没有任何用处,还不如用AllParametersAsSources ,简单粗暴。

二、精准标记参数

举个例子

1
2
3
4
5
void onReceive(Context ctx, Intent intent){
ctx.startActivity(intent);
Intent i = new Intent();
ctx.startService(i);
}

显然第一个参数不是我们需要的,而AllParametersAsSources会把它也标记了,但ctx 是没有任何用处的,会导致额外的误报。例如在startActivity 调用时,就会说来自2个source,分别是ctx 和intent ,在startActivity时参数并不是我们可以控制的,但却被认为是合理的sink点。所以必须要精准标记来减少误报。 理论上可以针对每个方法的每个参数进行过滤,但是写起来太复杂,而且配置文件也不好写。于是实现了稍微懒一点的方法,按照类名进行过滤,加个黑白名单。稍微写写配置文件和filter即可,懒得贴代码了,就在AbstractCallbackAnalyzer.analyzeClassInterfaceCallbacks 里,加一层filter即可,很简单。

三、FlowDroid流程总结

啊,这部分应该是很长很长的,不知道需要写多久,先占个坑,慢慢往上添,本文应该是全网第一篇吧,哈哈哈。

1、比较重要的配置选项 主要列举一下我关注的选项,因为这个非常多,而且部分功能是作者默认没有开启的。

app.getConfig().getSourceSinkConfig().setLayoutMatchingMode(XX);

针对某些回调函数的参数是否被标记为Source,有三个选项,NoMatch、MatchAll、MatchSensitiveOnly。例如OnClickListener.onClick(View view)中的view是否被认为是source,默认是MatchSeneitiveOnly,实际测试过程中发现这个选项是坏的,所以通常是开启为MatchAll,再加上一些自己的判断。

app.getConfig().getSourceSinkConfig().setEnableLifecycleSources(XX);

组件生命周期中的参数是否被认为是 Source,当我们关心exported组件时候,这个要设置为true。

app.getConfig().setMergeDexFiles(true);

针对multi-dex的处理,当然是需要的。

app.getConfig().setEnableRefection(false);

是否开启反射的检测(似乎没啥用,不知道怎么用) 建议关掉,因为开着的话会造成各种各样莫名其妙的 crash,经常在OnFlyCallGraph里和OnFlyCallGraphBuilder里发生。

app.getConfig().getPathConfiguration().setPathReconstructionMode(InfoflowConfiguration.PathReconstructionMode.Fast);

重建路径时的选项,首先会影响效率,因为要回溯一下从sink到source的路径,这里有三个选项,无/高速/高质量,区别也不大清楚,我只知道Precise慢的要死,真的要死,在Result中可以拿到ResultSourceInfo后可以 getPath 拿各个句子的Unit。

app.getConfig().getSourceSinkConfig().setLayoutMatchingMode(InfoflowAndroidConfiguration.LayoutMatchingMode.NoMatch);

是否将findViewById的结果作为source。作者可能用于监视某些View,我觉得不大需要,直接关掉。

1
2
3
4
pathReconstructionTimeout
resultSerializationTimeout
dataFlowTimeout
callbackAnalysisTimeout

几个timeout,防止跑如死循环(这个东西还是经常跑死的)

2、基础用法 FlowDroid主要用于检测代码的可达性,可用于检测信息泄露,例如在 Log 里打印了明文的密码(手动艾特 Twitter,哈哈哈),处理从Sources到Sinks之间的连接。 举个简单的例子,本文也会经常使用这个例子。

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

String s = "hello".toLowerCase();
Log.d("TAG", s);
}
}

这时候在SourceSink.txt里写

1
2
<java.lang.String: java.lang.String toLowerCase()> -> _SOURCE_
<android.util.Log: int e(java.lang.String,java.lang.String)> -> _SINK_

source 的意思就是这个变量作为起点,sink 的意思是这个函数调用作为终点,如果 sink 的参数里是被 taint 过的变量的话,就认为找到了一条路径。 在这个例子里,就表示所有的toLowerCase的结果都是source,将来被打印到log里。OK,我们提高一下难度,如果这个string后来被经过了处理,被放到了其他变量里,调用某些方法,是否还能被追踪到呢?这就涉及到了WrapTaintter。稍微复杂一点。

1
2
3
4
5
6
7
8
9
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String s = "hello".toLowerCase();
String foo = s + "foo";
String bar = foo + "bar";
Log.d("TAG", bar);
}

这时候FlowDroid也是可以测到的,加法在这里被翻译成了StringBuilder.append操作,因为默认的 EasyTaintWrapper.txt里有以下几行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<java.lang.StringBuilder: void <init>(java.lang.String)>
<java.lang.StringBuilder: java.lang.StringBuilder append(boolean)>
<java.lang.StringBuilder: java.lang.StringBuilder append(char)>
<java.lang.StringBuilder: java.lang.StringBuilder append(char[])>
<java.lang.StringBuilder: java.lang.StringBuilder append(char[],int,int)>
<java.lang.StringBuilder: java.lang.StringBuilder append(double)>
<java.lang.StringBuilder: java.lang.StringBuilder append(float)>
<java.lang.StringBuilder: java.lang.StringBuilder append(int)>
<java.lang.StringBuilder: java.lang.StringBuilder append(long)>
<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.Object)>
<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>
<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.CharSequence)>
<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.CharSequence,int,int)>
<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.StringBuffer)>

比如String foo = s + "foo",翻译一下就是

1
2
3
4
$r1 = new StringBuilder();
$r1.append(String)<s>;
$r1.append(String)<foo>;
$foo=$r1.toString();

因为有<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>,如果参数是被taint过的话,那么StringBuilder在调用过这句话时候,也会被 taint 掉。再翻译到java的话,因为s被污染了,foo也被污染了,bar 也被污染了,所以最后仍然是可以找到路径的。 同理,再举个例子,对Map的一些操作,当元素被污染时,Map 本身也会被污染,也是在TaintWrapper里实现的。 嗯,这就是最基本的用法了,可以根据需要添加自己的 source/sink/taint,有个坑点就是对于继承关系的处理,是否被 taint 还需要多写 testcase 去测一下(例如InputStream系列)。

3、运行流程 入口在SetupApplication.runInfoflow,先解析SourceSink的定义文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (sourceSinkFile == null || sourceSinkFile.isEmpty())
throw new RuntimeException("No source/sink file specified for the data flow analysis");
String fileExtension = sourceSinkFile.substring(sourceSinkFile.lastIndexOf("."));
fileExtension = fileExtension.toLowerCase();

ISourceSinkDefinitionProvider parser = null;
try {
if (fileExtension.equals(".xml")) {
parser = XMLSourceSinkParser.fromFile(sourceSinkFile,
new ConfigurationBasedCategoryFilter(config.getSourceSinkConfig()));
} else if (fileExtension.equals(".txt"))
parser = PermissionMethodParser.fromFile(sourceSinkFile);
else if (fileExtension.equals(".rifl"))
parser = new RIFLSourceSinkDefinitionProvider(sourceSinkFile);
else
throw new UnsupportedDataTypeException("The Inputfile isn't a .txt or .xml file.");
} catch (SAXException ex) {
throw new IOException("Could not read XML file", ex);
}

我用的是 txt 版本的配置文件,parser 主要就是解析文件从而生成Set<SourceSinkDefinition>,用于标记特定的source/sink。SourceSinkDefinition 是一个很重要的类,构造方法有多种,这里使用的是

1
2
3
public MethodSourceSinkDefinition(SootMethodAndClass am) {
this(am, null, null, null, CallType.MethodCall);
}

这种构造方法是没有定义baseObjects、parameters、retValue 的,将来在寻找Source/Sink时可能有坑。因为baseObjects、parameters需要上下文关系,我们解析文件时候还没有开始分析,这种SourceSinkDefinition是不完整的。在之后分析CallGraph时候也会产生新的SourceSinkDefinition,那种才是完整的。