長生劍是把神奇的劍,爲白玉京所配,劍名取意來自於李白的詩:「仙人撫我頂,結髮受長生。」長生劍是七種武器系列的第一種武器,而筆者接下來所要介紹的調試方法也是我最先學習的調試方法,而且這種方法就像長生劍同樣,簡單並一直都有很好的效果。這種方法就是Smali Instrumentation,又稱Smali 插樁。使用這種方法最大的好處就是不須要對手機進行root,不須要指定android的版本,若是結合一些tricks的話還會有意想不到的效果。java
作安卓逆向最早接觸到的東西確定就是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操做就不會有這些問題了。android
學習smali最好的方法就是本身先用java寫好程序,再用baksmali轉換成smali語句,而後對照學習。好比下面就是java代碼和用baksmali反編譯事後的smali文件的對照分析。git
MZLog類主要是用Log.d()輸出調試信息,Java代碼以下:github
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代碼以下:數組
.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)
若是僅僅用Smali來分析代碼,效果其實不如用dex2jar和jd-gui更直觀,畢竟看反編譯的java代碼要更容易一些。但Smali強大之處就是能夠爲所欲爲的進行插樁操做。何爲插樁,引用一下wiki的解釋:程序插樁,最先是由J.C. Huang
教授提出的,它是在保證被測程序原有邏輯完整性的基礎上在程序中插入一些探針(又稱爲「探測儀」),經過探針的執行並拋出程序運行的特徵數據,經過對這些數據的分析,能夠得到程序的控制流和數據流信息,進而獲得邏輯覆蓋等動態信息,從而實現測試目的的方法。下面我就來結合一個例子來說解一下何如進行smali插樁。函數
測試程序是一個簡單的crackme (圖1)。輸入密碼,而後點擊check,若是密碼正確會輸出yes,不然輸出no。工具
圖1 Crackme1的界面學習
首先咱們對crackme這個apk進行解壓,而後反編譯。咱們會在MainActivity
中看到一個getkey(String,int)
函數。這個函數貌似很是複雜,咱們暫時無論。咱們首先分析一下點下button後的邏輯。咱們發現程序會經過getkey("mrkxqcroxqtskx",42)
來計算出真正的密碼,而後與咱們輸人的密碼進行比較,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進行反編譯。
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進行簽名。幾條關鍵指令以下:
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)。
圖2 經過logcat獲取getkey的返回值
經過Smali/baksmali工具,咱們不光能夠插樁,還能夠修改apk的邏輯。幾個須要注意點以下:
1. if條件判斷以及跳轉語句
在smali中最多見的就是if這個條件判斷跳轉語句了,這個判斷一共有12條指令:
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代碼段:
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數量上增長相應的數值。
好比下面這個方法:
.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的方式調用新加入的邏輯。這樣的話無論加入再多的邏輯,也只是修改了原程序的幾行代碼而已。這個思路也是不少重打包病毒慣用的伎倆,確實很是方便好用。
當咱們在實戰中,有時會碰到某些apk在內部實現了本身的簽名檢查。此次咱們介紹的Smali Instrumentation
方法由於須要重打包,因此會改變原有的簽名。固然,你能夠經過修改apk把簽名檢查的邏輯刪掉,但這又費時又費力。筆者在這裏簡單介紹兩種很是方便的方法來解決簽名檢查問題。
1. Masterkey
Masterkey
漏洞一共有三個,能夠影響android 4.4如下版本。利用這個漏洞,咱們能夠插入新的classes.dex
替換掉原有的classes.dex
而不須要對apk自己進行從新簽名。若是apk自己有簽名校驗邏輯的話,利用這個漏洞來進行Smali Instrumentation
簡直再好不過了。首先,你須要一個android 4.4如下版本的虛擬機或者真機,而後再使用一個masterkey
利用工具對apk進行exploit便可。工具下載地址在文章最後,使用的命令以下:
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
。
圖3 Masterkey生成的apk文件有兩個classes.dex文件
經過masterkey打包後的apk文件簽名並不會有任何變化,這樣也就不用擔憂簽名校驗問題了。
2. 自定義ROM
簽名的判斷實際上是調用了android系統密碼庫的函數,若是咱們能夠本身定製ROM的話,只須要修改AOSP源碼路徑下的libcore\luni\src\main\java\java\security\MessageDigest.java
文件。將isEqual函數中的判斷語句註釋掉:
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文件,都不須要關心簽名問題了,系統會永遠返回簽名正確的。
雖然如今愈來愈多的apk開始使用so文件進行邏輯處理和加固,android 4.4也加入art運行環境,但dalvik永遠是android最經典的東西。若是想要學好android逆向,必定要把這部分知識學好。而且把smali研究透徹之後,會對咱們之後要講的自定義dalvik虛擬機有很大幫助。另外文章中全部提到的代碼和工具均可以在個人github下載到,地址是: https://github.com/zhengmin1989/TheSevenWeapons