#lab

lab2 soot

developer

lab2 soot静态分析

  • task1 活跃变量分析
  • task2 死代码检测

lab2 task1 活跃变量分析

soot环境搭建

soot是什么

soot是一个字节码分析工具,它输入编译好的java字节码(class文件),并提高到中间语言“jimple”。

我一开始以为soot就是一个exec了,可以直接在命令行中soot -[options] file。但并非如此,soot是一整套工具,像一个工具箱,可以在项目中import它。soot也可以以jar包的形式出现,在命令行中使用。

获得soot

从github上获得源码即可,https://github.com/soot-oss/soot/

我获得的版本是4.6.0,将它放在~/lab2/soot-4.6.0

用maven管理soot

soot支持用maven构建,可以用maven把soot当作依赖引入项目,然后就能调用soot的工具了。可以直接用jar包安装soot,不过我在lab1时先尝试了自行构建,记录如下。

应该可以直接在IDEA中构建soot,在maven -> lifecycle -> install。但是不知为何没有成功,似乎是卡住了什么测试用例。于是在命令行中安装。

cd ~/lab1/soot-4.6.0
mvn clean install
ls ~/.m2/repository/org/soot-oss

按理说这就可以了,但是我之前由于有过限定maven版本和路径的经历,导致mvn的种种配置十分混乱。虽然输出了Build Success,但是根本找不到soot在哪。尽管如此,soot/target中仍然生成了jar包。所以接下来手动安装它

mvn install:install-file \        
  -Dfile=target/sootclasses-trunk.jar \
  -DgroupId=org.soot-oss \
  -DartifactId=soot \
  -Dversion=4.6.0 \
  -Dpackaging=jar

还是Build Success之后~/.m2里什么都没有,折腾了半天,发现控制台输出了安装路径,给安装到之前毕设项目的依赖目录里了。把soot拷贝进~/.m2对应的位置就可以了。所以说还是得看看控制台到底输出了什么,不能字多就不看。

cp -r ~/IdeaProjects/GP1/repo/org/soot-oss ~/.m2/repository/org/soot-oss

现在终于可以愉快地用maven管理soot了。只需要在pom.xml中引入依赖

<dependencies>
    <dependency>
        <groupId>org.soot-oss</groupId>
        <artifactId>soot</artifactId>
        <version>4.6.0</version>
    </dependency>
</dependencies>

maven刷新一下,就可以import soot.xxx了。

构建测试用例

IDEA新建一个java项目lab2,注意jdk版本别太高,因为soot似乎只支持到java11。我一开始无脑选了1.8,但是后面报错了“类文件具有错误的版本 55.0, 应为 52.0”,所以还是得java11。添加soot依赖后,就完成了准备工作。

soot分析活跃变量

项目结构

使用/src/main/java/task1包作为分析的目录。SootInit.java初始化soot并作为入口,LiveVariables.java实现分析活跃变量的功能。

初始化soot

首先,需要指定soot分析的主类和工作的目录,以及想要分析的方法名。

String className = "lab3"; 
String classPath = "/Users/anpoliros/lab2/target";
String methodSignature = "void func1(int,int)";

两个注意事项,一是className不要加上.class后缀,这个typo很让人无语;二是不知何故lab2给的class文件的文件名和主类名不一致,需要把文件名改为lab3.class。一开始没有反编译,导致soot根本加载不了任何类,浪费了不少时间。

需要运行前指定的变量就这两个。除此之外还需要设置soot选项,而且为了debug我还加入了一段print功能,打印所有加载进来的类。这之后就可以调用LiveVariables的方法来分析活跃变量了。

soot框架机制

在 soot 中,每个方法会被转化为一个控制流图 UnitGraph,节点是 Jimple 中的语句(Unit),边是“下一条可能执行语句”。要分析活跃变量,需要进行逆向数据流分析,而soot提供了逆向数据流分析的框架BackwardFlowAnalysis<Unit, Set<Local>>。其中

  • Unit代表一个Jimple语句,也就是控制流图的节点
  • Set<Local>是活跃变量的集合,也就是这个语句处的数据流状态

