Android 逆向筆記 —— 一個簡單 CrackMe 的逆向總結(jié)

無意中在看雪看到一個簡單的 CrackMe 應(yīng)用,正好就著這個例子總結(jié)一下逆向過程中基本的常用工具的使用,和一些簡單的常用套路。感興趣的同學可以照著嘗試操作一下,過程還是很簡單的。APK 我已上傳至 Github,下載地址。

首先安裝一下這個應(yīng)用,界面如下所示:

image

要求就是通過注冊。爆破的方法很多,大致可以歸為三類,第一種是直接修改 smali 代碼繞過注冊,第二種是捋清注冊流程,得到正確的注冊碼。第三種是 hook 。下面就來說說這幾種爆破過程。

直接修改 smali 進行爆破

要獲取 smali 代碼,首先得反編譯這個 Apk,通過 ApkTool 就可以完成。ApkTool 的使用過程就不在這里贅述了,執(zhí)行如下命令:

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 文件夾,文件夾目錄如下:

image

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

導入 Android Studio 之后,看到了所有的 smali 代碼,那么我們該從何下手呢?注冊失敗的時候會彈一個 Toast,“無效用戶名或注冊碼”,這就是突破口。全局搜索這個字符串,


image

發(fā)現(xiàn)這個字符串定義在 string.xml 中的 unsuccessd ,在寫代碼的時候就是 R.string.unsuccessd,這是一個 int 值,編譯后就直接是一個數(shù)字了。我們再來全局搜索 unsuccessd :

image

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

image

可以看到這個 id 在 MainActivity.smali 中的 433 行使用到了,我們定位到這個文件:

    .line 117
    if-nez v0, :cond_0  # 如果 v0 不等于 0 ,跳轉(zhuǎn)到 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 都可以。簽名之后安裝,輸入用戶名:

image

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

獲取注冊碼爆破

我們之前已經(jīng)找到了具體的邏輯是在 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 中的部分核心代碼,調(diào)用 checkSN() 方法獲得一個 Boolean 值,根據(jù)這個值來判斷是否注冊成功。這個 checkSN() 方法就是我們需要重點關(guān)注的,我對這個方法的 smali 代碼逐行添加了注釋,還是很容易理解的,感興趣的同學可以看一下:

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

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

    .line 45
    if-eqz p1, :cond_0  # 如果 p1,即 userName 等于 0,跳轉(zhuǎn)到 cond_0

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

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

    if-nez v8, :cond_1 # 如果 v8 不等于 0,跳轉(zhuǎn)到 cond_1

    .line 69
    :cond_0
    :goto_0
    return v7

    .line 47
    :cond_1
    if-eqz p2, :cond_0  # 如果 p2,即注冊碼 sn 等于 0,跳轉(zhuǎn)到 cond_0

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

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

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

    if-ne v8, v9, :cond_0   # 如果 sn.length != 0x10 ,跳轉(zhuǎn)至 cond_0

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

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

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

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

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

    move-result-object v8   # 上一步得到的字節(jié)數(shù)組存入 v8

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

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

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

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

    # 調(diào)用 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    # 執(zhí)行 StringBuilder 的構(gòu)造函數(shù)

    .line 55
    .local v5, "sb":Ljava/lang/StringBuilder;   # 聲明變量 sb 指向剛才創(chuàng)建的 StringBuilder 實例
    const/4 v4, 0x0 # v4 = 0x0

    .local v4, "i":I    # i = 0x0
    :goto_1 # for 循環(huán)開始
    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(), 跳轉(zhuǎn)到 cond_2

    .line 58
    # 這里已經(jīng)跳出 for 循環(huán)
    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,跳轉(zhuǎn)到 cond_0,即 userSN != sn

    .line 69
    const/4 v7, 0x1

    goto :goto_0    # 跳轉(zhuǎn)到 goto_0,結(jié)束 checkSN() 方法并返回 v7

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

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

    # 調(diào)用 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    # 跳轉(zhuǎn)到 goto_1,形成 循環(huán)

    .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 值,再轉(zhuǎn)成十六進制字符串就是注冊碼了。那么,如何獲取注冊碼呢 ?一般有三種方式,打 log,動態(tài)調(diào)試 smali,自己寫注冊機。下面逐個說明一下。

打 log 日志

