1. 引入

当我们拿到一个APK,没有源代码,该怎么样去研究APK的核心逻辑呢?

限于运行环境的复杂,我们会首先使用静态分析的方式,大概可以想出这样一些静态分析APK的方法:

  1. 用apktool直接将APK转换为smali程序,再阅读smali代码(比较痛苦)

  2. 用dextojar将APK中的DEX转换为jar,再用JD-JUI来查看其java代码

  3. 用JEB,直接查看java或smali(JEB是收费软件,比较贵)

当我们做这样的静态分析,定位到某些关键逻辑,就要用动态调试的手段来观测某些变量的值了。

这时候,我们就会在APK中,关键逻辑的地方,插入LOG,然后运行观测,这个过程就是本文要讲述的内容。

本文的实验环境如下:

  • windows 10
  • apktool_2.4.0.jar
  • java version “1.8.0_201”

2. 要分析的APK

我们自己写了一个很简单的APK(文件名为hello-apk.apk),后面称之为hello-apk,专门用于反编译分析。分析其他复杂APK的过程也和本文讲述的过程是一样的,只是用这个简单hello-apk,更能清晰、简洁、易懂的说明这个过程。

hello-apk的界面很简单,就是一个button,加上一个textview。当我们单击button,就会在textview上显示"Hello world! click by button!!"。其核心逻辑见下面代码


public class MainActivity extends AppCompatActivity {

    private int count=0;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void btn1ClickEvent(View target){
        count++;
        TextView txt=(TextView)findViewById(R.id.mytextview);//find output label by id
        txt.setText("Hello world! click by button!!");
    }
}

这里的关键逻辑,就是函数btn1ClickEvent()中的内容,它负责处理button的click事件。

接下来我们讲解如何查看count值的过程。

3. 安装配置apktool的步骤

  1. 具体步骤参考: https://ibotpeaches.github.io/Apktool/install/

  2. 将如下链接中显示的脚本内容保存为apktool.bat

  • https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/windows/apktool.bat
  1. 到如下链接下载最新版本的apktool
  • https://bitbucket.org/iBotPeaches/apktool/downloads/
  1. 将下载的apktool重命名为apktool.jar,并与apktool.bat放在同一个文件夹,添加环境变量

本文将hello-apk与apktool.jarapktool.bat放在同一个文件夹,所以省去了添加环境变量的步骤。

4. 用apktool反编译APK为smali

使用如下命令(apktool d apkfile)来反编译apk

>apktool d hello-apk.apk

解包执行结束后,我们在当前目录得到hello-apk文件夹,里面就是从APK中反编译出来的smali代码,资源文件,与manifest文件。

5. 插入调试用到的smali代码

在解包得到的smali文件夹中,我们可以找到需要添加LOG的smali文件,位于hello-apk/smali/com/example/ybdesire/hello/MainActivity.smali

关键函数btn1ClickEvent()的smali代码如下,可以对比上面的纯java代码,这就是java编译后的smali:

# virtual methods
.method public btn1ClickEvent(Landroid/view/View;)V
    .locals 2#这个函数中,使用了2个局部变量,就是下面的v0,v1
    .param p1, "target"    # Landroid/view/View;# 函数形参

    .line 19#将count变量++
    iget v0, p0, Lcom/example/ybdesire/hello/MainActivity;->count:I

    add-int/lit8 v0, v0, 0x1

    iput v0, p0, Lcom/example/ybdesire/hello/MainActivity;->count:I

    .line 20#找到textview的id
    const v0, 0x7f070052

    invoke-virtual {p0, v0}, Lcom/example/ybdesire/hello/MainActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/widget/TextView;

    .line 21#将字符串显示到textview上
    .local v0, "txt":Landroid/widget/TextView;
    const-string v1, "Hello world! click by button!!"

    invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

    .line 23
    return-void
.end method

我们想在上面程序的最后,插入一行java程序的smali代码,这样我们就能在程序运行时,通过log输出其局部变量的值,也就是能够动态查看中间结果了。

Log.d("btn_debug", "count="+count);//这是想插入的java程序

这一行java程序对应的smali程序如下:

    .line 22
    const-string v1, "btn_debug"

    new-instance v2, Ljava/lang/StringBuilder;

    invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V

    const-string v3, "count="

    invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    # 通过阅读上面的smali,可以发现,count变量的值会被放到p0
    iget v3, p0, Lcom/example/ybdesire/hello/MainActivity;->count:I

    invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;

    invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v2

    invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

接下来我们来修改smali:

  1. 将这段smali程序,插入到hello-apk/smali/com/example/ybdesire/hello/MainActivity.smali.line 23上面。

  2. .locals 2改为.locals 4,因为上面插入的smali代码,还使用了v2与v3。

最终的smali如下:

# virtual methods
.method public btn1ClickEvent(Landroid/view/View;)V
    .locals 4
    .param p1, "target"    # Landroid/view/View;

    .line 19
    iget v0, p0, Lcom/example/ybdesire/hello/MainActivity;->count:I

    add-int/lit8 v0, v0, 0x1

    iput v0, p0, Lcom/example/ybdesire/hello/MainActivity;->count:I

    .line 20
    const v0, 0x7f070052

    invoke-virtual {p0, v0}, Lcom/example/ybdesire/hello/MainActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/widget/TextView;

    .line 21
    .local v0, "txt":Landroid/widget/TextView;
    const-string v1, "Hello world! click by button!!"

    invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
    
    .line 22
    const-string v1, "btn_debug"

    new-instance v2, Ljava/lang/StringBuilder;

    invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V

    const-string v3, "count="

    invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    iget v3, p0, Lcom/example/ybdesire/hello/MainActivity;->count:I

    invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;

    invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v2

    invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    .line 23
    return-void
.end method

通过这样修改smali,我们最终得到的程序逻辑,用java表示如下:

    public void btn1ClickEvent(View target){
        count++;
        TextView txt=(TextView)findViewById(R.id.mytextview);//find output label by id
        txt.setText("Hello world! click by button!!");
        Log.d("btn_debug", "my count="+count);
    }

6. 改动后的smali打包为APK

使用如下命令(apktool d dir_name)来将改动后的smali,打包为APK文件

>apktool b hello-apk

打包完成后,可以在hello-apk/dist中看到打包出来的APK文件。

7. 为APK签名

apktool打包出来的APK,缺少签名信息,无法在android设备安装运行,必须要重新签名(详细步骤参考[1])。

  1. 生成签名文件

运行如下命令

keytool -genkey -alias abc.keystore -keyalg RSA -validity 20000 -keystore abc.keystore

需要输入签名的密码与其他信息,下面用中文说明具体要输入的内容(括号中是说明,不是真实输入的内容):

hello-apk\dist>keytool -genkey -alias abc.keystore -keyalg RSA -validity 20000 -keystore abc.keystore
Enter keystore password:(输入密码)
Re-enter new password:(再次输入密码)
What is your first and last name?
  [Unknown]:  a
What is the name of your organizational unit?
  [Unknown]:  a
What is the name of your organization?
  [Unknown]:  a
What is the name of your City or Locality?
  [Unknown]:  a
What is the name of your State or Province?
  [Unknown]:  a
What is the two-letter country code for this unit?
  [Unknown]:  a
Is CN=a, OU=a, O=a, L=a, ST=a, C=a correct?
  [no]:  yes

Enter key password for <abc.keystore>
        (RETURN if same as keystore password):(再次输入密码)
Re-enter new password:(再次输入密码)

Warning:
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore abc.keystore -destkeystore abc.keystore -deststoretype pkcs12".

最终会生成一个abc.keystore证书文件。

  1. 为apk签名

运行如下命令,对hello-apk.apk签名,签名后生成的文件为hello-apk-s.apk

jarsigner -verbose -keystore abc.keystore -signedjar hello-apk-s.apk hello-apk.apk abc.keystore

运行后,需要输入上面为签名设置的密码。

Enter Passphrase for keystore:
   adding: META-INF/MANIFEST.MF
   adding: META-INF/ABC_KEYS.SF
   adding: META-INF/ABC_KEYS.RSA
  signing: AndroidManifest.xml
  signing: classes.dex
  signing: res/anim/abc_fade_in.xml
  signing: res/anim/abc_fade_out.xml
  signing: res/anim/abc_grow_fade_in_from_bottom.xml
  signing: resources.arsc
>>> Signer
    X.509, CN=a, OU=a, O=a, L=a, ST=a, C=a
    [trusted certificate]

jar signed.

Warning:
The signer's certificate is self-signed.

最终我们得到签名后的hello-apk-s.apk文件。

8. 查看调试信息

签名后的hello-apk-s.apk文件,可以直接安装在android设备上。

运行APP,用adb查看log,如下命令可以只查看tag为btn_debug的debug级别的log

adb logcat btn_debug:D

多次点击APP的button,可以得到如下log

09-01 16:33:41.863  1700  1700 I Zygote  : Process 4887 exited due to signal (9)
09-01 16:33:42.057  3346  3346 D btn_debug: count=7
09-01 16:33:42.837  3346  3346 D btn_debug: count=8
09-01 16:33:42.993  3346  3346 D btn_debug: count=9
09-01 16:33:43.191  3346  3346 D btn_debug: count=10

这就实现了把APK中的局部变量count的值作为输出。

9. 总结

使用apktool,可以把APK解包,我们就能在解包后的smali中插入调试APK需要的代码,再将改动后的smali代码打包为APK,签名运行后,就能用adb看到我们插入smali代码的输出了。

10. 参考

  • [1] https://blog.csdn.net/ybdesire/article/details/52505648
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