另外,soot提供了框架自动调用机制。BackwardFlowAnalysis是一个框架类,其核心逻辑封装在doAnalysis方法中,我们需要做的就是实现一些方法,然后让它来调用并执行逆向分析。

public abstract class BackwardFlowAnalysis<N, A> extends FlowAnalysis<N, A> {
    public void doAnalysis() {
        // 自动迭代所有节点
        // 调用 flowThrough, merge, copy 等函数
    }
}
public class LiveVariables extends BackwardFlowAnalysis<Unit, Set<Local>>{
    public LiveVariables(UnitGraph graph) {
        super(graph);
        doAnalysis();
    }
    @Override
    protected void flowThrough(Set<Local> out, Unit unit, Set<Local> in){}
}

总的来说,soot框架做了如下的事情:

  1. 从所有出口节点开始进行逆向分析
  2. 初始化每个节点的 in/out 状态为默认值(通过 newInitialFlow())
  3. 重复迭代节点:
    • 调用 flowThrough 计算 in/out
    • 判断集合是否有变化
  4. 直到所有 in/out 集合不再变化(达到“固定点”)

flowThrough

LiveVariables的核心逻辑在flowThrough中实现。我们的目标是,已知语句 s 的 out[s](执行后活跃变量),要推导出 in[s](执行前活跃变量)。而soot会分析出定义变量集合def[s]和使用变量集合use[s],所以核心的公式就是in[s] = (out[s] - def[s]) ∪ use[s]。

flowThrough的结构大致如下:

  • 输入:入集in[s],出集out[s],语句单元s
  • in[s] = (out[s] - def[s]) ∪ use[s]
  • 将in/out保存在map中,两个map分别保存每条语句s前后的活跃变量集合

实现

SootInit.java

package task1;

import soot.*;
import soot.options.Options;
import soot.toolkits.graph.BriefUnitGraph;
import soot.jimple.JimpleBody;
import task1.LiveVariables;

public class SootInit {
    public static void main(String[] args) {
        String className = "lab3";
        String classPath = "/Users/anpoliros/lab2/target";

        // 1. 设置Soot基本选项
        G.reset();

        Options.v().set_prepend_classpath(true);
        Options.v().set_soot_classpath(classPath + ":" + Scene.v().defaultClassPath());
        Options.v().set_output_format(Options.output_format_none); // 不生成输出文件
        Options.v().set_allow_phantom_refs(true);
        Options.v().set_whole_program(true);
        Options.v().set_keep_line_number(true);

        // 2. 加载目标类
        SootClass sClass = Scene.v().loadClassAndSupport(className);
        sClass.setApplicationClass();
        Scene.v().loadNecessaryClasses();
        for (SootMethod m : sClass.getMethods()) {
            System.out.println("FOUND: " + m.getSignature());
        }//验证
        System.out.println("-----LOAD FINISHED-----");

        // 3. 遍历所有方法
        for (SootMethod method : sClass.getMethods()) {
            if (!method.isConcrete()) continue; // 跳过抽象或native方法

            System.out.println("🟦🟦🟦Analyzing: " + method.getSignature());

            try {
                Body body = method.retrieveActiveBody();

                // 4. 构建控制流图
                BriefUnitGraph cfg = new BriefUnitGraph((JimpleBody) body);

                // 5. 执行活跃变量分析
                LiveVariables analysis = new LiveVariables(cfg);

                // 6. 打印结果
                analysis.printResults();

                System.out.println("--------------------------------");
            } catch (Exception e) {
                System.err.println("Failed to analyze " + method.getSignature() + ": " + e.getMessage());
            }
        }
    }
}

LiveVariables.java

package task1;

import soot.Local;
import soot.Unit;
import soot.toolkits.graph.UnitGraph;
import soot.toolkits.scalar.BackwardFlowAnalysis;

import java.util.*;

