JVM、Dalvik、ART

  • JVM是Java虚拟机,运行Java字节码程序
  • Dalvik是Android专属的虚拟机,有专属的文件执行格式DEX(Dalvik executable)
  • ART(Android Runtime)是Dalvik的升级版

smali基础语法

smali是Dalvik的寄存器语言,smali代码是dex反编译得到的。

常见关键词

名称 注释
.class 类名
.super 父类名,继承的上级类名名称
.source 源名
.field 变量
.method 方法名
.register 寄存器
.end method 方法名的结束
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位 需要2个寄存器存储) 双浮点类型,返回数字
string String 文本类型,返回字符串
Lxxx/xxx/xxx object 对象类型,返回对象

常用指令

关键字 注释
const 重写整数属性,真假属性内容,只能是数字类型
const-string 重写字符串内容
const-wide 重写长整数类型,多用于修改到期时间。
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 全称greater equal(a>=b),a大于或等于则跳
if-le 全称little equal(a<=b),a小于或等于则跳
goto 强制跳到指定位置
switch 分支跳转,一般会有多个分支线,并根据指令跳转到适当位置
iget 获取寄存器数据

寄存器

在smali中所有的操作都必须经过寄存器来进行:

  • v开头数字结尾的寄存器为本地寄存器,如v0、v1、v2等
  • p开头数字结尾的寄存器为参数寄存器,如p0、p1、p2等,需要注意的是
    • 在非static函数中,p0代指的是this,p1才是第一个参数
    • 在static函数中,p0是第一个参数

smali代码分析

现在我们看到示例app的第二关,我们长按一键三连发现要求我们充值大会员,现在我们需要找到他的这个判断逻辑代码

打开jadx,选择apk文件打开,搜索“大会员”作为关键词,如果没有搜索到的话可能需要unicode进行编码再搜索

image-20250721202900779

跳转到对应的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static final boolean m36onCreate$lambda2(Ref.IntRef intRef, ChallengeSecond challengeSecond, ImageView imageView, ImageView imageView2, ImageView imageView3, View view) {
if (intRef.element < 10) {
Toast.makeText(challengeSecond, "请先获取10个硬币哦", 1).show();
}
if (challengeSecond.isvip()) {
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;
}

这是jadx反编译后给我们得到的代码,我们要去Smali部分找到对应lambda2函数,我这边找到的是如下代码,并在代码中,我将一行一行注释代码所代表的含义

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
93
94
95
96
97
98
99
100
101
102
103
104
105
//私有 静态 不可变的方法
.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 //寄存器数量为7

.line 33 //代码在33行
iget p0, p0, Lkotlin/jvm/internal/Ref$IntRef;->element:I //读取第一个参数p0中element的值赋值给p0
//iget p0, p0, Lkotlin/jvm/internal/Ref$IntRef;->element:I
// │ │ │ └─────────────────────────────────────────┘
// │ │ │ │
// 赋值指令 目标寄存器 源对象寄存器 字段引用(类名 + 字段名 + 类型)


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

const/16 v0, 0xa //给v0赋值10,十六进制a代表10

if-ge p0, v0, :cond_15 //判断p0的值是否大于或等于v0的值,如果为真跳转到:cond15

.line 34 //Toast弹窗代码
move-object p0, p1 //对象引用p1到p0,相当于Object obj1 = obj2;

check-cast p0, Landroid/content/Context; //验证p0是否为Context类型

// "请先获取10个硬币哦"
const-string v0, "\u8bf7\u5148\u83b7\u53d610\u4e2a\u786c\u5e01\u54e6" //弹窗文本信息,将""的内容赋值给v0

check-cast v0, Ljava/lang/CharSequence; //验证v0是否是CharSequence类型或它的子类

invoke-static {p0, v0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
//调用静态方法,p0是Context引用,v0是显示内容,p5是弹窗显示时间
//Landroid/widget/Toast;->makeText:Toast类的静态方法makeText的签名
//括号内是方法参数
//Landroid/widget/Toast:返回值为配置好的Toast实例

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

invoke-virtual {p0}, Landroid/widget/Toast;->show()V //调用Toast示例p0的show()方法无返回值

.line 36
:cond_15 //跳转到的一个地址
invoke-virtual {p1}, Lcom/zj/wuaipojie/ui/ChallengeSecond;->isvip()Z //调用ChallengeSecond示例p1的方法isvip()返回一个布尔值

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

if-eqz p0, :cond_43 //如果p0为0则跳转到:cond_43

.line 37 //也是一个Toast弹窗
check-cast p1, Landroid/content/Context;

// "当前已经是大会员了哦!"
const-string p0, "\u5f53\u524d\u5df2\u7ecf\u662f\u5927\u4f1a\u5458\u4e86\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

//下方38、39、40均为图片资源绑定(将一键三连图标点亮)
const p0, 0x7f0d0018

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

const p0, 0x7f0d0008

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

const p0, 0x7f0d000a

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

.line 41
sget-object p0, Lcom/zj/wuaipojie/util/SPUtils;->INSTANCE:Lcom/zj/wuaipojie/util/SPUtils; //获取静态单例对象SPUtils.INSTANCE存入p0

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 //SP的数据写入

goto :goto_50 //跳转到:goto_50

.line 44 //Toast弹窗
: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,p5为开头赋值的1
.end method

下面还有isvip()的方法smali代码,实际上这个方法固定返回为否,并不会判断是否为vip

1
2
3
4
5
6
7
8
# virtual methods
.method public final isvip()Z
.registers 2

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

return v0 //返回v0
.end method

smali代码修改

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

我们使用到np管理器,进入classes.dex的编辑,通过搜索功能快速定位到我们要修改的位置

修改判断

首先我们要修改硬币数量的判断逻辑,这里我们选择把if-ge大于等于修改为if-le小于等于

image-20250721215648861

接着我们要修改判断是否为vip部分的逻辑,我们直接将他的判断注释掉即可

image-20250721215830377

保存看一下效果

image-20250721215924059

修改寄存器的值

一样的,快速定位到需要修改的函数。我们这里首先要把v0的值赋值为0,则v0和p0相比则符合条件

image-20250721220152850

接着我们修改isvip()返回的值,跳转到isvip()函数,修改返回值

image-20250721220420749

修改完成后,保存测试

image-20250721220544023

抓取按钮id定位

使用开发者工具,定位到按钮,找到View Id(HEX)

image-20250721220839867

搜索选择整数,勾选十六进制即可查找到对应的功能点附近的位置

image-20250721221003040