今天要給大家介紹一項調試技巧——反編譯。因為我們遇到的問題有時會跟第三方應用有關,對于這些我們沒有代碼的應用,如果不進行反編譯,很難對其進行分析。
說到apk反編譯不得不說代碼混淆,在簽名打包時啟動代碼混淆可以幫助我們保護代碼。曾經聽說有小公司發布了未做代碼混淆的應用,結果被別人反編譯后上架,辛辛苦苦做出的應用就這樣被別人竊取了,所以保護好自己的勞動成果很重要。除了代碼混淆,有些應用商店還會要求開發者進行安裝包加固,以提高安全性。
反編譯就是把對代碼做的這些保護解開,讓我們了解別人代碼的部分細節。比如下面要介紹的例子,GoogleSetupWizard 引起的問題,雖然我們沒有它的源碼,但是通過反編譯我們可以了解內部的邏輯。
反編譯工具
在處理這個問題之前我用的反編譯工具一直是Dex2Jar+jd-gui,用法簡單,只要將 apk解壓縮,把解壓出的dex文件用Dex2Jar反編譯,然后用jd-gui工具閱讀即可,對于沒有混淆的apk,閱讀起來跟閱讀代碼體驗差不多。但是在反編譯 GoogleSetupWizard 的時候卻出錯(提示notsupport version)。咨詢了別的同事建議試試apktool,解出smali文件。關于smali,這里有一份文檔供大家參考Smali學習筆記。
apktool的下載和使用: 將apktool.jar和apktool放到/usr/local/bin 目錄(需要root權限);運行apktool d <apk-file> 進行反編譯;
反編譯舉例
問題描述:
在從Owner賬戶切換到Guest賬戶時失敗,自動又切回Owner賬戶,并且狀態欄無法正常下拉。
問題分析:
該問題第一個難點是確認誰觸發了切回Owner賬戶的動作。這里我們用android.util.Log類的Log.getStackTraceString(newThrowable()) 方法來完成。賬戶切換會調用ActivityManagerNative類的switchUser方法,我們在該方法中添加如下語句:
Log.i(TAG,Log.getStackTraceString(newThrowable()));
這樣調用關系就一目了然了。結果如下:
ActivityManagerNative: at android.app.ActivityManagerProxy.switchUser(ActivityManagerNative.java:5866)
ActivityManagerNative: atcom.google.android.setupwizard.util.UserHelper.removeThisUser(UserHelper.java:43)
ActivityManagerNative: atcom.google.android.setupwizard.lifecycle.ProvisioningFlagsLifecycleCallback.onExit
(ProvisioningFlagsLifecycleCallback.java:49)
ActivityManagerNative: at com.google.android.setupwizard.lifecycle.LifecycleManager.notifyExit(LifecycleManager.java:115)
ActivityManagerNative: atcom.google.android.setupwizard.util.ExitHelper.finishSetup(ExitHelper.java:45)
ActivityManagerNative: at
**com.google.android.setupwizard.util.SetupWizardUserInitReceiver**
.onReceive (SetupWizardUserInitReceiver.java:33)
可以看出調用者是com.google.android.setupwizard應用的SetupWizardUserInitReceiver類,它是一個Google gms 應用。我們用apktool將其反編譯,SetupWizardUserInitReceiver.smali
文件如下:
.superLandroid/content/BroadcastReceiver;
.source"SetupWizardUserInitReceiver.java"
#direct methods
.methodpublic constructor <init>()V
.locals0
.prologue
.line 28
invoke-direct{p0}, Landroid/content/BroadcastReceiver;-><init>()V
return-void
.endmethod
#virtual methods
.methodpublic onReceive(Landroid/content/Context;Landroid/content/Intent;)V
.locals2
.paramp1, "context" # Landroid/content/Context;
.paramp2, "intent" # Landroid/content/Intent;
.prologue
.line 31
const-string/jumbov0, "android.intent.action.USER_INITIALIZE"
invoke-virtual{p2}, Landroid/content/Intent;->getAction()Ljava/lang/String;
move-result-objectv1
invoke-virtual{v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-resultv0
if-eqzv0, :cond_0
.line 32
invoke-static{p1},Landroid/os/UserManager;->get(Landroid/content/Context;)Landroid/os/UserManager;
move-result-objectv0
invoke-virtual{v0}, Landroid/os/UserManager;->isGuestUser()Z
move-resultv0
.line 31
if-eqzv0, :cond_0
.line 33
invoke-static{p1},Lcom/google/android/setupwizard/util/ExitHelper;->finishSetup(Landroid/content/Context;)V
.line 30
:cond_0
return-void
.endmethod
這是一個BroadcastReceiver,我們要關注的是它的onReceive方法。通過網上搜索smali語法,可以判斷出當為Guest用戶時會調用ExitHelper的finishSetup方法。我們再看這個方法,
.methodpublic static finishSetup(Landroid/content/Context;)V
.locals2
.paramp0, "context" # Landroid/content/Context;
.prologue
.line 45
invoke-static{},Lcom/google/android/setupwizard/lifecycle/LifecycleManager;->get()Lcom/google/android/setupwizard/lifecycle/LifecycleManager;
move-result-objectv0
invoke-virtual{v0, p0},Lcom/google/android/setupwizard/lifecycle/LifecycleManager;->notifyExit(Landroid/content/Context;)V
.line 49
>new-instancev0, Landroid/content/Intent;
const-string/jumbov1, "com.google.android.setupwizard.SETUP_WIZARD_FINISHED"
invoke-direct{v0, v1}, Landroid/content/Intent;-><init>(Ljava/lang/String;)V
invoke-virtual{p0, v0},Landroid/content/Context;->sendBroadcast(Landroid/content/Intent;)V
.line 42
return-void
.endmethod
上面先拿到了一個LifecycleManager 實例,然后調用了LifecycleManager的notifyExit方法,再發送了一個com.google.android.setupwizard.SETUP_WIZARD_FINISHED廣播。最終,我們跟蹤到調用用戶切換的代碼如下:
50 const-string/jumbo v3, "device_provisioned"**
51
52 invoke-static {v0, v3, v6},Landroid/provider/Settings$Global;>putInt(Landroid/content/ContentResolver;Ljava/lang/String;I)Z
53
54 .line 56
55 :cond_0
56 :goto_0
57 const-string/jumbo v3, "user_setup_complete"
58
59 invoke-static {v0, v3, v6},Landroid/provider/Settings$Secure;->putInt(Landroid/content/ContentResolver;Ljava/lang/String;I)Z
60
61 .line 34
62 return-void
63
64 .line 45
65 :cond_1
66 invoke-static {p1},Lcom/android/setupwizardlib/util/WizardManagerHelper;->isDeviceProvisioned(Landroid/content/Context;)Z
67
68 move-result v3
69
70 if-nez v3, :cond_0
71
72 .line 48
73 invoke-virtual {v2}, Landroid/os/UserManager;->getUserHandle()I
74
75 move-result v1
76
77 .line 49
78 .local v1, "user":I
79 invoke-static {p1},Lcom/google/android/setupwizard/util/UserHelper;->removeThisUser(Landroid/content/Context;)Z
上面會去讀device_provisioned這個setting項,當為false時會退出當前賬戶。通過在源碼下搜索,果然發現在packages/apps/Provision模塊下有對應的提交,這樣就定位到了問題點。
總結
反編譯作為一項調試技巧,在通往高手之路上是不必可少的。雖然反編譯后的代碼比較難懂,但是多加練習總有一天會得心應手。