public class LiveVariables extends BackwardFlowAnalysis<Unit, Set<Local>> {
    private final Map<Unit, Set<Local>> unitToInSet = new LinkedHashMap<>();
    private final Map<Unit, Set<Local>> unitToOutSet = new LinkedHashMap<>();

    public LiveVariables(UnitGraph graph) {
        super(graph);
        doAnalysis();
    }

    @Override
    protected void flowThrough(Set<Local> out, Unit unit, Set<Local> in) {
        // in[s] = (out[s] - def[s]) ∪ use[s]
        Set<Local> uses = new HashSet<>(unit.getUseBoxes().size());
        Set<Local> defs = new HashSet<>(unit.getDefBoxes().size());

        unit.getUseBoxes().forEach(vb -> {
            if (vb.getValue() instanceof Local) {
                uses.add((Local) vb.getValue());
            }
        });

        unit.getDefBoxes().forEach(vb -> {
            if (vb.getValue() instanceof Local) {
                defs.add((Local) vb.getValue());
            }
        });

        Set<Local> temp = new HashSet<>(out);
        temp.removeAll(defs);
        temp.addAll(uses);
        in.clear();
        in.addAll(temp);

        // 保存 in/out
        unitToInSet.put(unit, new HashSet<>(in));
        unitToOutSet.put(unit, new HashSet<>(out));
    }

    @Override
    protected Set<Local> newInitialFlow() {
        return new HashSet<>();
    }

    @Override
    protected Set<Local> entryInitialFlow() {
        return new HashSet<>();
    }

    @Override
    protected void merge(Set<Local> in1, Set<Local> in2, Set<Local> out) {
        out.clear();
        out.addAll(in1);
        out.addAll(in2);
    }

    @Override
    protected void copy(Set<Local> source, Set<Local> dest) {
        dest.clear();
        dest.addAll(source);
    }

    public void printResults() {
        for (Unit unit : graph) {
            System.out.println("--------------------------------------------------");
            System.out.println("Before: " + unitToInSet.get(unit));
            System.out.println("Stmt:   " + unit);
            System.out.println("After:  " + unitToOutSet.get(unit));
        }
    }
}

效果和验证

执行效果

执行SootInit,效果如图。

1-1

1-2

验证

反编译原class文件,得到

public class lab3 {
    public lab3() {
    }

    public static void main(String[] var0) {
        int var1 = func();
        func1(var1, var1 + 10);
    }

    public static int func() {
        byte var0 = 5;
        byte var2 = 6;
        boolean var3 = true;
        byte var4 = 0;
        int var5;
        if (var0 > 0) {
            var5 = var0 + var2;
        } else {
            var5 = var0 + 4;
        }

        int var10000 = var5 + var4;
        return var5 + var4;
    }

    public static void func1(int var0, int var1) {
        int var5 = var0 - 1;
        int var6 = var1;
        int[] var7 = new int[var1 + 1];
        int var8 = var7[var1];

        for(int var9 = 0; var9 <= var1; ++var9) {
            var7[var9] = var9 * 3;
        }

        while(true) {
            do {
                ++var5;
            } while(var7[var5] < var8);

            do {
                --var6;
            } while(var7[var6] < var8);

            int var2;
            if (var5 >= var6) {
                var2 = var7[var5];
                var7[var5] = var7[var1];
                var7[var1] = var2;
                return;
            }

            var2 = var7[var5];
            var7[var5] = var7[var6];
            var7[var6] = var2;
        }
    }
}

肉眼观察发现soot分析得没问题。

lab2 task2 死代码检测

准备工作

现在我们只有一个apk,首先人工看看它是什么。

安装进虚拟机

先启动虚拟机,直接install它试试看。

asemu -avd Small_Phone
cd ~/lab2
adb install ./lab2.apk

发现它就是个hello world,啥都没有。

jadx解包

jadx可以得到java文件,虽然会有一些细节的损失。

jadx -d upk-jadx lab2.apk

然后用Android Studio打开这个项目,可以看到源代码,包名com.example.lab_code。发现除了hello world还有一些用户管理和奇怪的比较,同时还有log写入。肉眼可见有不少死代码。

