前言
在我大一的时候遇到过安卓的一道题,该题的逻辑是检测到debug则输出flag,也就是说原本的考点是动态调试app,我当初就只用了动调的方法,而在与学长的交流中,学长直接修改了smali代码里的if判断条件使其未检测到debug则输出flag,使我对smali印象深刻
教程地址:《安卓逆向这档事》三、初识smali,vip终结者 - 吾爱破解 - 52pojie.cn
JVM、Dalvik与ART
- JVM是java虚拟机,运行java字节码
- Dalvik是Google专门为Android设计的虚拟机,执行.dex文件
- Art相当于升级版的Dalvik
smali语法
.dex文件反编译得到smali代码
我们修改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代码分析
我们来到教程demo的第二关

这里哪怕是获取到了硬币再长按一键三连也无法成功,下面小弹窗提示请先充值大会员
所以我们可以在jadx或者JEB中搜索这一串字符串,jadx左上角有一个搜索图标可以对文本进行搜索,JEB的话可以按Ctrl+f打开文本搜索框


我们双击就可以定位到代码所在的地方了
package com.zj.wuaipojie.ui;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import kotlin.Metadata;
import kotlin.jvm.internal.Ref.IntRef;
@Metadata(d1 = {"\u0000\u001E\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u000B\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001A\u00020\u0004J\u0012\u0010\u0005\u001A\u00020\u00062\b\u0010\u0007\u001A\u0004\u0018\u00010\bH\u0014¨\u0006\t"}, d2 = {"Lcom/zj/wuaipojie/ui/ChallengeSecond;", "Landroidx/appcompat/app/AppCompatActivity;", "()V", "isvip", "", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "app_release"}, k = 1, mv = {1, 7, 1}, xi = 0x30)
public final class ChallengeSecond extends AppCompatActivity {
public final boolean isvip() {
return false;
}
@Override // androidx.fragment.app.FragmentActivity
protected void onCreate(Bundle bundle0) {
super.onCreate(bundle0);
this.setContentView(0x7F0B001F); // layout:activity_challenge_second
IntRef ref$IntRef0 = new IntRef();
TextView textView0 = (TextView)this.findViewById(0x7F08007D); // id:coin_tv
Button button0 = (Button)this.findViewById(0x7F08007B); // id:coin_btn
Button button1 = (Button)this.findViewById(0x7F0801EB); // id:yjsl_btn
ImageView imageView0 = (ImageView)this.findViewById(0x7F0801EC); // id:zan_img
ImageView imageView1 = (ImageView)this.findViewById(0x7F08007C); // id:coin_img
ImageView imageView2 = (ImageView)this.findViewById(0x7F08007F); // id:collect_img
button0.setOnClickListener((View view0) -> {
++ref$IntRef0.element;
textView0.setText(((CharSequence)String.valueOf(ref$IntRef0.element)));
});
button1.setOnClickListener((View view0) -> Toast.makeText(((Context)this), "请长按完成一键三连", 1).show());
button1.setOnLongClickListener((View view0) -> {
if(ref$IntRef0.element < 10) {
Toast.makeText(((Context)this), "请先获取10个硬币哦", 1).show();
}
Toast.makeText(((Context)this), "请先充值大会员哦!", 1).show();
return true;
});
}
// Detected as a lambda impl.
private static final void onCreate$lambda-0(IntRef ref$IntRef0, TextView textView0, View view0) {
++ref$IntRef0.element;
textView0.setText(((CharSequence)String.valueOf(ref$IntRef0.element)));
}
// Detected as a lambda impl.
private static final void onCreate$lambda-1(ChallengeSecond challengeSecond0, View view0) {
Toast.makeText(((Context)challengeSecond0), "请长按完成一键三连", 1).show();
}
// Detected as a lambda impl.
private static final boolean onCreate$lambda-2(IntRef ref$IntRef0, ChallengeSecond challengeSecond0, ImageView imageView0, ImageView imageView1, ImageView imageView2, View view0) {
if(ref$IntRef0.element < 10) {
Toast.makeText(((Context)challengeSecond0), "请先获取10个硬币哦", 1).show();
}
Toast.makeText(((Context)challengeSecond0), "请先充值大会员哦!", 1).show();
return true;
}
}
我们切换到smali代码就能看见上面java代码中对应的一些方法

