Android 逆向筆記 —— 一個簡單 CrackMe 的逆向總結

往期目錄:java

Class 文件格式詳解android

Smali 語法解析——Hello Worldgit

Smali —— 數學運算,條件判斷,循環github

Smali 語法解析 —— 類shell

Android逆向筆記 —— AndroidManifest.xml 文件格式解析數組

Android逆向筆記 —— DEX 文件格式解析bash

無心中在看雪看到一個簡單的 CrackMe 應用,正好就着這個例子總結一下逆向過程當中基本的經常使用工具的使用,和一些簡單的經常使用套路。感興趣的同窗能夠照着嘗試操做一下,過程仍是很簡單的。APK 我已上傳至 Github,下載地址微信

首先安裝一下這個應用,界面以下所示:app

要求就是經過註冊。爆破的方法不少,大體能夠歸爲三類,第一種是直接修改 smali 代碼繞過註冊,第二種是捋清註冊流程,獲得正確的註冊碼。第三種是 hook 。下面就來講說這幾種爆破過程。框架

直接修改 smali 進行爆破

要獲取 smali 代碼,首先得反編譯這個 Apk,經過 ApkTool 就能夠完成。ApkTool 的使用過程就不在這裏贅述了,執行以下命令:

apktool d creackme.apk
I: Using Apktool 2.3.4-dirty on crackme.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/luyao/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
複製代碼

會在當前目錄生成 crackme 文件夾,文件夾目錄以下:

其中的 smali 文件夾就包含了該 Apk 的全部 smali 代碼。閱讀和修改 smali 代碼的工具不少,我我的偏好將整個反編譯獲得的文件夾導入 IDEA 或者 Android Studio 進行閱讀和修改,可能我是 Android 開發,用這兩個工具會比較順手,全局搜索功能也很給力。

導入 Android Studio 以後,看到了全部的 smali 代碼,那麼咱們該從何下手呢?註冊失敗的時候會彈一個 Toast,「無效用戶名或註冊碼」,這就是突破口。全局搜索這個字符串,

發現這個字符串定義在 string.xml 中的 unsuccessd ,在寫代碼的時候就是 R.string.unsuccessd,這是一個 int 值,編譯後就直接是一個數字了。咱們再來全局搜索 unsuccessd :

public.xml 中能夠看到它的 id,代碼中直接使用的就是這個 id了。全局搜索一下 0x7f05000b,看一下這個 Toast 是在哪裏彈出的。

