0%

安卓逆向的那些事第二集

前言:转轴拨弦三两声,未成曲调先有情,入了门但没有完全入门

课程目标

  1. 了解JVM,Dalvik,ART
  2. 初smali语法
  3. 实战修改smali

工具

  1. MT/NP管理器
  2. 雷电模拟器
  3. jadx-gui
  4. 核心破解

什么是JVM,Dalvik,ART

  • JVM是JAVA虚拟机,运行JAVA字节码程序
  • Dalvik是Google专门为Android设计的一个虚拟机,Dalvik有专属的文件执行格式dex(Dalvik executable)
  • ART(Android Runtime)相当于Dalvik的升级版,本质与Dalvilk无异

smali及其语法

smali是Dalvik的寄存器语言,smali代码是dex反编译而来的

关键字

名称 注释
.class 类名
.super 父类名,继承的上级类名名称
.source 源名
.field 变量
.method 方法名
.register 寄存器
.end register 方法名的结束
public 公有
protected 半公开,只有同一家人才能用
private 私有,只能自己使用
.parameter 方法参数
.prologue 方法开始
.line xxx 位于第xxx行

数据类型对应

smali类型 java类型 注释
V void 无返回值
Z boolean 布尔值类型,返回0或1
B byte 字节类型,返回字节
S short 短整数类型,返回数字
C char 字符类型,返回字符
I int 整数类型,返回数字
J long(64位,需要2个寄存器存储) 长整数类型,返回数字
F float 单浮点类型,返回数字
D double(64位,需要两个寄存器存储) 双浮点类型,返回数字
string String 文本类型,返回字符串
Lxxx/xxx/xxx object 对象类型,返回对象

常用指令

关键字 注释
const 重写整数类型,真假属性内容,只能是数字类型
const-string 重写字符串内容
return 返回指令
if-eq 全称equal(a=b),比较寄存器ab内容,相同则跳
if-ne 全称not equal(a!=b),ab内容不相同则跳
if-eqz 全称equal zero(a=0),z即是0的标记,a等于0则跳
if-nez 全称not equal zero(a!=0),a不等于0则跳
if-ge 全称garden equal(a>=b),a大于或等于则跳
if-le 全称little equal(a<=b),小于或等于则跳
goto 强制跳到指定位置
switch 分支跳转,一般会有多少个分支线,并根据指令跳转到适当位置
iget 获取寄存器数据
const-wide 重写长整型类型,多用于修改到期时间

其余指令可用语法工具查询

定位方法:所有弹窗关键字、抓取按钮id、

接下来我们用实战的方式讲解:

实战-VIP终结者

下载解压jadx-gui-1.4.4-with-jre-win压缩包,运行jadx-gui-1.4.4,选择打开文件,选择教程demo(更新).apk,反编译结果如下:

image-20241022210303300

之后安装压缩包里的核心破解。将该文件拖入到雷电模拟器下安装,出现如下提示:

image-20241022210511455

点击,选择启用模块勾选系统框架,然后重启

image-20241022210616409

此时安装文件夹里的教程demo(更新).apk跟上一章节里的吾爱破解是同一个。但由于两次的签名不一样,所以再安装就会失败,此时安装核心破解之后,发现原来的图标发生了变化:

image-20241022211355930

打开我们的第二关,这一关的要求是一键三连,当我们获取完硬币数量之后又要求我们长按,长按后的结果是要求我们充值大会员,暂时先记住关键词大会员。返回到之前的jadx-gui,选择工具栏中的搜索,输入大会员进行搜索:

image-20241022212157895

有时候我们可能会搜索不到,这有可能是因为在反编译的时候将中文转换为了unicode编码的形式,

我们点击左上角文件,选择首选项,取消勾选Unicode字符转义即可

image-20241023210938578

点击该关键字进去java代码

image-20241022212559143

这段java代码还是很好解读的,注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static final boolean m457onCreate$lambda2(Ref.IntRef intRef, ChallengeSecond challengeSecond, ImageView imageView, ImageView imageView2, ImageView imageView3, View view) {
if (intRef.element < 10) {
Toast.makeText(challengeSecond, "请先获取10个硬币哦", 1).show(); //这里是10个硬币的判断
}
if (challengeSecond.isvip()) { //这里是大会员的判断,如果是则执行下面语句,不是则跳转到else语句
ChallengeSecond challengeSecond2 = challengeSecond;
Toast.makeText(challengeSecond2, "当前已经是大会员了哦!", 1).show();
imageView.setImageResource(R.mipmap.zan_active);
imageView2.setImageResource(R.mipmap.coin_active);
imageView3.setImageResource(R.mipmap.collect_active);
SPUtils.INSTANCE.saveInt(challengeSecond2, "level", 2);
} else {
Toast.makeText(challengeSecond, "请先充值大会员哦!", 1).show();
}
return true;
}

