Post

安卓逆向破解

入坑安卓很久了,之前在酷安上一直接触各种破解软件,觉得非常有意思,正巧这次实训课在讲安卓逆向,索性记录了一些简单的逆向方法。

环境配置

  1. 安卓本地环境介绍

    由于我本人之前是做安卓开发的,并且对 XPosed 很熟悉,本次实验恰好涉及到软件逆向及跨签名安装,为了方便实验,我直接在我自己的手机进行实验了。手机已经 root 过并且装了 Magisk 和 LSPosed。下面仅给出最终截图,具体安装步骤可以在酷安找到。

  2. Magsik

    安卓 14 的系统需要安装 25.2 的版本,通过修补 boot.img 后在 recovery 中刷入即可。

    image-20240524153316957

  3. LSPosed

    LSPosed 是最新的 XPosed 管理器,拥有更高性能和更广的支持。首先在 Magisk 中刷入 LSPosed 模块并重启,之后安装 LSPosed 管理器。打开管理器即可激活 LSPosed:

    image-20240524153711451 image-20240524153755943

    可以看到现在已经激活了 Zygisk。LSPosed 能够直接在 Runtime 劫持 zygote 进程来动态的更改应用程序的功能。

  4. 安装核心破解

    为了避免在后续实验时因为签名校验不通过而反复卸载程序的情况,需要在 LSPosed 中安装核心破解,并将作用域设置为系统框架。核心破解(CorePatch)是一款基于Xposed 模块开发的工具,可以用来去除系统签名校验,直接安装修改过的未签名APK,禁用APK签名验证、覆盖安装不同签名应用等功能。

    image-20240524153926053 image-20240524153937500

电脑环境配置

  1. 安装反编译软件 JADX

    JADX 是一款用于反编译 Android 应用 APK 文件的强大工具,能将 DEX 文件转化为可读的 Java 源代码。它支持 APK 和 AAR 文件,并提供图形用户界面(GUI)和命令行接口,便于不同需求的用户使用。JADX 还能查看 Smali 代码,提取资源文件,并在Windows、macOS 和 Linux 等多个平台上运行。安装简单,无需复杂配置。使用 JADX可以有效地分析和理解应用程序的内部结构,但反编译的代码可能不完全可读,特别是经过混淆的代码。

    1
    
     brew install jadx
    
  2. 安装 Apktool

    Apktool 是一款开源工具,用于反编译和重新编译 Android 应用的 APK 文件。它可以将 APK 文件反编译成 Smali 代码(类似于汇编语言),并提取资源文件(如 XML、图片等),使开发者和逆向工程师能够修改应用的代码和资源。反编译后,可以对应用进行定制和修改,然后重新编译生成新的 APK 文件。Apktool 支持命令行操作,适用于 Windows、macOS 和 Linux 等多个平台。其主要用途包括调试、翻译、定制和安全分析。

    1
    
     brew install apktool
    

赛尔号破解

静态分析

  1. 解包

    使用 apktool 对 2seh.apk 进行解包,得到反汇编的源码

    image-20240524154938813

    进入到目录里然后 tree 一下,可以看到如下结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
     .
     ├── assets
     │   ├── CHANNEL
     │   ├── CMGC
     │   ├── SeerFightingUI
     │   ├── about
     │   ├── data
     │   ├── edfile
     │   ├── res
     │   ├── sound
     │   └── ui
     ├── lib
     │   └── armeabi
     ├── original
     │   └── META-INF
     ├── res
     │   ├── drawable
     │   ├── drawable-hdpi-v4
     │   ├── drawable-ldpi-v4
     │   ├── drawable-mdpi-v4
     │   ├── drawable-xhdpi-v4
     │   ├── layout
     │   ├── layout-v16
     │   ├── menu
     │   ├── raw
     │   └── values
     ├── smali
     │   ├── cn
     │   ├── com
     │   ├── com1010
     │   ├── com1111
     │   ├── com11111
     │   ├── com122
     │   ├── com222
     │   ├── com2222
     │   ├── com31
     │   ├── com44
     │   ├── com66
     │   ├── com77
     │   ├── com88
     │   ├── com99
     │   ├── org
     │   └── u
     └── unknown
         └── com
    

    其中 res/ 中存放了所有的资源文件,包括图标、布局等等;lib/ 存放所有 so 库;smali 中即为代码文件了,但是里面的代码并不可读,需要其他工具进行转换。

  2. 使用 JADX 进行反编译

    启动 JADX:

    1
    
     jadx-gui
    

    打开 2seh.apk:

    image-20240524155559436

    之后在侧栏中的 Source Code 中即可查看所有反编译的 java 代码了:

    image-20240524155711029

