软件分析笔记:5.Soot的安装与使用
最近在学习软件分析相关知识的过程中,很多老师都推荐了Soot这个代码分析工具,所以我就去学习了一下soot的基本用法。soot项目在github上的地址为:https://github.com/Sable/soot
1.Soot简介
soot是java优化框架,提供4种中间代码来分析和转换字节码。
- Baf:精简的字节码表示,操作简单
- Jimple:适用于优化的3-address中间表示
- Shimple:Jimple的SSA变体
- Grimple:适用于反编译和代码检查的Jimple汇总版本。
soot提供的输入和输出格式
输入格式
- java
- android 字节码
- Jasmin,低级中间表示
- soot提供的分析功能
- class(Java8以后)
输出格式
- Java字节码
- android字节码
- Jimple
- Jasmin
- shimple
- baf
- grimple
- xml
- class
- dava
- template
- jar文件
soot提供的分析功能
- 调用图构造
- 指针分析
- Def/use chains
- 模块驱动的程序内数据流分析
- 结合FlowDroid的污染分析
2.soot的安装
目前来说,要使用soot有三种途径,分别是命令行、程序内以及Eclipse插件(不推荐)
2.1命令行
可以在这里下载最新的soot jar包,我下载的是4.1.0版本中的sootclasses-trunk-jar-with-dependencies.jar 包,这个包应该自带了soot所需要的所有依赖。下载完成后使用powershell进入jar文件所在的文件夹(我的是D:\programing\sootTest),输入以下命令:
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main
可以看到:
再输入
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -h
可以看到有关soot的各种帮助信息。
2.2程序内使用soot
从github上soot项目的简介可知,soot一般配合maven来进行部署,相关的依赖添加语句如下:
ca.mcgill.sable
soot
4.1.0
因为目前我的目的只是简单的使用soot,所以对于程序中soot的使用在后面学习了相关api再来更新。
2.3Eclipse中的soot插件
Eclipse中可以安装soot插件,一键导出java程序对应的Jimple文件等,这种方法不推荐是因为:
- 该插件很久没有维护了,在当前soot已经更新到4.1.0版本的情况下,插件中的soot仅仅是2.5.2版本,在当时是只支持JDK1.7的,在当前环境下很显然已经过时。
- 该插件只能在老旧版本的Eclipse中使用,就我查到的最新的能使用该插件的Eclipse版本为kepler(4.3)版本,而当前的最新版本是2020-03(4.15),虽然新版的eclipse也可以安装soot插件,但是右键菜单栏中不会显示对应的选项。
3.命令行中soot的使用
我的目标是将java转化为Jimple以发现程序编译中的问题和规律。因此本文的重点就在这里,我先在soot.jar所在的文件夹下新建了一个java文件HelloWorld.java如下图所示:
因为我使用的Java版本是JDK1.8,根据soot提示,默认输入是class文件,所以我先用javac命令将HelloWorld.java编译为HelloWorld.class。
下面我们尝试将上面得到的class文件作为输入传给soot.
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main HelloWorld.class
结果会报错
这是因为soot不会默认去当前文件夹下寻找符合条件的文件,而是会去它自身的classpath寻找,而soot的classpath默认情况下是空的,这也就导致soot找不到对应的文件,解决办法是在命令里添加指定位置的代码-cp,-cp .表示在当前目录寻找。添加classpath相关语句之后再次尝试:
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -cp . HelloWorld.class
发现还是会报错
在网上查找相关原因后发现是缺少java.lang类,我按照网上的说法在语句里添加了-pp语句,即:
java -cp .\sootclasses-trunk-jar-with-dependencies.jar soot.Main -pp -cp . HelloWorld
得到的结果没有报错,但是也无事发生,这是因为soot需要通过-f属性指定输出的类型,这里我们将输出类型指定为Jimple,查询文档之后得知要添加-f J以确定输出格式,最终的语句如下:
java -cp .\sootclasses-trunk-jar-with-dependencies.jar soot.Main -pp -cp . HelloWorld
该命令在jar文件所在目录下生成了一个sootOutput文件夹,里面有一个HelloWorld.jimple文件,使用Idea编辑器打开这个文件,得到的内容如下,这就是一个最基本的HelloWorld.java文件所形成的jimple码。
public class HelloWorld extends java.lang.Object
{
public void ()
{
HelloWorld r0;
r0 := @this: HelloWorld;
specialinvoke r0.()>();
return;
}
public static void main(java.lang.String[])
{
java.io.PrintStream $r0;
java.lang.String[] r1;
r1 := @parameter0: java.lang.String[];
$r0 = ;
virtualinvoke $r0.("HelloWorld");
return;
}
}
3.soot命令行相关参数设置
soot/wiki里的命令表格写的十分清楚和明确,这里我就直接搬运过来,方便以后查阅。
4.Java代码转化为jimple码实例
这部分我将一系列Java经典的代码片段通过soot框架编译成jimple代码,以观察不同Java程序转化成jimple码之后的变化:
4.1Loop循环
源代码:
public class Loop {
public static void main(String[] args) {
int x = 0;
for (int i=0;i<10;i++){
x = x+1;
}
}
}
jimple:
public class Loop extends java.lang.Object
{
public void ()
{
Loop r0;
r0 := @this: Loop;
specialinvoke r0.()>();
return;
}
public static void main(java.lang.String[])
{
java.lang.String[] r0;
int i1;
r0 := @parameter0: java.lang.String[];
i1 = 0;
label1:
if i1 >= 10 goto label2;
i1 = i1 + 1;
goto label1;
label2:
return;
}
}
在jimple代码中,开头是一个叫Loop的类继承了java.lang.Object类(默认的所有类的父类),然后是一个初始化的过程,生成默认的构造函数,默认会调用父类的构造函数(即java.lang.Object),接下来就是main函数,在源代码里main函数有一个String[] args的参数,这在jimple代码中就对应了一个声明的参数r0(即r0 := ......这一段),源代码中for循环里面的i在jimple代码中用i1指代,jimpl;e代码中用label来表示程序语句的位置,label1里面的内容就是for循环的条件内容,只要不满足循环条件,用一个goto语句跳转到label2。这里出现了一个bug,那就是源代码中x值的变化在jimple中被“优化”掉了,这大概是soot自身的问题。
4.2do-while循环
源代码:
public class DoWhile {
public static void main(String[] args) {
int[] arr = new int[10];
int i = 0;
do {
i = i+1;
}while (arr[i]<10);
}
}
jimple:
public class DoWhile extends java.lang.Object
{
public void ()
{
DoWhile r0;
r0 := @this: DoWhile;
specialinvoke r0.()>();
return;
}
public static void main(java.lang.String[])
{
int[] r0;
int $i0, i1;
java.lang.String[] r1;
r1 := @parameter0: java.lang.String[];
r0 = newarray (int)[10];
i1 = 0;
label1:
i1 = i1 + 1;
$i0 = r0[i1];
if $i0 < 10 goto label1;
return;
}
}
与Loop循环的jimple码一致的代码就略过不表了,这里是给数据对象arr用r0来代替,并对它进行了初始化,接下来还是用label表示程序的位置,将每一次循环的条件都能表示出来。可以注意到,Do-While循环是先进入循环执行对应的语句,再通过if语句进行循环的跳转。
4.3方法调用method call(普通)
源代码:
public class MethodCall {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = foo(a,b);
}
static int foo(int x, int y){
return x + y;
}
}
jimple:
public class MethodCall extends java.lang.Object
{
public void ()
{
MethodCall r0;
r0 := @this: MethodCall;
specialinvoke r0.()>();
return;
}
public static void main(java.lang.String[])
{
java.lang.String[] r0;
r0 := @parameter0: java.lang.String[];
staticinvoke (1, 2);
return;
}
static int foo(int, int)
{
int i0, i1, $i2;
i0 := @parameter0: int;
i1 := @parameter1: int;
$i2 = i0 + i1;
return $i2;
}
}
可以看到我们定义的方法foo在源代码和jimple源码中差不多,很好理解,就是用了一个中间变量来取得i0和i1的和,再将这个中间变量$i2返回。不过在main函数中,对于方法的调用使用了一个staticinvoke以表示方法的调用,这部分还算简单。
4.4方法调用MethodCall(String)
源代码:
public class MethodCallString {
public static void main(String[] args) {
MethodCallString mcs = new MethodCallString();
String s = mcs.foo("hello","world");
}
String foo(String a,String b){
return a+" "+b;
}
}
jimple:
public class MethodCallString extends java.lang.Object
{
public void ()
{
MethodCallString r0;
r0 := @this: MethodCallString;
specialinvoke r0.()>();
return;
}
public static void main(java.lang.String[])
{
MethodCallString $r0;
java.lang.String[] r3;
r3 := @parameter0: java.lang.String[];
$r0 = new MethodCallString;
specialinvoke $r0.()>();
virtualinvoke $r0.("hello", "world");
return;
}
java.lang.String foo(java.lang.String, java.lang.String)
{
java.lang.StringBuilder $r0, $r2, $r3, $r5;
java.lang.String r1, r4, $r6;
MethodCallString r7;
r7 := @this: MethodCallString;
r1 := @parameter0: java.lang.String;
r4 := @parameter1: java.lang.String;
$r0 = new java.lang.StringBuilder;
specialinvoke $r0.()>();
$r2 = virtualinvoke $r0.(r1);
$r3 = virtualinvoke $r2.(" ");
$r5 = virtualinvoke $r3.(r4);
$r6 = virtualinvoke $r5.();
return $r6;
}
}
可以注意到源代码上改动不太大,但是反映到jimple码里面变化很明显,首先注意到foo方法中(33行)生成了java.lang.StringBuilder,然而事实上源代码中我们并没有使用StringBuilder,这就是soot根据java语言的语义生成的(用老师的话来说就是一个“语法糖”),相当于是将源代码里面字符串拼接的代码重载了,通过StringBuilder这个对象不断地调用append方法以将字符串进行累加操作(47到52行),最后(53行)将StringBuilder转化为String。接下来再看main方法们可以看到soot将实例化出来的对象mcs用$r0来表示,我们在实例化一个对象时,要自动的去调用对应类的构造函数,如果我们没有显式地定义这个构造函数,会初始化默认构造函数,这就是24行中specialinvoke的作用,接下来调用foo方法就会调用virtualinvoke相关的方法,并将真实值传入进去。
JVM里四种主要方法调用
-
invokespecial:调用构造函数、父类中的方法以及私有的方法
-
invokevirtual:常用的方法调用(instance methods call)virtual dispatch
-
invokeinterface:相对于上面invokevirtual不做优化,需要额外检查接口的实现
-
invokestatic:专门的静态方法调用
-
【Java 7:invokedynamic ->Java static typing,dynamic language runs on JVM】
Method signatuure
- class name
- return type
- method name (para1,para2,.......)