在这串代码的一开始有一个反混淆的解释:

1
/* renamed from: onCreate$lambda-2 */

所以我们根据这个找到smali中的部分,转为smali语法来理解:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//一个私有、静态、不可变的方法   方法名
.method private static final onCreate$lambda-2(Lkotlin/jvm/internal/Ref$IntRef;Lcom/zj/wuaipojie/ui/ChallengeSecond;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/view/View;)Z //(这里面是方法的参数)这里是方法返回值类型,表示布尔值类型,返回假或真
.registers 7 //寄存器数量

.line 33 //代码所在的行数
iget p0, p0, Lkotlin/jvm/internal/Ref$IntRef;->element:I //读取p0(第一个参数,参考寄存器知识)中element的值赋值给p0

const/4 p5, 0x1 //p5赋值1

const/16 v0, 0xa //v0赋值10,在16进制里a表示10

if-ge p0, v0, :cond_15 //判断p0的值是否大于或等于v0的值(即p0的值是否大于或等于10),如果大于或等于则跳转到:cond_15

.line 34 //以下是常见的Toast弹窗代码
check-cast p1, Landroid/content/Context; //检查Context对象引用

const-string p0, "请先获取10个硬币哦" //弹窗文本信息,把""里的字符串数据赋值给p0

check-cast p0, Ljava/lang/CharSequence; //检查CharSequence对象引用

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
//将弹窗文本、显示时间等信息传给p1

move-result-object p0 //结果传递给p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V //当看到这个Toast;->show你就应该反应过来这里是弹窗代码

goto :goto_31 //跳转到:goto_31

:cond_15 //跳转的一个地址

invoke-virtual {p1}, Lcom/zj/wuaipojie/ui/ChallengeSecond;->isvip()Z //判断isvip方法的返回值是否为真(即结果是否为1)

move-result p0 //结果赋值给p0

if-eqz p0, :cond_43 //如果结果为0则跳转cond_43地址

const p0, 0x7f0d0018 //在arsc中的id索引,这个值可以进行查询

.line 37
invoke-virtual {p2, p0}, Landroid/widget/ImageView;->setImageResource(I)V //设置图片资源

const p0, 0x7f0d0008

.line 38
invoke-virtual {p3, p0}, Landroid/widget/ImageView;->setImageResource(I)V

const p0, 0x7f0d000a

.line 39
invoke-virtual {p4, p0}, Landroid/widget/ImageView;->setImageResource(I)V

.line 40
sget-object p0, Lcom/zj/wuaipojie/util/SPUtils;->INSTANCE:Lcom/zj/wuaipojie/util/SPUtils;

check-cast p1, Landroid/content/Context;

const/4 p2, 0x2 //p2赋值2

const-string p3, "level" //sp的索引

invoke-virtual {p0, p1, p3, p2}, Lcom/zj/wuaipojie/util/SPUtils;->saveInt(Landroid/content/Context;Ljava/lang/String;I)V //写入数据

goto :goto_50 //跳转地址

:cond_43

check-cast p1, Landroid/content/Context;

const-string p0, "\u8bf7\u5148\u5145\u503c\u5927\u4f1a\u5458\u54e6\uff01" //请先充值大会员哦!

check-cast p0, Ljava/lang/CharSequence;

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V

:goto_50
return p5 //返回p5的值
.end method //方法结束

//判断是否是大会员的方法
.method public final isvip()Z
.registers 2

const/4 v0, 0x0 //v0赋值0

return v0 //返回v0的值

.end method

这边补充一下smali语法中的寄存器:

在smali里的所有操作都必须经过寄存器来进行:本地寄存器用v开头数字结尾的符号来表示,如v0、v1、v2。参数寄存器则使用p开头数字结尾符号来表示,如p0、p1、p2。特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指”this”,p1表示函数的第一个参数,p2代表函数中的第二个参数。而在static函数中p0才对应第一个参数(因为java的static方法中没有this方法)

根据上面的讲述,我们就可以来分析一下代码:

查找到这句代码: if-ge p0, v0, :cond_15 //判断p0的值是否大于或等于v0的值(即p0的值是否大于或等于10),如果大于或等于则跳转到:cond_15用于判断硬币个数是否到了10个

之后会进行p0数值的判断,如果小于10,,就会出现一个弹窗:

1
2
3
4
5
6
7
8
9
10
11
12
check-cast p1, Landroid/content/Context; //检查Context对象引用

