[论文笔记] FlowDroid: Precise Context, Flow, Field, Object-sensitive and Lifecycle-aware Taint Analysis for Android Apps

大名鼎鼎的 FlowDroid。而且作者之一 Steven Arzt 是个挺热心的哥们

Introduction

传统的 Java 静态分析工具面对 Android 应用有多个挑战:1. Android 的组件、生命周期和回调函数;2. 某些函数可能使用 XML 文件进行配置;3. 别名和虚调用(virtualInvoke)。过去的分析工具使用的欠近似或者过近似手段使得分析结果几乎不可用。

本文提出了:

  1. FlowDroid:上下文、字段和流敏感的静态分析工具,并且使用按需别名分析提高效率
  2. DroidBench:针对 Android 静态分析的基准测试

Background and example

image

sendMessage() 和 app 中的某个按钮的点击事件相绑定,静态分析应当能够识别这个回调函数以避免漏报。当 onRestart() 执行后,用户的密码会被写入 userpwd 字段,此时用户点击按钮会导致数据泄露。为了避免误报,分析必须对生命周期有清晰的建模,同时是字段敏感的。

Attacker model

FlowDroid 假设攻击者可能向 app 提供任意恶意的字节码。攻击者无法绕过 Android 平台的安全措施或利用侧信道。

Precise modeling of lifecycle

FlowDroid 生成一个虚拟的 Main 方法来模拟生命周期。

  1. FlowDroid 假设所有的 Android 组件可能以任意的顺序运行。FlowDroid 的静态分析基于 IFDS 框架,是天然路径不敏感的。
  2. Android 中回调可以通过调用 Android API 注册,也可以通过 XML 注册。FlowDroid 把所有的组件及其注册的回调关联起来,即回调会在组件的生命周期中执行分析。对于第一种注册方式,FlowDroid 首先为每个组件计算一个调用图,然后在调用图上发现新的回调,直到达到不动点为止。对于第二种注册方式,FlowDroid 分析每个 Activity,查看它注册了 XML 文件中的哪些标识符。

image

Precise flow-sensitive analysis

FlowDroid 使用 IFDS 处理污点分析。

image

Taint analysis

FlowDroid 使用访问路径(Access Path)处理字段敏感性,这意味着 AP 实际上是数据流分析的抽象域,污点会在 AP 上传播。具体的 flow function 要看 FlowDroid 的源代码实现。

On-demand alias analysis

image

taintIt() 的第一次调用使得 p.f 被污染了,这导致第二个 sink() 操作实际上是一个危险操作。FlowDroid 会对 x.f 启动一个反向查询,来查找 x.f 的所有别名,然后把 x.f 的所有别名标记为被污染的并重新启动正向污点分析(这可以通过往 worklist 里面添加一个新的 work 来实现)。

image

image

一个问题是,如果在反向分析中单纯的只查找 x.f 的所有别名而不考虑调用上下文会导致不精确性,因此 FlowDroid 把正向分析的上下文注入到反向分析中。在 Algorithm 1 的 16 行,把 \(\langle s_p, d_1 \rangle \rightarrow \langle n, d_3 \rangle\) 插入到反向分析的 worklist 中体现了这一点:\(langle s_p, d_1 \rangle\) 保存了这次函数调用上下文的状态。

image

另外的问题是,需要防止反向分析返回到未被正向分析分析的上下文,因此 FlowDroid 保证反向分析不会返回到调用位置。也就是说,当被查询别名的对象是当前方法的参数 p 的时候,反向分析不会回到 callsite 去找实际给出的参数 a,而会把参数 p 当作一个新的污点交给正向分析,正向分析会在 exit statement 的时候把 p 映射回 a

y = f(x); // x 被认为 tainted 并启动一个新的 forward analysis
n = f(m); // 无关的上下文
f(p) {
  // 找 p 的所有别名
}

这对应 Algorithm 2 的 11 ~ 14 行:当反向分析到达一个方法的头部时,会启动一个新的正向分析并注入上下文,当正向分析运行到函数的最后一条 exit 语句时,会把污点带回去。

此外,反向分析可能引入流不敏感的结果。比如例子:

image

显然只有第二个 sink() 才是真正危险的。但是反向查询会在第 1 行查询到 p2 被定义,然后正向分析会认为所有位置的 p2.f 都是污染的,第一个 sink() 也是危险的。为了解决这个问题,在触发反向分析时把语句标记为激活语句,每当反向分析再次生成正向分析,并且正向分析在其激活语句上传播别名污点时,该污点才会被激活。

Implementation

版本 2.14.1。实现依赖 Soot 和 Heros(IFDS 框架),实际的调试过程中遇到了一些问题

1

在运行 TaintBench 的 xbot 时,开启 -r 选项会导致 IFDS 错误:

