無意中在看雪看到一個(gè)簡(jiǎn)單的 CrackMe 應(yīng)用券册,正好就著這個(gè)例子總結(jié)一下逆向過程中基本的常用工具的使用滋捶,和一些簡(jiǎn)單的常用套路逆日。感興趣的同學(xué)可以照著嘗試操作一下,過程還是很簡(jiǎn)單的奠滑。APK 我已上傳至 Github丹皱,下載地址。
首先安裝一下這個(gè)應(yīng)用宋税,界面如下所示:
要求就是通過注冊(cè)摊崭。爆破的方法很多,大致可以歸為三類杰赛,第一種是直接修改 smali 代碼繞過注冊(cè)呢簸,第二種是捋清注冊(cè)流程,得到正確的注冊(cè)碼乏屯。第三種是 hook 根时。下面就來說說這幾種爆破過程。
直接修改 smali 進(jìn)行爆破
要獲取 smali 代碼辰晕,首先得反編譯這個(gè) 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...
會(huì)在當(dāng)前目錄生成 crackme
文件夾替裆,文件夾目錄如下:
其中的 smali
文件夾就包含了該 Apk 的所有 smali 代碼校辩。閱讀和修改 smali 代碼的工具很多,我個(gè)人偏好將整個(gè)反編譯得到的文件夾導(dǎo)入 IDEA 或者 Android Studio 進(jìn)行閱讀和修改辆童,可能我是 Android 開發(fā)宜咒,用這兩個(gè)工具會(huì)比較順手,全局搜索功能也很給力胸遇。
導(dǎo)入 Android Studio 之后荧呐,看到了所有的 smali 代碼,那么我們?cè)搹暮蜗率帜刂侥鳎孔?cè)失敗的時(shí)候會(huì)彈一個(gè) Toast倍阐,“無效用戶名或注冊(cè)碼”,這就是突破口逗威。全局搜索這個(gè)字符串峰搪,
發(fā)現(xiàn)這個(gè)字符串定義在 string.xml
中的 unsuccessd
,在寫代碼的時(shí)候就是 R.string.unsuccessd
凯旭,這是一個(gè) int 值概耻,編譯后就直接是一個(gè)數(shù)字了。我們?cè)賮砣炙阉?unsuccessd
:
在 public.xml
中可以看到它的 id
,代碼中直接使用的就是這個(gè) id了罐呼。全局搜索一下 0x7f05000b
鞠柄,看一下這個(gè) Toast 是在哪里彈出的。
可以看到這個(gè) id 在 MainActivity.smali
中的 433 行使用到了嫉柴,我們定位到這個(gè)文件:
.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
這段邏輯很簡(jiǎn)單。判斷寄存器 v0 的值是否為 0计螺,不為 0 的話則彈出 “無效用戶名或注冊(cè)碼” 夯尽。所以最簡(jiǎn)單的改法,邏輯反一下登馒,v0 為 0 的時(shí)候彈出該 Toast匙握,把 if-nez
改為 if-ez
即可。修改之后使用 ApkTool
重打包陈轿,重打包命令如下:
apktool b crackme -o crackme_new.apk
會(huì)在當(dāng)前目錄生成 crackme_new.apk
文件圈纺,注意這個(gè)安裝包是未簽名的,無法直接安裝济欢,需要先簽名赠堵。使用 jarsinger
或者 apksigner
都可以。簽名之后安裝法褥,輸入用戶名:
這樣就注冊(cè)成功了茫叭。方法雖然有點(diǎn) low ,但好歹爆破成功了半等。下面我們不修改 smali 代碼揍愁,通過閱讀 smali 代碼理解其注冊(cè)碼生成邏輯呐萨,通過正規(guī)方式來注冊(cè)。
獲取注冊(cè)碼爆破
我們之前已經(jīng)找到了具體的邏輯是在 MainActivity.smali
中莽囤,找到這個(gè)按鈕的 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()
方法獲得一個(gè) Boolean 值朽缎,根據(jù)這個(gè)值來判斷是否注冊(cè)成功惨远。這個(gè) checkSN()
方法就是我們需要重點(diǎn)關(guān)注的,我對(duì)這個(gè)方法的 smali 代碼逐行添加了注釋话肖,還是很容易理解的北秽,感興趣的同學(xué)可以看一下:
.method private checkSN(Ljava/lang/String;Ljava/lang/String;)Z
.locals 10 # 使用 10 個(gè)寄存器
.param p1, "userName" # Ljava/lang/String; 參數(shù)寄存器 p1 保存的是用戶名 userName
.param p2, "sn" # Ljava/lang/String; 參數(shù)寄存器 p2 保存的是注冊(cè)碼 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,即注冊(cè)碼 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 對(duì)象
.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邢锯,是一個(gè) byte[] 對(duì)象
.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 對(duì)象
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 實(shí)例
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
大致邏輯就是對(duì)輸入的用戶名 UserName 作 MD5 運(yùn)算得到 Hash 值,再轉(zhuǎn)成十六進(jìn)制字符串就是注冊(cè)碼了庶骄。那么毁渗,如何獲取注冊(cè)碼呢 ?一般有三種方式单刁,打 log灸异,動(dòng)態(tài)調(diào)試 smali,自己寫注冊(cè)機(jī)羔飞。下面逐個(gè)說明一下肺樟。
打 log 日志
其實(shí)在逆向過程中,注入 log 代碼是很常見的操作逻淌。適當(dāng)?shù)拇?log么伯,可以很好的幫助我們理解代碼執(zhí)行流程。在這里例子中卡儒,最終會(huì)拿我們輸入的注冊(cè)碼和正確的注冊(cè)碼進(jìn)行比較田柔,在比較的時(shí)候我們就可以通過打 log 把正確的注冊(cè)碼打印出來俐巴,這樣我們就可以直接輸入注冊(cè)碼進(jìn)行注冊(cè)了。
打 log 的 smali 代碼是固定的硬爆,一般格式如下:
const-string vX, "TAG"
invoke-static {vX,vX}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
vX
都是指寄存器欣舵。把這兩行代碼加到注冊(cè)碼的檢驗(yàn)操作之前就可以了:
.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
再次重新打包運(yùn)行,輸入用戶名和注冊(cè)碼缀磕,就會(huì)有如下日志:
這樣就拿到正確的注冊(cè)碼了缘圈。
動(dòng)態(tài)調(diào)試 smali
動(dòng)態(tài)調(diào)試 smali 來的更加直截了當(dāng)。不管是你自己寫程序袜蚕,還是做逆向糟把,debug 永遠(yuǎn)都是快速理清邏輯的好方法。smali 也是可以進(jìn)行動(dòng)態(tài)調(diào)試的廷没,依賴于 Smalidea 插件糊饱,你可以在 Android Studio 的 Plugin 中進(jìn)行安裝,也可以下載下來本地安裝颠黎。
第一步另锋,我們要保證我們的應(yīng)用處于 debug 版本,在 AndroidManifest.xml 中加上 android:debuggable="true"
即可狭归,重打包再安裝到手機(jī)上夭坪。
第二步,將之前反編譯得到的 smali 文件夾導(dǎo)入 Android Studio 或者 IDEA过椎,并配置遠(yuǎn)程調(diào)試環(huán)境室梅。選擇 Run -> Edit Configurations,點(diǎn)擊左上角 + 號(hào)疚宇,選擇 Remote亡鼠,彈出配置窗口,如下圖所示:
注意記住自己填寫的端口號(hào)敷待,端口號(hào)不是固定的间涵,只要未被占用即可。配置完成后榜揖,記得在合適的地方打上斷點(diǎn)勾哩,我這里就在 checkSN()
方法內(nèi)打上斷點(diǎn)。
第三步举哟,命令行啟動(dòng)進(jìn)程調(diào)試等待模式思劳。首先執(zhí)行:
adb shell am start -D -n com.droider.crackme0201/.MainActivity
應(yīng)用此時(shí)會(huì)進(jìn)入等待調(diào)試模式,如下圖所示:
然后建立端口轉(zhuǎn)發(fā)妨猩,輸入如下命令:
adb forward tcp:8700 jdwp:pid
用你自己的應(yīng)用的 pid 替換進(jìn)去潜叛。關(guān)于 pid 的獲取,可以通過 ps
和 grep
組合:
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 中啟動(dòng) debug 钠导。 點(diǎn)擊 Run -> Debug震嫉,應(yīng)用就進(jìn)入調(diào)試模式了。之后的操作就和我們開發(fā)中的 debug 模式一模一樣了牡属。我們可以在運(yùn)行中看到寄存器中的值票堵,運(yùn)行邏輯一覽無遺。運(yùn)行至注冊(cè)碼校驗(yàn)處的斷點(diǎn)逮栅,截圖如下:
userName
是用戶名悴势,sn
是我輸入的注冊(cè)碼,userSN
是正確的注冊(cè)碼措伐。
注冊(cè)機(jī)
注冊(cè)機(jī)其實(shí)就是自己重寫注冊(cè)碼生成過程了特纤,看懂了 smali 就可以自己寫個(gè)程序來生成注冊(cè)碼了。這個(gè)就不多說了侥加。
Hook
具體的 Hook 操作由于篇幅原因就不在這里演示了捧存。關(guān)于 Java 層的 Hook 工具很多,最普遍的就是 Xposed担败,直接 hook checkSN
方法的返回值昔穴,或者打印出正確的注冊(cè)碼。如果你沒有 Root 設(shè)備提前,還有一系列基于 VirtualApp 的 hook 框架吗货,例如支持 Xposed 應(yīng)用的 VirtualXposed 等等,當(dāng)然 VirtualApp 本身也支持 hook 操作狈网。另外宙搬,還有 Frida 等等框架,也可以進(jìn)行類似的操作拓哺。
JADX
最后再介紹一個(gè)反編譯利器 JADX 勇垛,它可以直接將 Apk 反編譯成 Java 代碼進(jìn)行查看,畢竟 smali 代碼不是那么人性化士鸥。我拿到一個(gè) Apk窥摄,基本上第一件事就是丟到 JADX 中進(jìn)行查看,它同時(shí)支持命令行操作和圖形化界面础淤。我們就用 JADX 打開這個(gè) CrackMe 應(yīng)用看一下:
直接就可以看到對(duì)應(yīng)的 Java 代碼,理清邏輯之后再去閱讀 smali 代碼進(jìn)行修改哨苛,事半功倍鸽凶。支持反編譯 Java 代碼的工具還有很多,例如基于 Python 實(shí)現(xiàn)的 Androgurad 等等建峭,大家也可以嘗試去使用一下玻侥。
總結(jié)
就逆向難度來說,這個(gè) CrackMe 還是很簡(jiǎn)單的亿蒸,但本文主旨在于介紹一些逆向相關(guān)的知識(shí)凑兰,實(shí)際逆向過程中你面對(duì)的任何一個(gè) Apk 肯定都比這復(fù)雜的多掌桩。看到這里姑食,你應(yīng)該了解到了下面這些知識(shí)點(diǎn):
- 使用 ApkTool 反編譯以及重打包
- smali 代碼的基本閱讀能力
- smali 代碼中注入 log 日志
- 動(dòng)態(tài)調(diào)試 smali 代碼
- 常用 hook 框架
- jadx 使用
關(guān)于 smali 語法我之前也寫過幾篇文章波岛,往期目錄:
Smali —— 數(shù)學(xué)運(yùn)算,條件判斷音半,循環(huán)
下一篇來寫寫 Android Apk 中資源包文件 resources.arsc
的文件結(jié)構(gòu)则拷,同樣會(huì)配套思維導(dǎo)圖和 Java 源碼解析。
文章首發(fā)微信公眾號(hào):
秉心說
曹鸠, 專注 Java 煌茬、 Android 原創(chuàng)知識(shí)分享,LeetCode 題解彻桃。更多 JDK 源碼解析坛善,掃碼關(guān)注我吧!