2-1

apktool解包

apktool解包的精确度更高,但是smali不好阅读。不过可以得到AndroidManifest.xml,也非常有用,可以找到入口类和申请权限等等。

apktool d lab2.apk -o upk-apktool

这里其实埋下了一个伏笔,就是我并没有仔细观察解包出来的项目结构,后面会说。

让分析器跑起来

新建一个项目,添加soot依赖。soot内置了apk分析的工具,可以在初始化时加载apk和android.jar

        String apkPath = "/Users/anpoliros/lab2/lab2.apk"; // apk
        String androidJarPath = "/Users/anpoliros/lab2"; // android.jar

        G.reset();
        Options.v().set_src_prec(Options.src_prec_apk);
        Options.v().set_process_dir(Collections.singletonList(apkPath));
        Options.v().set_android_jars(androidJarPath);
        Options.v().set_force_android_jar(androidJarPath + "/android.jar");

这个android.jar在安卓sdk里,我把它拿出来了,不过似乎没什么必要。

直接扫描

非常自然的想法就是直接这样扫描。加一段自动扫描入口点的代码,例如onCreate等。找到入口点后挨个分析,输出检测到的死代码的类。然而,确实扫描出东西了,但是全是安卓自带的库,输出非常多,却一个包含com.example的都没有。

2-2

合并dex

经过一番debug,也毫无进展。这时注意到jadx解出来的代码中有

/* loaded from: classes3.dex */

这也许是因为soot只加载apk中的classes.dex导致的。所以我们之前的解包看来还是有问题,没有注意到有三个smali目录。

unzip lab2.apk -d upk-unzip

果然,里面有三个dex文件。使用sdk带的d8把它们合并。

cd upk-unzip
mkdir merge
d8 classes.dex classes2.dex classes3.dex --output merge

重新打包apk

我试图直接用soot分析这个dex,但是老是报错,干脆直接重新打包成apk。现在apktool解包出来有三个smali目录,这就对应的三个dex。只需要简单地把这三个目录合并成一个就可以了。然后重新打包。

apktool b . o lab2_merged.apk

这样之后不需要签名,把soot中的apk路径改成这个就可以了。这次soot终于找到了com.example,成功打印出了包含死代码的类。

2-3

经过对比,发现这几个类中确实有死代码。

实现

SootAPK.java

package task2;

import soot.*;
import soot.options.Options;
import soot.jimple.toolkits.callgraph.CallGraph;
import soot.jimple.toolkits.callgraph.Edge;
import java.util.*;

public class SootAPK {
    public static void main(String[] args) {
        String apkPath = "/Users/anpoliros/lab2/lab2_merged.apk";
        String androidJarPath = "/Users/anpoliros/lab2";
        
        // ---- 初始化soot ----
        G.reset();
        Options.v().set_src_prec(Options.src_prec_apk);
        Options.v().set_process_dir(Collections.singletonList(apkPath));
        Options.v().set_android_jars(androidJarPath);
        Options.v().set_force_android_jar(androidJarPath + "/android.jar");
        Options.v().set_output_format(Options.output_format_none);
        Options.v().set_whole_program(true);
        Options.v().set_allow_phantom_refs(true);
        Options.v().setPhaseOption("cg.spark", "on");// 启用Spark
        Scene.v().loadNecessaryClasses();
        System.out.println("[+] Soot Initialized");

        // ---- 分析入口点 ----
        List<SootMethod> entryPoints = new ArrayList<>();
        for (SootClass sc : Scene.v().getApplicationClasses()) {
            for (SootMethod sm : sc.getMethods()) {
                String name = sm.getName();
                if (name.equals("onCreate") || name.equals("onStartCommand") ||
                        name.equals("onReceive") || name.equals("doInBackground")) {
                    if (sm.isConcrete()) {
                        entryPoints.add(sm);
                        System.out.println("[Auto EntryPoint] " + sm.getSignature());
                    }
                }
            }
        }
        Scene.v().setEntryPoints(entryPoints);
        // 运行soot 
        PackManager.v().runPacks();

        // --- 构造调用图 ---
        CallGraph cg = Scene.v().getCallGraph();
        Set<SootMethod> reachable = new HashSet<>();
        for (Edge edge : cg) {
            reachable.add(edge.getTgt().method());
        }

        // --- 输出筛选后的死代码 ---
        System.out.println("\n=== Dead Code in com.example.lab_code ===");
        int count = 0;
        for (SootClass sc : Scene.v().getApplicationClasses()) {
            if (!sc.getName().startsWith("com.example.lab_code")) continue;

            for (SootMethod sm : sc.getMethods()) {
                if (!reachable.contains(sm)) {
                    String methodName = sm.getName();
                    if (methodName.equals("<init>") || methodName.equals("toString") || methodName.equals("hashCode"))
                        continue; // 忽略构造器、toString等
                    System.out.println("[DeadCode] " + sm.getSignature());
                    count++;
                }
            }
        }

        System.out.println("\n[+] Total Dead Methods in com.example.lab_code: " + count);
    }
}