[FlowDroid] ERROR heros.solver.CountingThreadPoolExecutor - Worker thread execution failed: Index 2 out of bounds for length 2
java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
        at soot.jimple.internal.AbstractInvokeExpr.getArg(AbstractInvokeExpr.java:71)
        at soot.jimple.infoflow.problems.InfoflowProblem$1$3.computeTargetsInternal(InfoflowProblem.java:618)
        at soot.jimple.infoflow.problems.InfoflowProblem$1$3.computeTargets(InfoflowProblem.java:518)
        at soot.jimple.infoflow.solver.fastSolver.InfoflowSolver.computeReturnFlowFunction(InfoflowSolver.java:92)
        at soot.jimple.infoflow.solver.fastSolver.InfoflowSolver.computeReturnFlowFunction(InfoflowSolver.java:1)
        at soot.jimple.infoflow.solver.fastSolver.IFDSSolver.processExit(IFDSSolver.java:522)
        at soot.jimple.infoflow.solver.fastSolver.InfoflowSolver.processExit(InfoflowSolver.java:140)
        at soot.jimple.infoflow.solver.fastSolver.IFDSSolver$PathEdgeProcessingTask.runInternal(IFDSSolver.java:751)
        at soot.jimple.infoflow.solver.fastSolver.LocalWorklistTask.run(LocalWorklistTask.java:27)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at java.base/java.lang.Thread.run(Thread.java:840)

正在排查问题中...看上去发生在 callToReturn 的 flow function,在获取 callee 中与 callsite 对应的第 i 个参数的时候超出了 callsite 中参数的数量:

originalCallArg = iCallStmt.getInvokeExpr().getArg(i);

原始方法,用 logger 打印了一下出错位置:

[FlowDroid] INFO soot.jimple.infoflow.problems.InfoflowProblem - Could not get argument 2 for call site $r6 = virtualinvoke $r4.<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>($r1, $r2) in <org.mozilla.javascript.MemberBox: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>
[FlowDroid] INFO soot.jimple.infoflow.problems.InfoflowProblem - Could not get argument 3 for call site $r6 = virtualinvoke $r4.<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>($r1, $r2) in <org.mozilla.javascript.MemberBox: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>

对应的源码:

/* access modifiers changed from: 0000 */
public Object invoke(Object target, Object[] args) {
    Method method = method();
    try {
        return method.invoke(target, args);
    }
    // ...
}

这里对 Java 的反射 API 做了一层封装。猜测错误原因是 FlowDroid 处理参数对应关系的时候出错,有一说一,看上去像是 im 写反了。

提了个 issue,这个 bug 已经被修复。Issue

2

开启了 -r-cg SPARK,对于 Class.forName 可以得到正确的 class 对象,但是对于 method.invoke soot 把 class 下所有的 public method 全部作为可能被调用的对象。

Example:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        try {
            // String methodNameFromFile = readMethodNameFromAssets();
            // if (methodNameFromFile != null && !methodNameFromFile.isEmpty()) {
            Method method = MainActivity.class.getMethod("reflectedMethod", String.class);
            method.invoke(this, "Hello from reflection!");
            MainActivity activity = MainActivity.class.newInstance();
            activity.simpleMethod("Hello from callsite!");
            // }
        } catch (Exception e) {
            Log.e("ReflectionError", "File-based reflection failed", e);
        }
    }

    public void reflectedMethod(String message) {
        Log.i("ReflectedMethod", "Message: " + message);
    }

    public void anotherReflectedMethod(String message) {
        Log.i("AnotherReflectedMethod", "Message: " + message);
    }

    public void deadReflectedMethod(String message) {
        Log.i("DeadReflectedMethod", "This method is never called.");
    }

    public void simpleMethod(String message) {
        Log.i("SimpleMethod", "Message: " + message);
    }

    private String readMethodNameFromAssets() {
        String methodName = "";
        try (java.io.InputStream is = getAssets().open("config.txt");
             java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(is))) {
            methodName = reader.readLine();
        } catch (java.io.IOException e) {
            Log.e("FileReadError", "Could not read config.txt", e);
        }
        return methodName;
    }
}

Callgraph:

"<com.example.myapplication.MainActivity: void onCreate(android.os.Bundle)>"->"<com.example.myapplication.MainActivity: void anotherReflectedMethod(java.lang.String)>" [label=virtualinvoke $r4.<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>(r0, $r5),];
"<com.example.myapplication.MainActivity: void onCreate(android.os.Bundle)>"->"<com.example.myapplication.MainActivity: void deadReflectedMethod(java.lang.String)>" [label=virtualinvoke $r4.<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>(r0, $r5),];
"<com.example.myapplication.MainActivity: void onCreate(android.os.Bundle)>"->"<com.example.myapplication.MainActivity: void reflectedMethod(java.lang.String)>" [label=virtualinvoke $r4.<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>(r0, $r5),];
"<com.example.myapplication.MainActivity: void onCreate(android.os.Bundle)>"->"<com.example.myapplication.MainActivity: void simpleMethod(java.lang.String)>" [label=virtualinvoke $r4.<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>(r0, $r5),];

假设 invoke 参与到污点流中,那么会出现一个假阳性。

3

FlowDroid 在 apk 上的分析过程?

posted @ 2025-11-30 19:27  sysss  阅读(24)  评论(0)    收藏  举报