//一个私有、静态、不可变的方法 方法名
.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
copy一下这些注释,至于注释是如何得到的,我们可以去搜索这里面的smali代码在smali语法中对应的效果(有种在IDA中看汇编的感觉
smali代码修改
其实光静态看的话感觉JEB更直观一些
.method private static final onCreate$lambda-2(Ref$IntRef, ChallengeSecond, ImageView, ImageView, ImageView, View)Z
.registers 7
00000000 iget p0, p0, Ref$IntRef->element:I
00000004 const/4 p5, 1
00000006 const/16 v0, 10
0000000A if-ge p0, v0, :2A
:E
0000000E move-object p0, p1
00000010 check-cast p0, Context
00000014 const-string v0, "请先获取10个硬币哦"
00000018 check-cast v0, CharSequence
0000001C invoke-static Toast->makeText(Context, CharSequence, I)Toast, p0, v0, p5
00000022 move-result-object p0
00000024 invoke-virtual Toast->show()V, p0
:2A
0000002A invoke-virtual ChallengeSecond->isvip()Z, p1
00000030 move-result p0
00000032 if-eqz p0, :86
:36
00000036 check-cast p1, Context
0000003A const-string p0, "当前已经是大会员了哦!"
0000003E check-cast p0, CharSequence
00000042 invoke-static Toast->makeText(Context, CharSequence, I)Toast, p1, p0, p5
00000048 move-result-object p0
0000004A invoke-virtual Toast->show()V, p0
00000050 const p0, 0x7F0D0018 # mipmap:zan_active
00000056 invoke-virtual ImageView->setImageResource(I)V, p2, p0
0000005C const p0, 0x7F0D0008 # mipmap:coin_active
00000062 invoke-virtual ImageView->setImageResource(I)V, p3, p0
00000068 const p0, 0x7F0D000A # mipmap:collect_active
0000006E invoke-virtual ImageView->setImageResource(I)V, p4, p0
00000074 sget-object p0, SPUtils->INSTANCE:SPUtils
00000078 const/4 p2, 2
0000007A const-string p3, "level"
0000007E invoke-virtual SPUtils->saveInt(Context, String, I)V, p0, p1, p3, p2
00000084 goto :A0
:86
00000086 check-cast p1, Context
0000008A const-string p0, "请先充值大会员哦!"
0000008E check-cast p0, CharSequence
00000092 invoke-static Toast->makeText(Context, CharSequence, I)Toast, p1, p0, p5
00000098 move-result-object p0
0000009A invoke-virtual Toast->show()V, p0
:A0
000000A0 return p5
.end method
可以先修改这一行
0000000A if-ge p0, v0, :2A
改为
0000000A if-le p0, v0, :2A
使其直接进到2A也就是isvip()是否为大会员的判断,根据smali代码中的描述,
00000032 if-eqz p0, :86
这一行会使程序逻辑进到86也就是提示你充值大会员,所以我们可以把这一行注释掉,
让程序直接执行36也就是"当前已经是大会员了哦!"的逻辑,从而实现跳过
我们可以使用MT管理器找到对应的smali代码进行修改,这样似乎很方便

包括后续我们也可以将isvip的返回值改为1,这样直接能判断你是大会员,以及将之前比较的p0 v0改为相同的值实现跳转
也可以将当前已经是大会员的代码行加上:cond_42这种然后在上面跳转到cond_43的地方改为goto

以及将上面的cond_15直接改goto

都是没有问题的

