365bet官网中文网-365网站靠谱不-28365365体育在线

Est. 1980 · 每日复古新闻

安卓逆向破解

安卓逆向破解

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

环境配置安卓本地环境介绍

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

Magsik

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

LSPosed

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

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

安装核心破解

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

电脑环境配置安装反编译软件 JADX

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

1

brew install jadx

安装 Apktool

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

1

brew install apktool

赛尔号破解静态分析解包

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

进入到目录里然后 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 中即为代码文件了,但是里面的代码并不可读,需要其他工具进行转换。

使用 JADX 进行反编译

启动 JADX:

1

jadx-gui

打开 2seh.apk:

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

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

安装软件并赋予所有权限

软件分析

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

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

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

1

点击购买按钮 -> 购买的业务逻辑 -> 根据结果进行弹窗提示用户

源码分析

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

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

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

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

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

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

他实现了一个叫做 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。

软件破解

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

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

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

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

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

下面进行安装:

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

破解验证启动软件进入购买界面

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

购买钻石

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

浅塘破解软件分析打开浅塘应用,选择一个皮肤:

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

源码分析Dex 分析

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

支付分析

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

在 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 的值来进行破解。

软件破解

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

重新打包并签名

破解验证打开付款界面

选择 1280 个金币,点击支付

取消付款

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

相关文章