能夠看到這個 id 在 MainActivity.smali 中的 433 行使用到了,咱們定位到這個文件:

 .line 117
    if-nez v0, :cond_0  # 若是 v0 不等於 0 ,跳轉到 cond_0

 .line 119
    const v0, 0x7f05000b

 .line 118
    invoke-static {p0, v0, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;

    move-result-object v0

 .line 119
    invoke-virtual {v0}, Landroid/widget/Toast;->show()V
複製代碼

這段邏輯很簡單。判斷寄存器 v0 的值是否爲 0,不爲 0 的話則彈出 「無效用戶名或註冊碼」 。因此最簡單的改法,邏輯反一下,v0 爲 0 的時候彈出該 Toast,把 if-nez 改成 if-ez 便可。修改以後使用 ApkTool 重打包,重打包命令以下:

apktool b crackme -o crackme_new.apk
複製代碼

會在當前目錄生成 crackme_new.apk 文件,注意這個安裝包是未簽名的,沒法直接安裝,須要先簽名。使用 jarsinger 或者 apksigner 均可以。簽名以後安裝,輸入用戶名:

這樣就註冊成功了。方法雖然有點 low ,但好歹爆破成功了。下面咱們不修改 smali 代碼,經過閱讀 smali 代碼理解其註冊碼生成邏輯,經過正規方式來註冊。

獲取註冊碼爆破

咱們以前已經找到了具體的邏輯是在 MainActivity.smali 中,找到這個按鈕的 onClick() 事件,來看一下具體邏輯:

.line 116 invoke-direct {p0, v0, v1}, Lcom/droider/crackme0201/MainActivity;->checkSN(Ljava/lang/String;Ljava/lang/String;)Z
 move-result v0

.line 117 if-eqz v0, :cond_0

.line 119 const v0, 0x7f05000b

.line 118 invoke-static {p0, v0, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
 move-result-object v0

.line 119 invoke-virtual {v0}, Landroid/widget/Toast;->show()V
 goto :goto_0
複製代碼

這裏只截取了 onClick 中的部分核心代碼,調用 checkSN() 方法得到一個 Boolean 值,根據這個值來判斷是否註冊成功。這個 checkSN() 方法就是咱們須要重點關注的,我對這個方法的 smali 代碼逐行添加了註釋,仍是很容易理解的,感興趣的同窗能夠看一下:

.method private checkSN(Ljava/lang/String;Ljava/lang/String;)Z
 .locals 10  # 使用 10 個寄存器
 .param p1, "userName"   # Ljava/lang/String; 參數寄存器 p1 保存的是用戶名 userName
 .param p2, "sn"    # Ljava/lang/String; 參數寄存器 p2 保存的是註冊碼 sn

 .prologue
    const/4 v7, 0x0 # 將 0x0 存入寄存器 v7

 .line 45
    if-eqz p1, :cond_0  # 若是 p1,即 userName 等於 0,跳轉到 cond_0

    :try_start_0
    invoke-virtual {p1}, Ljava/lang/String;->length()I # 調用 userName.length()

    move-result v8  # 將 userName.length() 的執行結果存入寄存器 v8

    if-nez v8, :cond_1 # 若是 v8 不等於 0,跳轉到 cond_1

 .line 69
    :cond_0
    :goto_0
    return v7

 .line 47
    :cond_1
    if-eqz p2, :cond_0  # 若是 p2,即註冊碼 sn 等於 0,跳轉到 cond_0

    invoke-virtual {p2}, Ljava/lang/String;->length()I  # 執行 sn.length()

    move-result v8  # 將 sn.length() 執行結果存入寄存器 v8

    const/16 v9, 0x10 # 將 0x10 存入寄存器 v9

    if-ne v8, v9, :cond_0   # 若是 sn.length != 0x10 ,跳轉至 cond_0

 .line 49
    const-string v8, "MD5"  # 將字符串 "MD5" 存入寄存器 v8

    # 調用靜態方法 MessageDigest.getInstance("MD5")
    invoke-static {v8}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;

    move-result-object v1   # 將上一步方法的返回結果賦給寄存器 v1,這裏是 MessageDigest 對象

 .line 50
 .local v1, "digest":Ljava/security/MessageDigest;
    invoke-virtual {v1}, Ljava/security/MessageDigest;->reset()V # 調用 digest.reset() 方法

 .line 51
    invoke-virtual {p1}, Ljava/lang/String;->getBytes()[B   # 調用 userName.getByte() 方法

    move-result-object v8   # 上一步獲得的字節數組存入 v8

    invoke-virtual {v1, v8}, Ljava/security/MessageDigest;->update([B)V # 調用 digest.update(byte[]) 方法

 .line 52
    invoke-virtual {v1}, Ljava/security/MessageDigest;->digest()[B  # 調用 digest.digest() 方法

    move-result-object v0   # 上一步的執行結果存入 v0,是一個 byte[] 對象

 .line 53
 .local v0, "bytes":[B
    const-string v8, "" # 將字符串 "" 存入 v8

    # 調用 MainActivity 中的 toHexString(byte[] b,String s) 方法
    invoke-static {v0, v8}, Lcom/droider/crackme0201/MainActivity;->toHexString([BLjava/lang/String;)Ljava/lang/String;

    move-result-object v3   # 上一步方法返回的字符串存入 v3

 .line 54
 .local v3, "hexstr":Ljava/lang/String;
    new-instance v5, Ljava/lang/StringBuilder;  # 新建 StringBuilder 對象

    invoke-direct {v5}, Ljava/lang/StringBuilder;-><init>()V    # 執行 StringBuilder 的構造函數

 .line 55
 .local v5, "sb":Ljava/lang/StringBuilder;   # 聲明變量 sb 指向剛纔建立的 StringBuilder 實例
    const/4 v4, 0x0 # v4 = 0x0

 .local v4, "i":I    # i = 0x0
    :goto_1 # for 循環開始
    invoke-virtual {v3}, Ljava/lang/String;->length()I  # 獲取 hexstr 字符串的長度

    move-result v8  # v8 = hexstr.length()

    if-lt v4, v8, :cond_2   # 若是 v4 小於 v8,即 i < hexstr.length(), 跳轉到 cond_2

 .line 58
    # 這裏已經跳出 for 循環
    invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v6   # v6 = sb.toString()

 .line 63
 .local v6, "userSN":Ljava/lang/String;  # userSN = sb.toString()

    # userSN.equalsIgnoreCase(sn)
    invoke-virtual {v6, p2}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z

    move-result v8  # v8 = userSN.equalsIgnoreCase(sn)

    if-eqz v8, :cond_0 # 若是 v8 等於 0,跳轉到 cond_0,即 userSN != sn

 .line 69
    const/4 v7, 0x1

    goto :goto_0    # 跳轉到 goto_0,結束 checkSN() 方法並返回 v7

 .line 56 .end local v6    # "userSN":Ljava/lang/String;
    :cond_2
    invoke-virtual {v3, v4}, Ljava/lang/String;->charAt(I)C # 執行 hexstr.charAt(i)

    move-result v8  # v8 = hexstr.charAt(i)

    # 調用 sb.append(v8)
    invoke-virtual {v5, v8}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
    :try_end_0
 .catch Ljava/security/NoSuchAlgorithmException; {:try_start_0 .. :try_end_0} :catch_0

 .line 55
    add-int/lit8 v4, v4, 0x2    # v4 自增 0x2,即 i+=2

    goto :goto_1    # 跳轉到 goto_1,造成 循環

 .line 65 .end local v0    # "bytes":[B .end local v1    # "digest":Ljava/security/MessageDigest; .end local v3    # "hexstr":Ljava/lang/String; .end local v4    # "i":I .end local v5    # "sb":Ljava/lang/StringBuilder;
    :catch_0
    move-exception v2

 .line 66
 .local v2, "e":Ljava/security/NoSuchAlgorithmException;
    invoke-virtual {v2}, Ljava/security/NoSuchAlgorithmException;->printStackTrace()V

    goto :goto_0 .end method
複製代碼

大體邏輯就是對輸入的用戶名 UserName 做 MD5 運算獲得 Hash 值,再轉成十六進制字符串就是註冊碼了。那麼,如何獲取註冊碼呢 ?通常有三種方式,打 log,動態調試 smali,本身寫註冊機。下面逐個說明一下。

打 log 日誌

其實在逆向過程當中,注入 log 代碼是很常見的操做。適當的打 log,能夠很好的幫助咱們理解代碼執行流程。在這裏例子中,最終會拿咱們輸入的註冊碼和正確的註冊碼進行比較,在比較的時候咱們就能夠經過打 log 把正確的註冊碼打印出來,這樣咱們就能夠直接輸入註冊碼進行註冊了。

打 log 的 smali 代碼是固定的,通常格式以下:

const-string vX, "TAG" invoke-static {vX,vX}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
複製代碼

vX 都是指寄存器。把這兩行代碼加到註冊碼的檢驗操做以前就能夠了:

.line 63
.local v6, "userSN":Ljava/lang/String;  # userSN = sb.toString()
 const-string v8, "TAG" invoke-static {v8,v6}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

# userSN.equalsIgnoreCase(sn) invoke-virtual {v6, p2}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
複製代碼

再次從新打包運行,輸入用戶名和註冊碼,就會有以下日誌:

這樣就拿到正確的註冊碼了。

動態調試 smali

動態調試 smali 來的更加直截了當。不論是你本身寫程序,仍是作逆向,debug 永遠都是快速理清邏輯的好方法。smali 也是能夠進行動態調試的,依賴於 Smalidea 插件,你能夠在 Android Studio 的 Plugin 中進行安裝,也能夠下載下來本地安裝。

第一步,咱們要保證咱們的應用處於 debug 版本,在 AndroidManifest.xml 中加上 android:debuggable="true" 便可,重打包再安裝到手機上。

第二步,將以前反編譯獲得的 smali 文件夾導入 Android Studio 或者 IDEA,並配置遠程調試環境。選擇 Run -> Edit Configurations,點擊左上角 + 號,選擇 Remote,彈出配置窗口,以下圖所示:

注意記住本身填寫的端口號,端口號不是固定的,只要未被佔用便可。配置完成後,記得在合適的地方打上斷點,我這裏就在 checkSN() 方法內打上斷點。

第三步,命令行啓動進程調試等待模式。首先執行:

adb shell am start -D -n com.droider.crackme0201/.MainActivity
複製代碼

應用此時會進入等待調試模式,以下圖所示:

而後創建端口轉發,輸入以下命令:

adb forward tcp:8700 jdwp:pid
複製代碼

用你本身的應用的 pid 替換進去。關於 pid 的獲取,能夠經過 psgrep 組合:

adb shell ps | grep com.droider.crackme0201
u0_a364   30110 537   2166480 30204 futex_wait 0000000000 S com.droider.crackme0201
複製代碼

我這裏的 pid 就是 30010

最後在 Android Studio 或 IDEA 中啓動 debug 。 點擊 Run -> Debug,應用就進入調試模式了。以後的操做就和咱們開發中的 debug 模式如出一轍了。咱們能夠在運行中看到寄存器中的值,運行邏輯一覽無遺。運行至註冊碼校驗處的斷點,截圖以下:

userName 是用戶名,sn 是我輸入的註冊碼,userSN 是正確的註冊碼。

註冊機

註冊機其實就是本身重寫註冊碼生成過程了,看懂了 smali 就能夠本身寫個程序來生成註冊碼了。這個就很少說了。

Hook

具體的 Hook 操做因爲篇幅緣由就不在這裏演示了。關於 Java 層的 Hook 工具不少,最廣泛的就是 Xposed,直接 hook checkSN 方法的返回值,或者打印出正確的註冊碼。若是你沒有 Root 設備,還有一系列基於 VirtualApp 的 hook 框架,例如支持 Xposed 應用的 VirtualXposed 等等,固然 VirtualApp 自己也支持 hook 操做。另外,還有 Frida 等等框架,也能夠進行相似的操做。

JADX

最後再介紹一個反編譯利器 JADX ,它能夠直接將 Apk 反編譯成 Java 代碼進行查看,畢竟 smali 代碼不是那麼人性化。我拿到一個 Apk,基本上第一件事就是丟到 JADX 中進行查看,它同時支持命令行操做和圖形化界面。咱們就用 JADX 打開這個 CrackMe 應用看一下:

直接就能夠看到對應的 Java 代碼,理清邏輯以後再去閱讀 smali 代碼進行修改,事半功倍。支持反編譯 Java 代碼的工具還有不少,例如基於 Python 實現的 Androgurad 等等,你們也能夠嘗試去使用一下。

總結

就逆向難度來講,這個 CrackMe 仍是很簡單的,但本文主旨在於介紹一些逆向相關的知識,實際逆向過程當中你面對的任何一個 Apk 確定都比這複雜的多。看到這裏,你應該瞭解到了下面這些知識點:

  • 使用 ApkTool 反編譯以及重打包
  • smali 代碼的基本閱讀能力
  • smali 代碼中注入 log 日誌
  • 動態調試 smali 代碼
  • 經常使用 hook 框架
  • jadx 使用

關於 smali 語法我以前也寫過幾篇文章,往期目錄:

Class 文件格式詳解

Smali 語法解析——Hello World

Smali —— 數學運算,條件判斷,循環

Smali 語法解析 —— 類

Android逆向筆記 —— AndroidManifest.xml 文件格式解析

Android逆向筆記 —— DEX 文件格式解析

下一篇來寫寫 Android Apk 中資源包文件 resources.arsc 的文件結構,一樣會配套思惟導圖和 Java 源碼解析。

文章首發微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解。

更多 JDK 源碼解析,掃碼關注我吧!

相關文章
相關標籤/搜索