Android高效调试技巧02——反编译

今天要给大家介绍一项调试技巧——反编译。因为我们遇到的问题有时会跟第三方应用有关,对于这些我们没有代码的应用,如果不进行反编译,很难对其进行分析。

说到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 进行反编译;

反编译举例

问题描述:
  在从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 ()V
.locals0
.prologue
.line 28
invoke-direct{p0}, Landroid/content/BroadcastReceiver;->()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;->(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模块下有对应的提交,这样就定位到了问题点。

总结

反编译作为一项调试技巧,在通往高手之路上是不必可少的。虽然反编译后的代码比较难懂,但是多加练习总有一天会得心应手。

你可能感兴趣的:(Android高效调试技巧02——反编译)