软件逆向破解

为了方便实验,后续的破解我直接在手机上的 mt 管理器中进行。

  1. 安装软件并赋予所有权限

    image-20240524150154681

    image-20240524145931055

  2. 软件分析

    启动游戏,打开购买界面:

    image-20240524160039711

    可以看到总共分为两种购买模式,我们点击第一个六块钱的,在支付栏跳出来后关掉购买界面,可以看到程序弹出了一个 toast 提示我们取消购买:

    image-20240524160109999

    OK,那么我们得到了软件的运行逻辑:

    1
    
     点击购买按钮 -> 购买的业务逻辑 -> 根据结果进行弹窗提示用户
    
  3. 源码分析

    在 mt 管理器中打开 apk 文件,点击查看:

    image-20240524160535321 image-20240524160552298

    可以看到已经解包得到了跟 apktool 类似的结构。这里我们点击 classes.dex(这个是所有的 java 代码经过 Android Studio 编译后得到的 dex 字节码)并用 Dex 编辑器 ++ 打开

    image-20240524160652874 image-20240524160748711

    刚刚我们已经得到了软件的业务逻辑,关键字是 “购买道具”,那么我们可以直接从根目录进行搜索:

    image-20240524160904160

    可以看到 “购买道具” 总共出现在了两个文件中,我们依次进行查看。首先打开 dsd,能够看到这个字符串,但是所有的代码都是 smali,我们在右上角进行反编译转成 java:

    image-20240524161224335

    image-20240524161242218

    做过安卓开发的人都比较熟悉这段代码,他是通过重写接口的 onResult() 函数来进行业务逻辑判断,然后根据支付结果向其父线程发送不同消息。但是有个问题,一般来说对于一个 Message msg 对象,应该填写 msg.whatmsg.obj 两个成员,分别用于记录消息的类型和内容,然后再通过 handler 进行发送,这里却没有填写 .what,这个绝对是有问题的。

    我们回到文件的开头,看一下这个类:

    image-20240524161817073

    他实现了一个叫做 IPayCallback 的接口,所以我们换到另一个出现了“购买道具”的文件,他是 IAPCallBack。还是按照上面的步骤将其反编译:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    
     package cn.seerFighting.cpp;
        
     import android.content.Context;
     import android.os.Message;
     import android.util.Log;
     import android.widget.Toast;
     import cn.cmgame.billing.api.GameInterface;
        
     public class IAPCallBack implements GameInterface.IPayCallback {
         AppActivity activity;
         IAPHandlerPay iapHandler;
         private volatile boolean isCallback = false;
        
         public IAPCallBack(AppActivity activity, IAPHandlerPay iapHandler) {
             this.activity = activity;
             this.iapHandler = iapHandler;
         }
        
         public void onResult(int resultCode, String billingIndex, Object obj) {
             String result;
             if (!this.isCallback) {
                 this.isCallback = true;
                 Log.i("result", billingIndex);
                 switch (resultCode) {
                     case 1:
                         result = "购买道具:[" 
                             + IAPUtilPay.getShopName(billingIndex) 
                             + "] 成功!";
                         Message successMsg = this.iapHandler.obtainMessage();
                         successMsg.what = 10005;
                         successMsg.obj = result;
                         this.iapHandler.sendMessage(successMsg);
                         break;
                     case 2:
                         result = "购买道具:[" 
                             + IAPUtilPay.getShopName(billingIndex) 
                             + "] 失败!";
                         Message failMsg = this.iapHandler.obtainMessage();
                         failMsg.what = 10006;
                         failMsg.obj = result;
                         this.iapHandler.sendMessage(failMsg);
                         break;
                     default:
                         result = "购买道具:[" 
                             + IAPUtilPay.getShopName(billingIndex) 
                             + "] 取消!";
                         Message cancelMsg = this.iapHandler.obtainMessage();
                         cancelMsg.what = 10006;
                         cancelMsg.obj = result;
                         this.iapHandler.sendMessage(cancelMsg);
                         break;
                 }
                 Toast.makeText(
                     (Context) this.activity, (CharSequence) result, 0
                 ).show();
             }
         }
     }
    

    其业务逻辑如下:

    • 根据支付结果设置提示信息:
      • 购买成功:msg.what = 10005
      • 购买失败:msg.what = 10006
      • 购买取消:msg.what = 10006
    • 向父线程发送消息
    • 弹出一个 toast 提示用户

    这样以来就很清晰了,我们得到的提示就是通过 53-55 行实现的,而我们触发的是 default

  4. 软件破解

    根据上述源码分析,我们的思路就非常清晰了:不管什么支付结果如何,全都让 handler 发送支付成功的消息。

    实现起来也十分简单,就是把所有的 .what 全都改成支付成功,也就是从 10006 改成 10005。

    我们回到 smali 代码中搜索一下 10006,发现一个都搜不到,我们知道在编译后系统很喜欢 16 进制,所以我们尝试换成他的 16 进制 0x2716,这次找到了:

    image-20240524162917142

    image-20240524163444452 image-20240524164218040

    我们只需要将这个值改成 10005 (0x2715) 即可。为了让结果更明显,我在这里还更改了支付取消的字符串,在前面加入了 “hooking” 字眼。然后保存并返回:

    image-20240524163550840 image-20240524163608048 image-20240524163915811

    可以看到 mt 管理器已经识别到字节码被更改,我们进行打包并重签名。签名是为了让文件能够被系统所认证。

    下面进行安装:

    image-20240524164025053

    由于我们没有赛尔号官方的签名,因此我当时使用的是我自己开发软件时用的签名,这个和官方是有差异的,在一个正常的系统中,不同签名但是相同包名的软件是无法直接覆盖安装的,需要将原始的软件卸载。但是由于我的设备是装过核心破解的,因此可以绕过签名验证直接覆盖。

