Soot在Android组件NPE拒绝服务检测中的应用
点击上方蓝字关注我们噢~
在对移动应用进行逆向代码静态分析时,通常可以使用正则表达式对逆向后的代码进行搜索来定位安全问题,但正则表达式仅能够对文本进行匹配,无法跟踪到代码的上下文与运行时的数据传递,效果欠佳。
本文将介绍如何利用 Soot 来静态模拟应用运行时的数据传递,分析 Android 应用组件中的空指针异常。
01
Soot介绍
Soot是什么
Soot 最初是一个 Java 优化框架,它提供了四种 IR(中间表示形式),用于分析和转换Java字节码。
到目前为止,Soot 可以被用来检测、优化与可视化 Java 和 Android 应用程序。Soot 详细介绍:https://soot-oss.github.io/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 的核心对象与 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 拒绝服务分析
所以对 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。
可得到每条语句的约束,如下图:
下面,根据约束来分析语句的调用方是否为 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
中间分支:
右边分支:
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();
}
准备工作
· 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:进入分支的约束
05
分析流程
流程图如下:
分支约束计算举例
① 获取向前数据流,这里为$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
长按关注 更多安全技术干货等你发现