查看原文
其他

Soot在Android组件NPE拒绝服务检测中的应用

chiu vivo千镜 2022-11-05

点击上方蓝字关注我们噢~


在对移动应用进行逆向代码静态分析时,通常可以使用正则表达式对逆向后的代码进行搜索来定位安全问题,但正则表达式仅能够对文本进行匹配无法跟踪到代码的上下文与运行时的数据传递,效果欠佳。


本文将介绍如何利用 Soot 来静态模拟应用运行时的数据传递,分析 Android 应用组件中的空指针异常。

01

Soot介绍


Soot是什么


Soot 最初是一个 Java 优化框架,它提供了四种 IR(中间表示形式),用于分析和转换Java字节码。

到目前为止,Soot 可以被用来检测、优化与可视化 Java 和 Android 应用程序。Soot 详细介绍:https://soot-oss.github.io/soot/


Soot能做什么

下面是官方介绍:

Call-graph construction(Call graph 构造)

Points-to analysis (指针分析)

Def/use chains (定义/使用链)

Template-driven Intra-procedural data-flow analysis(过程内数据流分析)

Template-driven Inter-procedural data-flow analysis(过程间数据流分析), in combination with heros (uses IFDS/IDE) or Weighted Pushdown Systems

Aliasing can be resolved using the flow-, field-, context-sensitive demand-driven pointer analysis Boomerang

Taint analysis in combination with FlowDroid or IDEal


Soot 提供了非常丰富的功能,我们主要目的是用 Soot 来分析 Android 组件中 Intent 取值可能引起的空指针异常重点需要关注 Soot 提供的Call graph 生成与数据流分析。

Soot 的核心对象与 IR (中间表示形式)


Soot 核心对象

· Scene:Soot 完整的分析环境,可获取程序的分析信息,如 Call Graph

· SootClass:对应 Java 中的 class

· SootMethod:SootClass 中的方法

· SootField:SootClass 中的域(成员变量)

· Body:SootMethod 方法体,表示方法内语句的集合


Soot的 IR

Soot 会将程序转换成 IR 后进行分析,Soot 提供了四种 IR 来分析和转换 Java 字节码:

· Baf:基于栈的 bytecode

· Jimple:有类型、三地址、基于语句的 IR,soot 主要分析 Jimple

·Shimple:Jimple 的 SSA(Static Single Assignment)变种

· Grimp:Jimple 的聚合版本,更适合人读


Jimple

Soot 分析 Java 时主要使用的 IR 为Jimple,下面将介绍 Jimple 的特点与语句类型。

· 有类型:Java 被转换成 Jimple 后,类型仍会被保留

· 三地址表示:一条语句中最多只会出现三个地址,复杂语句将会被拆分

· 基于语句:语句是 Jimple 的基本组成单位

· 指令简单:相对于bytecode的200多种指令,Jimple 只有15种,分析起来更简便


Jimple的语句类型

· 核心语句:NopStmt, IdentityStmt, AssignStmt

· 过程内控制流语句:IfStmt, GotoStmt, TableSwitchStmt, LookupSwitchStmt

· 过程间控制流语句:InvokeStmt, ReturnStmt, ReturnVoidStmt

· 监控语句:EnterMonitorStmt, ExitMonitorStmt

· 其他:ThrowStmt, RetStmt



02

Soot 构建 CFG


将 Java 代码转换 Jimple


纸上谈来终觉浅,介绍了这么多,还是没有看到 Jimple 的真身,那么下面将来介绍如何利用 Soot 将 Java 代码转换成 Jimple 代码,来看看 Jimple 的真面目吧。


首先需要下载 Soot 的 release 版 jar包:https://soot-build.cs.uni-paderborn.de/public/origin/master/soot/soot-master/4.1.0/build/sootclasses-trunk-jar-with-dependencies.jar
写一个简单的 Java demo,命名为 Test.java
public class Test {    public static void main(String[] args) {        int a = 1;        int b = 2;        System.out.println(new Test().add(a, b));    }    public int add(int a, int b) {        return a + b;    }}


编译得到 Test.class 文件:


javac Test.java


Soot 拥有自己的类路径,进行 Java 分析需要将 JDK 的 rt.jar 添加到 soot 的类路径中(jdk1.8 中 rt.jar 位于$JAVA_HOME/jre/lib 中),同时亦需将 .class 文件所在目录添加到 soot 的类路径。


执行以下命令转换成 Jimple 代码:


java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -cp rt.jar;. Test -f J


  • soot.Main 为 soot 的主类路径

  • -cp 用于指定 soot 的类路径,windows下用分号隔开,linux下用冒号隔开

  • Test 为需要转换的目标

  • -f 用于指定输出的 IR,J 代表 Jimple


执行成功后当前目录将生成 sootOutput 目录,里面即为转换后的输出文件 Test.jimple


