安卓動態除錯七種武器之長生劍 - Smali Instrumentation

wyzsk發表於2020-08-19
作者: 蒸米 · 2015/05/15 10:01

0x00 序


隨著移動安全越來越火,各種除錯工具也都層出不窮,但因為環境和需求的不同,並沒有工具是萬能的。另外工具是死的,人是活的,如果能搞懂工具的原理再結合上自身的經驗,你也可以創造出屬於自己的除錯武器。因此,筆者將會在這一系列文章中分享一些自己經常用或原創的除錯工具以及手段,希望能對國內移動安全的研究起到一些催化劑的作用。

目錄如下:

安卓動態除錯七種武器之長生劍 - Smali Instrumentation

安卓動態除錯七種武器之孔雀翎 - Ida

安卓動態除錯七種武器之離別鉤 - Hooking

安卓動態除錯七種武器之碧玉刀 - Customized DVM

安卓動態除錯七種武器之多情環 - Customized Kernel

安卓動態除錯七種武器之霸王槍 - Anti Anti-debugging

安卓動態除錯七種武器之拳頭 - Tricks & Summary

0x01 長生劍


長生劍是把神奇的劍,為白玉京所配,劍名取意來自於李白的詩:“仙人撫我頂,結髮受長生。”長生劍是七種武器系列的第一種武器,而筆者接下來所要介紹的除錯方法也是我最早學習的除錯方法,並且這種方法就像長生劍一樣,簡單並一直都有很好的效果。這種方法就是Smali Instrumentation,又稱Smali 插樁。使用這種方法最大的好處就是不需要對手機進行root,不需要指定android的版本,如果結合一些tricks的話還會有意想不到的效果。

0x02 Smali/baksmali


做安卓逆向最先接觸到的東西肯定就是smali語言了,smali最早是由Jasmin提出,隨後jesusfreke開發了最有名的smali和baksmali工具將其發揚光大,幾乎dex上所有的靜態分析工具都是在這個專案的基礎上建立的。什麼?你沒聽說過smali和baksmali?你只用過Apktool?如果你仔細閱讀了Apktool官網的說明你就會發現,Apktool其實只是一個將各種工具結合起來的懶人工具而已。並且筆者建議從現在起就拋棄Apktool吧。原因如下:首先,Apktool更新並沒有smali/baksmali頻繁,smali/baksmali更新後要過非長久的時間才會合併到Apktool中,在這之前你可能需要忍受很多詭異的bug。其次,Apktool在反編譯或者重打包dex的時候,如果發生錯誤,僅僅只會提供錯誤的exception資訊而已,但如果你使用smali/baksmali,工具會告訴你具體的出錯原因,會對重打包後的除錯有巨大的幫助。最後,很多apk為了對付反除錯會在資原始檔中加入很多junk code從而使得Apktool的解析崩潰掉,造成反編譯失敗或者無法重打包。但如果你僅對classes.dex操作就不會有這些問題了。

學習smali最好的方法就是自己先用java寫好程式,再用baksmali轉換成smali語句,然後對照學習。比如下面就是java程式碼和用baksmali反編譯過後的smali檔案的對照分析。

MZLog類主要是用Log.d()輸出除錯資訊,Java程式碼如下:

#!java
package com.mzheng;

public class MZLog {

    public static void Log(String tag, String msg)
    {
        Log.d(tag, msg);
    }

    public static void Log(Object someObj)
    {
        Log("mzheng", someObj.toString());
    }

    public static void Log(Object[] someObj)
    {
        Log("mzheng",Arrays.toString(someObj));
    }

}

對應的smali程式碼如下:

#!bash
.class public Lcom/mzheng/MZLog; # class的名字
.super Ljava/lang/Object;  #這個類繼承的物件
.source "MZLog.java" # java的檔名


# direct methods #直接方法
.method public constructor <init>()V  #這是class的建構函式實現
    .registers 1 #這個方法所使用的暫存器數量

    .prologue  # prologue並沒有什麼用
    .line 7 #行號
invoke-direct {p0}, Ljava/lang/Object;-><init>()V #呼叫Object的構造方法,p0相當於"this" 指標
    return-void #返回空
.end method

.method public static Log(Ljava/lang/Object;)V # Log(Object)的方法實現
    .registers 3
    .param p0, "someObj"    # Ljava/lang/Object; 引數資訊

    .prologue
    .line 16
    const-string v0, "mzheng"  #給v0賦值”mzheng”

    invoke-virtual {p0}, Ljava/lang/Object;->toString()Ljava/lang/String; #呼叫toString()函式

    move-result-object v1 #將toString()的結果儲存在v1

    invoke-static {v0, v1}, Lcom/mzheng/MZLog;->Log(Ljava/lang/String;Ljava/lang/String;)V #呼叫MZLog的另一個Log函式,引數是v0和v1

    .line 17
    return-void
.end method

.method public static Log(Ljava/lang/String;Ljava/lang/String;)V #Log(String, String)的方法實現
    .registers 2
    .param p0, "tag"    # Ljava/lang/String;
    .param p1, "msg"    # Ljava/lang/String;

    .prologue
    .line 11
    invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I #呼叫android API裡的Log函式實現Log功能

    .line 12
    return-void
.end method

.method public static Log([Ljava/lang/Object;)V #Log(Object[])函式實現 ‘[’符號是陣列的意思
    .registers 3
    .param p0, "someObj"    # [Ljava/lang/Object;

    .prologue
    .line 21
    const-string v0, "mzheng"

    invoke-static {p0}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String;  #將Object陣列轉換為String

    move-result-object v1 #轉換後的結果存在v1中

    invoke-static {v0, v1}, Lcom/mzheng/MZLog;->Log(Ljava/lang/String;Ljava/lang/String;)V #呼叫Log(String, String)函式

    .line 22
    return-void
.end method

最後簡單介紹一下smali常用的資料型別:

V - void
Z - boolean
B - byte
S - short
C - char
I - int
J - long (64 bits)
F - float
D - double (64 bits)

0x03 Smali插樁


如果僅僅用Smali來分析程式碼,效果其實不如用dex2jar和jd-gui更直觀,畢竟看反編譯的java程式碼要更容易一些。但Smali強大之處就是可以隨心所欲的進行插樁操作。何為插樁,引用一下wiki的解釋:程式插樁,最早是由J.C. Huang 教授提出的,它是在保證被測程式原有邏輯完整性的基礎上在程式中插入一些探針(又稱為“探測儀”),透過探針的執行並丟擲程式執行的特徵資料,透過對這些資料的分析,可以獲得程式的控制流和資料流資訊,進而得到邏輯覆蓋等動態資訊,從而實現測試目的的方法。下面我就來結合一個例子來講解一下何如進行smali插樁。

測試程式是一個簡單的crackme (圖1)。輸入密碼,然後點選check,如果密碼正確會輸出yes,否則輸出no。

enter image description here

圖1 Crackme1的介面

首先我們對crackme這個apk進行解壓,然後反編譯。我們會在MainActivity中看到一個getkey(String,int)函式。這個函式貌似非常複雜,我們暫時不管。我們首先分析一下點下button後的邏輯。我們發現程式會透過getkey("mrkxqcroxqtskx",42)來計算出真正的密碼,然後與我們輸人的密碼進行比較,java程式碼如下:

#!java
public void onClick(View arg0) {  
String str = editText0.getText().toString();
if (str.equals(getkey("mrkxqcroxqtskx",42)))
    {
        Toast.makeText(MainActivity.this,"Yes!", Toast.LENGTH_LONG).show();
    }
    else
    {
        Toast.makeText(MainActivity.this,"No!", Toast.LENGTH_LONG).show();                  }
}  

這時候就是smali插樁大顯身手的時候了,我們可以透過插樁直接獲取getkey("mrkxqcroxqtskx",42)這個函式的返回值,然後Log出來。這樣我們就不需要研究getkey這個函式的實現了。具體過程如下:

1 首先解壓apk然後用baksmali進行反編譯。

#!bash
unzip crackme1.apk
java -jar baksmali-2.0.3.jar classes.dex

2 將上一節MZLog類的MZLog.smali檔案複製到com/mzheng目錄下,這個檔案有3個LOG函式,分別可以輸出String的值,Object的值和Object陣列的值。注意,如果原程式中沒有com/mzheng這個目錄,你需要自己用mkdir建立一下。複製完後,目錄結構如下:

com
└─mzheng
    │  MZLog.smali
    │
    └─crackme1
            BuildConfig.smali
            MainActivity$1.smali
            MainActivity.smali
            R$attr.smali
            R$dimen.smali
            R$drawable.smali
            R$id.smali
            R$layout.smali
            R$menu.smali
            R$string.smali
            R$style.smali
            R.smali

3 用文字編輯器開啟MainActivity$1.smali檔案進行插樁。為什麼是MainActivity$1.smali而不是MainActivity.smali呢?因為主要的判斷邏輯是在OnClickListener這個類裡,而這個類是MainActivity的一個內部類,同時我們在實現的時候也沒有給這個類宣告具體的名字,所以這個類用$1表示。加入MZLog.smali這個檔案後,我們只需要在MainActivity$1.smali的第71行後面加上一行程式碼,invoke-static {v1}, Lcom/mzheng/MZLog;->Log(Ljava/lang/Object;)V,就可以輸出getkey的值了。Invoke是方法呼叫的指令,因為我們要呼叫的類是靜態方法,所以使用invoke-static。如果是非靜態方法的話,第一個引數應該是該方法的例項,然後依次是各個引數。具體插入情況如下:

const-string v1, "mrkxqcroxqtskx"

const/16 v2, 0x2a

# invokes: Lcom/mzheng/crackme1/MainActivity;->getkey(Ljava/lang/String;I)Ljava/lang/String;

invoke-static {v1, v2}, Lcom/mzheng/crackme1/MainActivity;->access$0(Ljava/lang/String;I)Ljava/lang/String;

move-result-object v1

############################## begin ##############################
invoke-static {v1}, Lcom/mzheng/MZLog;->Log(Ljava/lang/Object;)V
############################## end ###############################
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

move-result v1

4 用smali.jar重新編譯修改後的smali檔案,把新編譯的classes.dex覆蓋老的classes.dex,然後再用signapk.jar對apk進行簽名。幾條關鍵指令如下:

#!bash
java -jar smali.jar out
java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk

5 安裝程式到android,隨便輸入點啥,然後點選check按鈕,隨後在logcat中就可以看到getkey("mrkxqcroxqtskx",42)這個函式的返回值了(圖2)。

enter image description here

圖2 透過logcat獲取getkey的返回值

0x03 Smali修改


透過Smali/baksmali工具,我們不光可以插樁,還可以修改apk的邏輯。幾個需要注意點如下:

1. if條件判斷以及跳轉語句

在smali中最常見的就是if這個條件判斷跳轉語句了,這個判斷一共有12條指令:

#!bash
if-eq vA, VB, cond_** 如果vA等於vB則跳轉到cond_**。相當於if (vA==vB)
if-ne vA, VB, cond_** 如果vA不等於vB則跳轉到cond_**。相當於if (vA!=vB)
if-lt vA, VB, cond_** 如果vA小於vB則跳轉到cond_**。相當於if (vA<vB)
if-le vA, VB, cond_** 如果vA小於等於vB則跳轉到cond_**。相當於if (vA<=vB)
if-gt vA, VB, cond_** 如果vA大於vB則跳轉到cond_**。相當於if (vA>vB)
if-ge vA, VB, cond_** 如果vA大於等於vB則跳轉到cond_**。相當於if (vA>=vB)

if-eqz vA, :cond_** 如果vA等於0則跳轉到:cond_** 相當於if (VA==0)
if-nez vA, :cond_** 如果vA不等於0則跳轉到:cond_**相當於if (VA!=0)
if-ltz vA, :cond_** 如果vA小於0則跳轉到:cond_**相當於if (VA<0)
if-lez vA, :cond_** 如果vA小於等於0則跳轉到:cond_**相當於if (VA<=0)
if-gtz vA, :cond_** 如果vA大於0則跳轉到:cond_**相當於if (VA>0)
if-gez vA, :cond_** 如果vA大於等於0則跳轉到:cond_**相當於if (VA>=0)

比如我們在crackme1裡判斷密碼是否正確的smali程式碼段:

#!bash
    invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v1

    if-eqz v1, :cond_25  # if (v1==0)

    iget-object v1, p0, Lcom/mzheng/crackme1/MainActivity$1;->this$0:Lcom/mzheng/crackme1/MainActivity;

    const-string v2, "Yes!"

    invoke-static {v1, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v1

    invoke-virtual {v1}, Landroid/widget/Toast;->show()V

    :cond_25
    iget-object v1, p0, Lcom/mzheng/crackme1/MainActivity$1;->this$0:Lcom/mzheng/crackme1/MainActivity;

    const-string v2, "No!"

    invoke-static {v1, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v1

        invoke-virtual {v1}, Landroid/widget/Toast;->show()V

如果我們不關心密碼內容,只是希望程式輸出”yes”的話。我們可以把if-eqz v1, :cond_25改成if-nez v1, :cond_25。這樣邏輯就變為:當輸錯密碼的時候,程式反而會輸出”yes”。

2. 暫存器問題

修改Smali時有一件很重要的事情就是要注意暫存器。如果亂用暫存器的話可能會導致程式崩潰。每個方法開頭宣告瞭registers的數量,這個數量是引數和本地變數總和。引數統一用P表示。如果是非靜態方法p0代表this,p1-pN代表各個引數。如果是靜態方法的話,p0-pN代表各個引數。本地變數統一用v表示。如果想要增加的新的本地變數,需要在方法開頭的registers數量上增加相應的數值。

比如下面這個方法:

#!bash
.method public constructor <init>()V
    .registers 1

    .prologue
    .line 7
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method

因為這不是靜態方法,所以p0代表this。如果想要增加一個新的本地變數,比如v0。就需要把.registers 1改為.registers 2

3. 給原程式增加大量邏輯的辦法

我非常不建議在程式原有的方法上增加大量邏輯,這樣可能會出現很多暫存器方面的錯誤導致編譯失敗。比較好的方法是:把想要增加的邏輯先用java寫成一個apk,然後把這個apk反編譯成smali檔案,隨後把反編譯後的這部分邏輯的smali檔案插入到目標程式的smali資料夾中,然後再在原來的方法上採用invoke的方式呼叫新加入的邏輯。這樣的話不管加入再多的邏輯,也只是修改了原程式的幾行程式碼而已。這個思路也是很多重打包病毒慣用的伎倆,確實非常方便好用。

0x04 APK簽名Tricks


當我們在實戰中,有時會碰到某些apk在內部實現了自己的簽名檢查。這次我們介紹的Smali Instrumentation方法因為需要重打包,所以會改變原有的簽名。當然,你可以透過修改apk把簽名檢查的邏輯刪掉,但這又費時又費力。筆者在這裡簡單介紹兩種非常方便的方法來解決簽名檢查問題。

1. Masterkey

Masterkey漏洞一共有三個,可以影響android 4.4以下版本。利用這個漏洞,我們可以插入新的classes.dex替換掉原有的classes.dex而不需要對apk本身進行重新簽名。如果apk本身有簽名校驗邏輯的話,利用這個漏洞來進行Smali Instrumentation簡直再好不過了。首先,你需要一個android 4.4以下版本的虛擬機器或者真機,然後再使用一個masterkey利用工具對apk進行exploit即可。工具下載地址在文章最後,使用的命令如下:

#!bash
java -jar AndroidMasterKeys.jar -a orig.apk -z moddedClassesDex.zip -o out.apk

orig.apk是原本的apk檔案,moddedClassesDex.zip是修改後的classes.dex並壓縮成zip檔案,out.apk就是利用Masterkey漏洞生成的新的apk檔案。如果成功的話用rar開啟檔案會看到兩個classes.dex

enter image description here

圖3 Masterkey生成的apk檔案有兩個classes.dex檔案

透過masterkey打包後的apk檔案簽名並不會有任何變化,這樣也就不用擔心簽名校驗問題了。

2. 自定義ROM

簽名的判斷其實是呼叫了android系統密碼庫的函式,如果我們可以自己定製ROM的話,只需要修改AOSP原始碼路徑下的libcore\luni\src\main\java\java\security\MessageDigest.java檔案。將isEqual函式中的判斷語句註釋掉:

#!java
public static boolean isEqual(byte[] digesta, byte[] digestb) {
        if (digesta.length != digestb.length) {
            return false;
        }
//        for (int i = 0; i < digesta.length; i++) {
//            if (digesta[i] != digestb[i]) {
//                return false;
//            }
//        }
        return true;
}

這樣的話,如果在你自定義的ROM上執行apk,無論你怎麼修改classes.dex檔案,都不需要關心簽名問題了,系統會永遠返回簽名正確的。

0x05 小結


雖然現在越來越多的apk開始使用so檔案進行邏輯處理和加固,android 4.4也加入art執行環境,但dalvik永遠是android最經典的東西。如果想要學好android逆向,一定要把這部分知識學好。並且把smali研究透徹以後,會對我們以後要講的自定義dalvik虛擬機器有很大幫助。另外文章中所有提到的程式碼和工具都可以在我的github下載到,地址是: https://github.com/zhengmin1989/TheSevenWeapons

0x06 參考文章


Way of the AndroidCracker http://androidcracking.blogspot.hk/p/way-of-android-cracker-lessons.html

Android Master Key Exploit – Uncovering Android Master Key

https://bluebox.com/technical/uncovering-android-master-key-that-makes-99-of-devices-vulnerable/

https://github.com/Fuzion24/AndroidZipArbitrage

Min Zheng, Patrick P. C. Lee, John C. S. Lui. "ADAM: An Automatic and Extensible Platform to Stress Test Android Anti-Virus Systems", DIMVA 2012

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章