更精确的分析

上面的工作虽然让分析器跑起来了,但是存在两个问题:

  • 无法识别出具体的死代码类型
  • 经过验证,存在误报漏报问题

死代码分类

我们主要识别三种死代码,并用颜色标识:

  • 🟥方法未被调用,仅声明未使用的方法
  • 🟨不可达代码,包括以下两种
    • 分支不可达:if(false) xxx;
    • 控制流不可达:xxx; return; xxx;
  • 🟦无用赋值,赋值后从未使用过的变量

在这个环节,需要注意的是soot并不能识别出分支不可达的代码,例如

if (index.intValue() > 10) {
    Log.d("Exec1", index.toString());
} else {
    Log.d("Exec2", index.toString());
}

这属于更高级的静态值分析,soot默认功能无法实现。所以接下来的不可达代码仅包含控制流不可达的类型。

分类识别算法

🟥未调用方法,原理基于调用图(Call Graph)分析,找出程序中未被任何路径调用的方法。

  • Soot 构建全程序调用图(Call Graph),表示方法之间的调用关系。
  • 从一个或多个入口点(如 main()、onCreate())出发,进行深度或广度遍历。
  • 如果某个方法在调用图中不可达,说明它从未被任何地方调用,即为死代码方法。

🟨不可达代码,原理基于控制流图(Control Flow Graph, CFG)分析,识别从入口点永远到达不了的代码块。

  • Soot 使用 Jimple 生成方法的 CFG,表示基本块之间的跳转关系。
  • 从入口语句(如第一条指令)出发,执行图遍历(如 BFS)。
  • 如果某些语句或代码块在图中不可达,说明它们是永远不会被执行的。

🟦无用赋值,原理:基于活跃变量分析(Live Variable Analysis),识别赋值后从未被使用的局部变量。这和task1的实现异曲同工。

  • 对方法的Jimple构建控制流图(CFG)。
  • 使用反向数据流分析,计算每条语句之后哪些变量是“活跃”的(即未来会被读取)。
  • 如果某条语句为变量赋值,但该变量在之后从未被读取,说明该赋值是无效的,即死赋值。

项目结构

由于功能复杂度提升了,前述的单文件探测器已经不能满足要求。采用三个文件来实现:

  • Detector 程序入口,设置基本参数,构建调用图
  • SootInit 初始化soot,并自动寻找入口点
  • Analysis 分析模块,对三种情况分别分析

误报漏报问题

经过改进,分析器再次跑起来了。然而,误报问题依然存在。对于待测代码中的void pathTest(Integer index),明明没有无用赋值却报有。而我由基于原有代码新建了一个测试项目,加入了int a=0这种明显的无用赋值,却又识别不出来。为了debug,加入了一段打印Jimple的代码,终于搞清楚了问题所在。

误报无用赋值的原因在于

r0 := @this: com.example.lab_code.MainActivity

它的含义是,将当前对象引用 this 赋值给局部变量 r0,也就是 Java 中隐式存在的 this 被显式映射到 Jimple 中的变量。所以对this的显式赋值被误报成了无用变量。对于这类的误报,我的处理手段就是直接在后面加上对应的Jimple语句,毕竟也好认出。

漏报无用赋值的原因在于,这种显式的无用赋值会被编译优化掉,Jimple在生成过程中有内建的死局部变量消除。下面展示了Java源代码和Jimple的区别,可以看到Jimple中完全没有int a了。

public void assign_test(){
    int a = 0;
}

Jimple of ‹com.anpoliros. lab2test.MainActivity: void assign_test()>:
public void assign_test() {
    com.anpoliros.lab2test.MainActivity r0;
    r0 := @this: com.anpoliros.lab2test.MainActivity;
    return;
}

如果想要识别出这种无用赋值,就需要直接分析Java源代码或者class文件,不过因为附上了this赋值语句所以挺有辨识度的,就不额外剔除了。

效果

2-4

可以看到,分析器正确地检测出了这两种死代码。

分支不可达的检测

对于方法未调用和无用赋值的检测其实比较简单也比较直观,控制流不可达也好识别,但是分支不可达却非常难。分支不可达又可以分为两个类型(个人理解):

  • 参数上下文型,分支不可达仅涉及到过程内
  • 传参型,分支不可达涉及到过程间

参数上下文型

Integer index = 8;
if (index.intValue() > 10) {
    Log.d("Exec1", index.toString());
} else {
    Log.d("Exec2", index.toString());
}

这种比较好识别,因为在cfg中控制流总体还是线性的,变量没有改过名字,追踪常量就可以发现有些语句到达不了。

另外还需要注意的是这里面的Integer index的声明方式。如果这样声明变量,相当于初始化了一个包装类,和int index是不同的。这就要求分析器能够进行拆箱操作,找出常量的传播线索。

参数上下文型的检测

为了找到这种死代码,需要实现一个恒值条件分析器。主要算法如下:

  • 遍历方法中的所有 IfStmt(条件跳转语句)
  • 对每个条件表达式,尝试获取其两个操作数的常量值
  • 如果两个操作数均为常量,计算条件结果:
    • 若恒为真:报告为 [恒为真分支]
    • 若恒为假:报告为 [恒为假分支 => 不可达]

还需要一个辅助方法执行简易常量传播,收集形如 a = 8 或 a = Integer.valueOf(8) 等对变量赋定值的语句。支持识别的语句类型:

  • a = 8 (常量赋值)
  • a = Integer.valueOf(8)(自动装箱)
  • a = new Integer(8); a.init(8)(手动装箱)
  • int a = index.intValue();(拆箱)
  • a = b 且 b 是已知常量(变量传递)
  • 遇到非常量表达式赋值时,丢弃原有常量信息

传参型

public void onCreate(){
    //...
    pathTest(12);
}
public void pathTest(Integer index) {
    if (index.intValue() > 10) {
        Log.d("Inter Exec1", index.toString());
    } else {
        Log.d("Inter Exec2", index.toString());
    }
}

这就涉及到传参的识别,难度又上了一个台阶,因为参数在传递过程中在Jimple/smali级别发生了名字上的变化。让我们看看这个传参过程的Jimple代码就能明白了。这段代码十分清晰地展示了变量名的变化。

$r3 = staticinvoke <java.lang.Integer: java.lang.Integer valueOf(int)>(12);
virtualinvoke r0.<com.example.lab_code.MainActivity: void pathTest(java.lang.Integer)>($r3);
public void pathTest(java.lang.Integer)
{
	//...
    int $i0;
    $r1 := @parameter0: java.lang.Integer;
    $i0 = virtualinvoke $r1.<java.lang.Integer: int intValue()>();
    if $i0 <= 10 goto label1;
    //...
}

传参型的检测

传参问题的检测实现起来复杂一些,需要用cg.spark来准确分析调用中发生的事情,实现一个基于调用图与实参恒定传播分析的恒值分支检测器。

