最近在學習軟件分析相關知識的過程中,很多老師都推薦了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,.......)