public class Test extends java.lang.Object{    public void <init>() // 默认构造方法    {        Test r0;        r0 := @this: Test;        specialinvoke r0.<java.lang.Object: void <init>()>(); // 调用父类构造方法        return;    }
   public static void main(java.lang.String[]){        Test $r0;        java.io.PrintStream $r1;        int $i2;        java.lang.String[] r2;        r2 := @parameter0: java.lang.String[]; // IdentityStmt        $r1 = <java.lang.System: java.io.PrintStream out>; // AssignStmt        $r0 = new Test;        specialinvoke $r0.<Test: void <init>()>(); // 构造方法        $i2 = virtualinvoke $r0.<Test: int add(int,int)>(1, 2); // InvokeStmt        virtualinvoke $r1.<java.io.PrintStream: void println(int)>($i2); // InvokeStmt        return; // ReturnVoidStmt    }
   public int add(int, int){        int i0, i1, $i2;        Test r0;        r0 := @this: Test; // IdentityStmt        i0 := @parameter0: int; // IdentityStmt        i1 := @parameter1: int; // IdentityStmt        $i2 = i0 + i1; // AssignStmt        return $i2; // ReturnStmt    }}

可以看到,上面 Jimple 用到了 InvokeStmt、AssignStmt、IdentityStmt、ReturnStmt和ReturnVoidStmt,其中 IdentityStmt 与 AssignStmt 都为赋值语句,前者指的是本地变量的赋值(入参、成员变量),后者指的是其他普通赋值。


生成Android组件入口的控制流图


Soot 可以对 apk 进行分析,下面使用 soot 来生成一个 apk 导出组件的控制流图。


首先,我们来写一个 apk demo,并进行编译,得到 app-debug.apk


这次需要用到 soot.tools.CFGViewer 这个类,命令如下:


java -cp sootclasses-trunk-jar-with-dependencies.jar soot.tools.CFGViewer--graph=BriefUnitGraph--ir=Jimple--soot-class-path rt.jar;android.jar--src-prec apk--allow-phantom-refs -ire--process-dir app-debug.apk


关键参数解释:


  • --graph 用于指定图的类型

  • --ir 用于指定 IR

  • --soot-class-path 用于指定 soot 的类路径,这里需要传入 android.jar,需要找到编译 apk 对应 API 版本的 SDK,一般位于 xxx/Android/Sdk/platforms/android-xx/目录下

  • --src-prec,分析 apk 时传入 apk 即可

  • --process-dir 用于指定被分析 apk 的路径


执行成功后,sootOutput 会生成以下文件



下面需要使用 Graphviz 提供的 dot 工具将 dot 文件转换成图片格式
Graphviz 下载地址:http://www.graphviz.org/


命令如下:

dot -Tpng "com.example.npedemo1.NpeActivity void onCreate(android.os.Bundle).dot" -o npe.png


执行成功后即可得到该类的控制流图(见下文)。

03

NPE 拒绝服务分析


不同于 Java 程序具有固定的 main 方法入口,Android 程序的入口一般为可导出组件,而唤起导出组件时需要传入一个 intent。
所以对 Android 组件的 NPE 拒绝服务分析,我们一般只需关注 intent 带来的数据。下面将介绍对以下 demo 的 NPE 拒绝服务分析。
源码:
public class NpeActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_npe);        Intent intent = getIntent();        String extra = intent.getStringExtra("extra");        if (extra != null && intent.getAction() != null) {            intent.getStringArrayExtra("extra").equals("abc");            intent.getAction().equals("def");            return;        }        if (extra == null) {            extra.hashCode();            return;        }        extra.toUpperCase();        intent.getAction().toUpperCase();    }}


通过上述方法得到的控制流图:


可以看到,BriefUnitGraph 只保留了关键的语句,使对控制流的分析更加友好。在这里,我们只关注对 intent 相关数据引用可能带来的 NPE。

基本分析思路


· 数据收集,分析整个流程 intent 产生的数据

· 约束收集,通过判断语句来获得每条语句的前置约束

· 约束计算


上图由 intent 产生的数据共有三种,分别是extra、other_extra、action。


假设:· a = (extra == null)· b = (action == null)· c = (other == null)
可得到每条语句的约束,如下图:

下面,根据约束来分析语句的调用方是否为 null。


首先看最左边的分支:



① $r3 = $r2.getStringExtra("extra") ,$r2为 getIntent(),不为空,无问题② $r3.equals("abc"),此处$r3为extra数据,根据当前约束!a可得到extra不为空,无问题③ $r3 = $r2.getStringExtra("other_extra"),同1,无问题④ $r3.equals("def"),此处$r3为other_extra数据,此处之前并没有关于other_extra的约束,所以默认认为other_extra == null,即假设c,存在 NPE
中间分支:


① $r3 = $r2.getAction(),$r2为 getIntent(),不为空,无问题② $r3 .toUpperCase(),此处$r3为action数据,根据当前约束!a只能得到extra不为空,无法判断action是否为空,所以默认认为action == null,存在NPE
右边分支:
$r3.hashCode(),此处$r3为extra数据,根据当前约束a可得到extra == null,存在NPE



04

利用 Soot 进行 NPE 分析


上文提供了分析的思路,下面将简单介绍如何利用 soot 进行自动化分析。由于篇幅有限,本文只介绍关键思路与关键API的实现。