其實在逆向過程中,注入 log 代碼是很常見的操作。適當?shù)拇?log,可以很好的幫助我們理解代碼執(zhí)行流程。在這里例子中,最終會拿我們輸入的注冊碼和正確的注冊碼進行比較,在比較的時候我們就可以通過打 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

再次重新打包運行,輸入用戶名和注冊碼,就會有如下日志:

image

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

動態(tài)調(diào)試 smali

動態(tài)調(diào)試 smali 來的更加直截了當。不管是你自己寫程序,還是做逆向,debug 永遠都是快速理清邏輯的好方法。smali 也是可以進行動態(tài)調(diào)試的,依賴于 Smalidea 插件,你可以在 Android Studio 的 Plugin 中進行安裝,也可以下載下來本地安裝。

第一步,我們要保證我們的應(yīng)用處于 debug 版本,在 AndroidManifest.xml 中加上 android:debuggable="true" 即可,重打包再安裝到手機上。

第二步,將之前反編譯得到的 smali 文件夾導入 Android Studio 或者 IDEA,并配置遠程調(diào)試環(huán)境。選擇 Run -> Edit Configurations,點擊左上角 + 號,選擇 Remote,彈出配置窗口,如下圖所示:

image

注意記住自己填寫的端口號,端口號不是固定的,只要未被占用即可。配置完成后,記得在合適的地方打上斷點,我這里就在 checkSN() 方法內(nèi)打上斷點。

第三步,命令行啟動進程調(diào)試等待模式。首先執(zhí)行:

adb shell am start -D -n com.droider.crackme0201/.MainActivity

應(yīng)用此時會進入等待調(diào)試模式,如下圖所示:

image

然后建立端口轉(zhuǎn)發(fā),輸入如下命令:

adb forward tcp:8700 jdwp:pid

用你自己的應(yīng)用的 pid 替換進去。關(guān)于 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,應(yīng)用就進入調(diào)試模式了。之后的操作就和我們開發(fā)中的 debug 模式一模一樣了。我們可以在運行中看到寄存器中的值,運行邏輯一覽無遺。運行至注冊碼校驗處的斷點,截圖如下:

image

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

注冊機

注冊機其實就是自己重寫注冊碼生成過程了,看懂了 smali 就可以自己寫個程序來生成注冊碼了。這個就不多說了。

Hook

具體的 Hook 操作由于篇幅原因就不在這里演示了。關(guān)于 Java 層的 Hook 工具很多,最普遍的就是 Xposed,直接 hook checkSN 方法的返回值,或者打印出正確的注冊碼。如果你沒有 Root 設(shè)備,還有一系列基于 VirtualApp 的 hook 框架,例如支持 Xposed 應(yīng)用的 VirtualXposed 等等,當然 VirtualApp 本身也支持 hook 操作。另外,還有 Frida 等等框架,也可以進行類似的操作。

JADX

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

image

直接就可以看到對應(yīng)的 Java 代碼,理清邏輯之后再去閱讀 smali 代碼進行修改,事半功倍。支持反編譯 Java 代碼的工具還有很多,例如基于 Python 實現(xiàn)的 Androgurad 等等,大家也可以嘗試去使用一下。

總結(jié)

就逆向難度來說,這個 CrackMe 還是很簡單的,但本文主旨在于介紹一些逆向相關(guān)的知識,實際逆向過程中你面對的任何一個 Apk 肯定都比這復雜的多。看到這里,你應(yīng)該了解到了下面這些知識點:

  • 使用 ApkTool 反編譯以及重打包
  • smali 代碼的基本閱讀能力
  • smali 代碼中注入 log 日志
  • 動態(tài)調(diào)試 smali 代碼
  • 常用 hook 框架
  • jadx 使用

關(guān)于 smali 語法我之前也寫過幾篇文章,往期目錄:

Class 文件格式詳解

Smali 語法解析——Hello World

Smali —— 數(shù)學運算,條件判斷,循環(huán)

Smali 語法解析 —— 類

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

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

下一篇來寫寫 Android Apk 中資源包文件 resources.arsc 的文件結(jié)構(gòu),同樣會配套思維導圖和 Java 源碼解析。

文章首發(fā)微信公眾號: 秉心說 , 專注 Java 、 Android 原創(chuàng)知識分享,LeetCode 題解。

更多 JDK 源碼解析,掃碼關(guān)注我吧!

green.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容