软件分析笔记: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,.......)