破解验证

  1. 启动软件进入购买界面

    可以看到此时我们没有任何钻石

    image-20240524164516842

  2. 购买钻石

    我们点击购买 80 钻石,然后点击取消购买,可以看到程序弹出了一个 toast,里面的内容是 “hooking:购买道具:[钻石小包箱] 取消!” 这正是我修改过的字符串,同时在钻石栏也开始增加钻石,最终在 80 个时停止。

    image-20240524164614863

    image-20240524164743515

浅塘破解

软件分析

  1. 打开浅塘应用,选择一个皮肤:

    image-20240524165057616 image-20240524165159244

    跳到支付宝后我们取消支付,然后返回应用,可以看到出现了一个“购买失败”的弹窗。因此我们可以按照之前的思路去源码中进行寻找

源码分析

  1. Dex 分析

    我们还是按照之前的步骤,去 dex 里搜索“购买失败”,发现搜不到,那有没有可能是放在 string 里了?去 resource 中搜索发现也搜不到:

    image-20240524165941371 image-20240524170029098
  2. 支付分析

    我们现在搜不到任何结果,那只剩下一个入手点,就是软件的支付方式。我去搜了一下得到支付宝的异步调用返回值,在成功时是 9000。因此去 dex 中搜索 9000:

    image-20240524170531487

    在 com.ttzgame.pay 中发现了 smali 代码的赋值语句 const-string v1, "9000" 这应该就是要找的地方了。把相关代码反编译:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
     package com.ttzgame.pay;
        
     import android.text.TextUtils;
     import com.alipay.sdk.app.PayTask;
        
     class a$1 implements Runnable {
         final String a;
         final String b;
         final a c;
        
         a$1(a aVar, String str, String str2) {
             this.c = aVar;
             this.a = str;
             this.b = str2;
         }
        
         @Override
         public void run() {
             a aVar;
             String str;
             boolean z = true;
             String a = new c(new PayTask(this.c.d()).pay(this.a, true)).a();
             if (TextUtils.equals(a, "9000")) {
                 aVar = this.c;
                 str = this.b;
             } else {
                 if (TextUtils.equals(a, "8000")) {
                     return;
                 }
                 aVar = this.c;
                 str = this.b;
                 z = false;
             }
             aVar.a(str, z);
         }
     }
        
    

    整个类通过实现 Runnable 接口并重写 run() 方法来形成一个线程。可以看到当支付成功时,a == "9000"。 而失败时,则将 z 赋值为 false 来表示没有付款。所以我们可以通过修改 z 的值来进行破解。

  3. 软件破解

    结合 smali 代码和反编译出的 java,我们能够看到在 106 行的位置即为 z = false 的语句。将 0x0 改成 0x1。这样当支付失败的时候,程序也会认为已经付款成功了。

    image-20240524171535772

  4. 重新打包并签名

    image-20240524171722482 image-20240524171734576 image-20240524171752162

破解验证

  1. 打开付款界面

    选择 1280 个金币,点击支付

    image-20240524171840295

  2. 取消付款

    取消付款然后回到应用中,可以看到已经成功获得了 1280 个金币。

    image-20240524171944883

This post is licensed under CC BY 4.0 by the author.