Soot 环境配置与分析:
public static void sootPreSet() {    G.reset();    Options.v().set_src_prec(Options.src_prec_apk);    Options.v().set_output_format(Options.output_format_jimple);    Options.v().set_process_dir(Collections.singletonList("app-debug.apk"));    Options.v().set_android_jars("D:\\Android\\Sdk\\platforms");    Options.v().set_whole_program(true);    Options.v().set_allow_phantom_refs(true);    Scene.v().loadNecessaryClasses();    PackManager.v().runPacks();}


执行完上面的代码就可以通过Scene.v()获取到apk相关的信息(类、方法等)。


准备工作


· Intent 数据流建模:建立每条语句的向前数据流

· Intent 分支约束建模:建立每条语句的向前分支约束


Intent 数据流建模

soot 提供了三种FlowAnalysis,分别是ForwardFlowAnalysis、BackwardsFlowAnalysis与ForwardBranchedFlowAnalysis,这一步,我们需要收集每条语句执行后,该语句之前的所有数据,所以应选用向前数据流分析。


ForwardFlowAnalysis关键API:
entryInitialFlow数据流的初始化,指方法入口可能产生的数据流,这里我们只需要关注 intent 数据。在方法入口中,intent的来源有两种,分别是类成员变量或方法的入参,只需要根据参数类型是否为android.content.Intent即可判断是否需要加入到数据流。
flowThrough
protected abstract void flowThrough(A in, N d, A out);// in:执行前的数据流// d:当前语句// out:执行后的数据流
该API用于计算每条语句执行后的数据流,即out,已知参数为执行前的数据流。
在这个方法中,首先我们需要将in复制到out(即默认情况下出口需要包含语句执行前的入口数据)。
然后判断语句是否为赋值语句(有赋值才有数据的产生),再判断语句的右表达式是否为调用语句(如getIntent、getStringExtra)。
如是且返回类型为intent或调用者为intent(需判断是否为getAction或getXxxExtra),则加入到out中。
Intent 分支约束建模:

这一步目的是收集每条语句执行后,该语句之前的所有约束条件,需继承 soot 的 BranchedFlowAnalysis。
BranchedFlowAnalysis关键API:
entryInitialFlow约束的初始化,指方法入口可能产生的约束,这里我们默认为true(方法入口不存在任何约束)
flowThrough


protected abstract void flowThrough(A in, Unit s, List<A> fallOut, List<A> branchOuts);// in:执行前的约束// unit:即语句stmt// fallOut:不进分支的约束// branchOut:进入分支的约束


同理,这个方法需要计算语句执行后的约束,首先也是需要将in赋值到out,然后判断语句是否为判断语句(IfStmt),如是,将语句加入到out中。这里fallOut是指if条件不成立的约束,branchOut指if条件成立的约束。




05

分析流程


前面计算好每条语句之前的数据流与分支约束,信息已经收集完毕,下面可以开始对数据进行分析了。
流程图如下:



① 遍历所有语句,并判断是否为调用语句(InvokeStmt,有调用关系才有NPE)② 获取调用者(NPE的关键在于调用者是否为空)③ 获取该语句的向前数据流(soot提供的getFlowBefore)④ 判断数据中是否有调用者(数据流建模时,只收集了与intent相关的数据,如向前的数据流中包含调用者,则可认为这个调用者来自intent)⑤ 获取该语句的向前分支约束⑥ 计算约束,得到调用者是否为空


分支约束计算举例



如上图,假设当前遍历到的语句为第三个红框($r3.equals("abc")),即计算$r3(此处的$r3并不是之前的$r3,之前的$r3为extra数据,此处为重新赋值的extra,他们内容相同,但实际意义不同)是否为空,具体步骤如下:
① 获取向前数据流,这里为$r4(action)、$r3(extra)、$r2(intent)② 获取之前的约束(前两个红框),这里共有三个,分别为true(入口默认)、!if$r3 == null(fallOut)、!if$r4 == null(fallOut),将这两条fallOut转换成数据,即为!extra == null与!action == null③ 总的表达式为 true & !extra == null & !action == null④ 可推断extra不为空且action不为空⑤ 回到$r3.equals("abc")这条语句,这里的$r3即为extra数据,所以可得出结论为该语句不会产生NPE
上述只是简单描述了Android组件中NPE自动化检测的流程,介绍了分析的基本思路,实际代码实现中比较复杂,还需要考虑数据的建模、intent数据获取的判断、是否有try catch包围等问题。由于篇幅有限,此处就不作详细介绍了。
以上就是本文的所有内容啦,上述提到的其实只是 soot 的冰山一角,感兴趣的同学可以到 soot 的 wiki 上深入学习,也可以阅读 soot 官方发布的《soot生存手册》作深一步了解。
参考:1.https://github.com/soot-oss/soot/wiki2.https://soot-oss.github.io/soot/3.https://github.com/flankerhqd/JAADAS/4.https://blog.csdn.net/LZQ729089549/article/details/51538622/


更多精彩阅读
如何用lint扫出不安全代码
如何用OLLVM来保护你的关键代码
一文读懂 | 内置安全成熟度模型BSIMM




长按关注  更多安全技术干货等你发现 


好文!在看吗?点一下鸭!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存