為了某個實驗的動機,我們評估反編譯 Android 應用程式的可行性,本文即是筆者的心得與實際的範例,僅供參考。就筆者的認知,目前還沒有針對 Android 的 DEX to Java source 反編譯工具,可實際處理一般的 Android 應用程式,多半要繞幾圈,還會得到不甚理想的結果,不過,smali 這個反組譯工具,已是可用了,只是得對付類似 Jasmin 語法的 Dalvik 組合語言。筆者打包了 smali 與 Frozen Bubble for Android,作為示範:
$ adb install -r FrozenBubble-orig.apk
$ make extract這時候會看到兩個目錄:
smali-src$ find就 忠實地依據 Java package 的方式呈現,檔名結尾是 ".smali"。筆者的修改目標是,讓一開始的關卡 (Level) 從第一關直接跳躍到第五關。在 smali 原始程式碼 (注意:這完全不同於 Java 原始程式碼,而是極為貼近 Dalvik VM 所接受的 DEX 檔案的組合語言形式) 搜尋 "Level" 字眼,可發現主要的分佈就落於兩個檔案:
./org/jfedor/frozenbubble/FrozenBubble.smali
./org/jfedor/frozenbubble/R$id.smali
./org/jfedor/frozenbubble/GameView.smali
./org/jfedor/frozenbubble/SoundManager.smali
./org/jfedor/frozenbubble/LaunchBubbleSprite.smali
./org/jfedor/frozenbubble/Compressor.smali
./org/jfedor/frozenbubble/R$attr.smali
./org/jfedor/frozenbubble/BubbleFont.smali
./org/jfedor/frozenbubble/PenguinSprite.smali
./org/jfedor/frozenbubble/GameView$GameThread.smali
./org/jfedor/frozenbubble/BubbleSprite.smali./org/jfedor/frozenbubble/R$string.smali
./org/jfedor/frozenbubble/R$drawable.smali
./org/jfedor/frozenbubble/ImageSprite.smali
./org/jfedor/frozenbubble/BubbleManager.smali
./org/jfedor/frozenbubble/GameScreen.smali
./org/jfedor/frozenbubble/R.smali
./org/jfedor/frozenbubble/R$layout.smali
./org/jfedor/frozenbubble/BmpWrap.smali./org/jfedor/frozenbubble/FrozenGame.smali
./org/jfedor/frozenbubble/Sprite.smali
./org/jfedor/frozenbubble/LevelManager.smali
./org/jfedor/frozenbubble/R$raw.smali
smali-src$ grep "\.method" org/jfedor/frozenbubble/LevelManager.smali倘若我們以 "goToFirstLevel" 一類的關鍵字,在 org/jfedor/frozenbubble/GameView$GameThread.smali 檔案中搜尋,可找出有具體的呼叫行為:
.method public constructor <init>([BI)V
.method private getLevel(Ljava/lang/String;)[[B
.method public getCurrentLevel()[[B
.method public getLevelIndex()I
.method public goToFirstLevel()V
.method public goToNextLevel()V.method public restoreState(Landroid/os/Bundle;)V
.method public saveState(Landroid/os/Bundle;)V
smali-src$ grep -r goToFirstLevel *由此更確定我們之前的猜想。其中組合語言寫為以下:
org/jfedor/frozenbubble/GameView$GameThread.smali: invoke-virtual {v2}, Lorg/jfedor/frozenbubble/LevelManager;-> goToFirstLevel()V
org/jfedor/frozenbubble/LevelManager.smali:.method public goToFirstLevel()V
move-object/from16 v0, p0不 要被貌似複雜的語法嚇到了,基本上掌握 Java 程式語言的原則 "Everything is Object" (不過仍有提供 primitive type),組合語言仍會作 Java Object 的實體化 (instantialization),Dalvik 本身是 Register-based Virtual Machine,而扣除 static/class method 外,Java 中所有的 method invocation 多為 virtual function (對應於 C++ 的觀點,才能更具體用機械方式思考),所以組合語言的指令為 "invoke-virtual" (注意有連字號,此與 Java bytecode 不同),"{v2}" 表示第一受者的 Register,此為 Object 本體。接著,與 Java bytecode 一樣,"Lorg/jfedor/frozenbubble/LevelManager;" 就表示 Java 層面的 class "org.jfedor.frozenbubble.LevelManager",字母 "L" 即為 class 的識別,而 "->" 就很直觀了,自然是 method invocation,所以連貫來看,這一段組合語言的 Java 意思為:
iget-object v0, v0, Lorg/jfedor/frozenbubble/GameView$GameThread;->mLevelManager:Lorg/jfedor/frozenbubble/LevelManager;
move-object v2, v0
invoke-virtual {v2}, Lorg/jfedor/frozenbubble/LevelManager;->goToFirstLevel()V
objectLevelManager.goToFirstLevel();其 中 objectLevelManager 是一個 class LevelManager 的實例/實體 (instance)。倘若需要在 method invocation 時,帶入參數,那麼前述的 "{v2}" 一般會被替換為 "{v0, v1, v2, ...} 的 register 列表。關於詳細的狀況,可參考 Dalvik 非官方說明,另外 smali 的 wiki 也提供一些範例,可多加利用。
const-string v3, "level"在 上述程式碼列表中,"Lorg/jfedor/frozenbubble/LevelManager;-><init>" 表示呼叫 class LevelManager 的 constructor,也就是 "<init>"。注意到 method invocation 方式就不同了,是 "invoke-direct",表示 class constructor,而這之前要有 "new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;" 的組合語言指令宣告。
const/4 v4, 0x0
move-object/from16 v0, v25 move-object v1, v3
move v2, v4
invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I
move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
move/from16 v2, p4 invoke-direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;-><init>([BI)V
const-string v3, "level"顯 然,Register v1 還被帶入到 android.content.Shared.Preference.getInt() method,而更早以前,其內含值被設定為 Register v3 的值,也就是常數字串 (const-string) "level",這好像與我們的焦點不同。另外,像是 Register v22 這個編號較大的 register,表示 local variable,這點需要多留意,因為 Java 程式設計的規範來說,往往會將程式切割為若干 method,而 method 實做體中,又有極多的 local variable,於是往往可從組合語言反推 Java 原始碼的型態。
const/4 v4, 0x0
move-object/from16 v0, v25
move-object v1, v3
move v2, v4
invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I
move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
move/from16 v2, p4
invoke-direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;-><init>([BI)V
const-string v3, "level"這 個 Register v4 的內含值 "0x0" 會指派到 Register v2 中,而讓我們似乎找到方向了,回頭看看 class org.jfedor.frozenbubble.LevelManager 的 constructor 宣告方式: (之前 grep 結果的第一行)
const/4 v4, 0x0
move-object/from16 v0, v25
move-object v1, v3
move v2, v4
invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I
move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
move/from16 v2, p4
invoke-direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;-><init>([BI)V
smali-src$ grep "\.method" org/jfedor/frozenbubble/LevelManager.smali其中 "public" 是 ACL (存取權限) 的宣告,而 constructor 的符號規範為 "<init>",注意到括號 "(" 與 ")" 裡面的兩個大寫字母,表示接受兩個參數,對應的型態為:
.method public constructor <init>([BI)V
invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I"p4" 用以保存 method invocation 之後的回傳值,顯然,Register v2 受到 p4 的指派,也就是被更動為 android.content.Shared.Preference.getInt() method 的回傳值,這存在不確定性,於是,我們乾脆一口氣改掉: (修改的部份會用井字號 "#" 作註解)
move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
move/from16 v2, p4
# Modified from 0x0 to 0x4"改好程式,當然要驗證,回到上一層目錄,透過 smali 提供的組譯器,重新產生 Dalvik DEX 輸出,為了簡化流程,筆者把 smali, apkbuilder, aapt, adb install 都一次整合進去,所以會直接讓 Android Emulator 生效,來看看我們的戰果吧:
const/4 v4, 0x4
move-object/from16 v0, v25
move-object v1, v3
move v2, v4
# Modified: removed the following 2 lines
# invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getInt(Ljava/lang/String;I)I
# move-result p4
new-instance v3, Lorg/jfedor/frozenbubble/LevelManager;
move-object v0, v3
move-object/from16 v1, v22
# Modified: removed the following 1 line
# move/from16 v2, p4
invoke-direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;-><init>([BI)V