const-string p0, "请先获取10个硬币哦" //弹窗文本信息,把""里的字符串数据赋值给p0

check-cast p0, Ljava/lang/CharSequence; //检查CharSequence对象引用

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
//将弹窗文本、显示时间等信息传给p1

move-result-object p0 //结果传递给p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V //当看到这个Toast;->show你就应该反应过来这里是弹窗代码然后向下查找到这句代码:
1
2
3
4
5
6
:cond_15 //跳转的一个地址
invoke-virtual {p1}, Lcom/zj/wuaipojie/ui/ChallengeSecond;->isvip()Z //判断isvip方法的返回值是否为真(即结果是否为1)

move-result p0 //结果赋值给p0

if-eqz p0, :cond_43 //如果结果为0则跳转cond_43地址

这句代码判断是否充值为大会员,如果是大会员的话,则跳转到cond_43

如果不是大会员,根据这串代码:

1
2
3
4
5
:cond_43

check-cast p1, Landroid/content/Context;

const-string p0, "\u8bf7\u5148\u5145\u503c\u5927\u4f1a\u5458\u54e6\uff01" //请先充值大会员哦!

屏幕显示要求先充值大会员

事实上,在另一个方法中:这部分始终将v0的值赋值为0,其实表示用户不是大会员

1
2
3
4
5
6
.method public final isvip()Z
.registers 2
const/4 v0, 0x0 //v0赋值0

return v0 //返回v0的值
.end method

整个代码并没有充值大会员的入口,所以要完成一键三连的话,需要对isvip进行修改,那该怎么修改呢

修改方法:修改判断、强制跳转、修改寄存器的内容

image-20240719104116576

修改方法1-修改判断

我们在smali语法中找到第125行:

1
if-ge p0, v0, :cond_15 //如果p0的值不等于v0的值,跳转到cond_15

跳转到cond_15,所以我们往下寻找cond_15的入口点:

1
2
:cond_15
invoke-virtual {p1}, Lcom/zj/wuaipojie/ui/ChallengeSecond;->isvip()Z

这三段就是在判断p0的值与v0的值是否相等,之后我们再寻找,找到148行:

1
if-eqz p0, :cond_43

如果p0等于0 的话,跳转到conde_43:

1
2
3
4
5
6
7
8
9
10
11
12
:cond_43
check-cast p1, Landroid/content/Context;

const-string p0, "\u8bf7\u5148\u5145\u503c\u5927\u4f1a\u5458\u54e6\uff01"

check-cast p0, Ljava/lang/CharSequence;

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V

这里是提示要充值大会员,所以我们不能跳转到充值大会员,因为jdx不能直接修改代码,所以我们用MT管理器打开wuaipojie,打开其中的classes.dex文件

image-20241024192957050

选择Dex编辑器++的方式打开。在搜索中输入”硬币”,找到对应所在的代码,,向上寻找到

1
if-ge p0, v0, :cond_15

这个部分是对硬币数量的判断,if-ge指的是如果p0大于或等于v0则跳转,所以我们修改为小于或等于,用if-le指令:

image-20241024195121013

此时如果数量小于v0则直接跳转到cond_15,现在跳到下面147行,修改这部分的代码使其不会跳转:用注释的方式

image-20241024195402366

此时程序会执行输出 “当前已经是大会员了哦”,一直到186行的

1
goto :goto_50
1
2
3
	:goto_50
return p5
.end methon

自动结束这一方法

我们保存试试看此时的效果:

image-20241024195827992

不仅已经是大会员,同时一键三连图标全部点亮

修改方法2-修改寄存器的值

我们先删除原来的程序软件,恢复原来的备份,同样用dex编辑器++打开classes.dex文件,回到之前硬币数量的判断,上卖弄一行的代码其实是对v0的值赋值为10:

image-20241024200350096

那我们可以修改v0为0,使p0永远大于v0,跳过中间获取硬币

之后代码跳转到cond_15,选择isvip,长按,选择跳转,之后会跳转到isvip的方法中:

image-20241024200905904

通过分析发现,v0始终返回值为0,即假,所以我们只需要将0x0修改为0x1,这样v0的返回值就为真了,回到之前的代码部分:

1
2
3
4
5
invokevirtual {p1} ,Lcom/zj/wuaipojie/ui/ChallengeSecond;->isvip()Z

move-result p0

if-eqz p0, :cond_43

这里move-result p0是将上述代码的返回值给p0,也就是说修改完之后p0的值为1,不等于0,不会跳转到cond_43

修改保存完,运行软件,此时硬币数量是0,选择长按“一键三连”同样也能点亮图标和弹出已经是大会员