一个首先需要解决的问题是:虽然 $r1 是参数且恒值为 12,但在 CFG 中判断的是 $i0,我们要让 Soot知道 $i0 == 12,而不是 $r1 == 12。这就涉及到常量传播,需要在分析时增加一段捕获常量的代码

Map<Local, Integer> propagatedConsts = new HashMap<>(constMap);
for (Unit u : body.getUnits()) {
    if (u instanceof AssignStmt assign &&
        assign.getLeftOp() instanceof Local left &&
        assign.getRightOp() instanceof VirtualInvokeExpr virt &&
        virt.getMethod().getSignature().equals("<java.lang.Integer: int intValue()>")) {

        Value base = virt.getBase();
        if (base instanceof Local baseLocal && constMap.containsKey(baseLocal)) {
            propagatedConsts.put(left, constMap.get(baseLocal));
        }
    }
}

整体上,算法分为两个部分:

Step 1:遍历调用图CallGraph收集实参

  • 遍历 CallGraph 中所有显式边(edge.isExplicit());
  • 对每个被调用方法 callee,提取调用语句中的实参;
  • 若该实参在 caller 方法体中可以还原为 IntConstant,则记录为恒定参数;
  • 将每个 callee 方法的参数映射为 Map<Integer, Value>(参数索引 → 恒定值),存入 methodArgConstMap;
  • 排除不是恒值的情况(出现多个不同值或有一个非常量),标记为 null。

Step 2:对具有恒参的方法做条件分支分析

  1. 匹配参数:
    • 在被调方法体中,找到所有 IdentityStmt;
    • 如果它是形如 param0 = @parameter0 且在调用图中已有恒值映射,则记录为参数恒值。
  2. 传播拆箱结果:
    • 支持 int x = param0.intValue(); 的情况;
    • 将其对应的常量继续传播。
  3. 不可达分支检测:
    • 利用 SootIfConditionAdapter 适配器+ BranchUnreachableDetector 工具类;
    • 结合控制流图 BriefUnitGraph;
    • 根据已知的参数恒值判断条件跳转是否永远不成立;
    • 打印所有检测到的不可达 if 分支语句。

SootInit的调整

我在传参型的实现中浪费了大量时间debug,分析器可以找到cg边但就是无法找到参数传递的过程。具体体现为,可以识别出类之间的调用,但无法识别到类内部的调用。例如,可以发现MainActivity对User的调用,但无法发现onCreate对pathTest的调用。最后发现这个问题是SootInit中初始化的问题导致的。

首先,要明确把app包设为ApplicationClass。这是因为Soot 只对 application classes 内部方法做深度分析,library class 会被跳过。

for (SootClass sc : Scene.v().getApplicationClasses()) {
    if (sc.getName().startsWith("com.example.lab_code")) {
        sc.setApplicationClass();
    }
}

其次,要强制启用精确调用图。

Options.v().set_whole_program(true);
Options.v().setPhaseOption("cg.spark", "on");
Options.v().setPhaseOption("cg", "safe-newinstance:true");
Options.v().setPhaseOption("cg.cha", "on");

加上这两个选项后,soot识别到的cg边从2000+增长到了260000+,足以说明问题。

整合实现

在之前工作的基础上,把ConstantBranch和ArgumentBranch整合进Detector中,同时设置一些bool来开关各个功能。

Detector.java的结构:

  • 入口参数设定
    • apkPath
    • androidJarPath
    • filter 包名过滤器
  • 功能开关设定
    • printJimple 是否打印Jimple
    • detectBasic
    • detectConstantBranch
    • detectArgumentBranch
  • soot初始化(调用SootInit.init)
  • 构造调用图cg
  • 功能调用
    • Analysis 方法未调用、无用赋值、控制流不可达
    • ConstantBranch 参数上下文型分支不可达(过程内)
    • ArgumentBranch 传参型分支不可达(过程间)

参数上下文型的检测结果

2-5

传参型的检测结果

2-6

2-7