軟件分析筆記: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來進行部署,相關的依賴添加語句如下:

<dependencies>
  <dependency>
    <groupId>ca.mcgill.sable</groupId>
    <artifactId>soot</artifactId>
    <version>4.1.0</version>
  </dependency>
</dependencies>

因為目前我的目的只是簡單的使用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 <init>()
    {
        HelloWorld r0;

        r0 := @this: HelloWorld;

        specialinvoke r0.<java.lang.Object: void <init>()>();

        return;
    }

    public static void main(java.lang.String[])
    {
        java.io.PrintStream $r0;
        java.lang.String[] r1;

        r1 := @parameter0: java.lang.String[];

        $r0 = <java.lang.System: java.io.PrintStream out>;

        virtualinvoke $r0.<java.io.PrintStream: void println(java.lang.String)>("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 <init>()
    {
        Loop r0;

        r0 := @this: Loop;

        specialinvoke r0.<java.lang.Object: void <init>()>();

        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 <init>()
    {
        DoWhile r0;

        r0 := @this: DoWhile;

        specialinvoke r0.<java.lang.Object: void <init>()>();

        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 <init>()
    {
        MethodCall r0;

        r0 := @this: MethodCall;

        specialinvoke r0.<java.lang.Object: void <init>()>();

        return;
    }

    public static void main(java.lang.String[])
    {
        java.lang.String[] r0;

        r0 := @parameter0: java.lang.String[];

        staticinvoke <MethodCall: int foo(int,int)>(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 <init>()
    {
        MethodCallString r0;

        r0 := @this: MethodCallString;

        specialinvoke r0.<java.lang.Object: void <init>()>();

        return;
    }

    public static void main(java.lang.String[])
    {
        MethodCallString $r0;
        java.lang.String[] r3;

        r3 := @parameter0: java.lang.String[];

        $r0 = new MethodCallString;

        specialinvoke $r0.<MethodCallString: void <init>()>();

        virtualinvoke $r0.<MethodCallString: java.lang.String foo(java.lang.String,java.lang.String)>("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.<java.lang.StringBuilder: void <init>()>();

        $r2 = virtualinvoke $r0.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r1);

        $r3 = virtualinvoke $r2.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>("   ");

        $r5 = virtualinvoke $r3.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r4);

        $r6 = virtualinvoke $r5.<java.lang.StringBuilder: java.lang.String toString()>();

        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,.......)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM