1. 逆向分析目标 App 行为(比如 VIP 限制、广告、登录检测)
2. 使用 jadx 等工具找到对应 Java 函数(如 isVip())
3. 找到其对应的 smali 文件(class 路径)
4. 修改 smali 中该函数逻辑(比如直接返回 true)
方法一:根据界面/按钮/行为查找 Java 函数
场景举例 1:点击「登录」后触发某行为
操作:
先运行 App,点击「登录」按钮,看是否出现登录限制或跳转。
回 jadx,搜索关键词:
onClick
、LoginActivity
、checkLogin()
、isLogin()
等
找到类似代码:
if (!UserManager.getInstance().isLogin()) {
showLoginDialog();
return;
}
就定位到了核心判断点 isLogin()
,对应的类比如是 com/example/UserManager
。
方法二:搜索字符串(如登录提示、VIP提示)
场景举例 2:点击 VIP 内容出现“请先登录”或“升级会员”
在 jadx 中搜索提示文字,比如:
“请先登录”
“会员专享内容”
找到代码:
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
再往上回溯调用逻辑,通常能看到:
if (!UserManager.isLogin()) {
// 弹出 toast
}
确认 isLogin()
或 isVip()
这样的判断逻辑。
方法三:通过日志、抓包、frida 追踪函数调用
如果代码混淆严重或函数名乱码,可以:
启用 Logcat,查关键类名调用栈
用 frida hook 所有返回布尔值的函数,打印调用栈筛选出 false
来源
或者动态插桩 dump 所有类名
在 APKTool 解包后的目录中,搜索 smali 文件路径:
Java 类 com.example.UserManager
smali 路径对应为:smali/com/example/UserManager.smali
用 VSCode、Notepad++ 等打开,查找目标方法:
.method public isLogin()Z
1)修改布尔函数恒返回 true
原始代码:
.method public isLogin()Z # 定义一个 public 方法 isLogin,返回 boolean 类型(Z)
.locals 1 # 声明一个本地变量(v0)
iget-boolean v0, p0, Lcom/example/UserManager;->login:Z # 从 this(p0)中获取 login 字段(boolean),存入 v0
return v0 # 返回 v0 的值(即 login 字段)
.end method # 方法结束
修改后:
.method public isLogin()Z
.locals 1
const/4 v0, 0x1 # 返回 true
return v0
.end method
2)修改整型返回值(如等级、VIP 等级)
原始:
.method public getVipLevel()I
.locals 1
const/4 v0, 0x0
return v0
.end method
修改:
const/16 v0, 0x3 # 修改为 VIP 3
3)修改字符串返回值(用户名、接口 token)
原始:
.method public getUserName()Ljava/lang/String;
.locals 1
const-string v0, "guest"
return-object v0
.end method
修改:
const-string v0, "VIP_USER"
return-object v0
.end method
1)使用 APKTool 重打包:
apktool b yourapp -o yourapp_mod.apk
2)使用 apksigner 或 signapk 签名:
apksigner sign --ks mykey.jks --out signed.apk yourapp_mod.apk
功能场景 | 查找方法 | 修改方式 |
---|---|---|
登录限制 | 搜 isLogin / checkLogin |
恒返回 true:const/4 v0, 0x1 |
VIP 限制 | 搜 isVip / getVipLevel |
返回 true / 修改等级 |
广告判断 | 搜 shouldShowAd() |
改为 false:const/4 v0, 0x0 |
限制跳转 | 搜提示文字 / 界面类 | 修改 if 判断或 goto |
遇到混淆函数名:
方法 1:通过行为关键字(Toast、Dialog)反查调用路径
方法 2:通过动态调试(frida/logcat)确定调用点
方法 3:函数名看不懂也能直接修改返回值(哪怕叫 a()
也可以强改)
目标:让 App 把我们当作“已登录用户”,跳过登录弹窗、限制等。
第一步:定位登录判断逻辑
在 jadx 中搜索关键函数/字符串:
关键词:
isLogin()
、checkLogin()
、hasLogin()
、getUserToken()
"请先登录"、"您尚未登录"
找到类似 Java 代码:
if (!UserManager.getInstance().isLogin()) { // 如果用户未登录(isLogin 返回 false)
showLoginDialog(); // 弹出登录对话框
return; // 结束当前方法或逻辑(不再往下执行)
}
说明判断逻辑就是 isLogin()
→ 这个就是目标函数!
第二步:找到对应的 smali
比如类名是 com/example/UserManager
,那路径是:
smali/com/example/UserManager.smali
找到函数:
.method public isLogin()Z # 定义一个 public 方法 isLogin,返回类型为 boolean(Z)
.locals 1 # 声明一个局部变量(v0)
iget-boolean v0, p0, Lcom/example/UserManager;->login:Z # 从 this(p0)对象中获取 login 字段(boolean),赋值给 v0
return v0 # 返回 login 字段的值
.end method # 方法结束
第三步:修改返回值
直接改为返回 true
(登录状态):
.method public isLogin()Z
.locals 1
const/4 v0, 0x1
return v0
.end method
效果
所有使用 isLogin()
判断的地方都会当作“已登录”
登录弹窗不再出现
可以访问原本受限的页面
目标:阻止开屏广告、插屏广告、Banner 广告的显示或加载。
第一步:定位广告函数或类
搜索关键词:
AdManager
、SplashAd
、InterstitialAd
、loadAd()
、showAd()
“正在加载广告”、“广告加载失败”
例子 Java 代码:
SplashAd ad = new SplashAd(this); // 创建一个 SplashAd 实例,并传入当前上下文(this)
ad.loadAd(); // 加载广告资源
ad.show(); // 显示广告
第二步:找到 loadAd()
或 show()
的 smali
路径可能是:
smali/com/example/ads/SplashAd.smali
找到函数:
.method public loadAd()V # 定义一个 public 的 loadAd 方法,返回类型为 void(V)
.locals 1 # 声明一个局部变量(如 v0)
# 下面是实际逻辑代码,比如调用某些广告 SDK 的加载方法
invoke-virtual {p0}, Lcom/example/ads/AdSdk;->requestAd()V
# 调用 AdSdk 实例的 requestAd 方法加载广告(举例)
# 可能还有其他操作,比如设置回调、加载状态判断等
# ...
return-void # 方法无返回值,正常结束
.end method # 方法结束
第三步:屏蔽逻辑(清空函数体)
修改为:
.method public loadAd()V
.locals 0
return-void # 什么都不做
.end method
或者更暴力:
.method public show()V
.locals 0
return-void
.end method
效果
广告永远不会显示
有些 App 可能会卡住(建议配合修改广告加载结果返回)
目标:访问“仅限 VIP”的页面、功能、特权内容等。
第一步:定位 VIP 判断逻辑
搜关键词:
isVip()
、getVipLevel()
、hasAccess()
、isPremium()
“会员功能”、“您不是会员”、“升级会员享受特权”
Java 示例:
if (!User.isVip()) {
showVipDialog();
return;
}
第二步:smali 修改
定位到:
smali/com/example/User.smali
函数:
.method public isVip()Z # 定义一个 public 方法 isVip,返回 boolean 类型(Z)
.locals 1 # 声明一个局部变量 v0
iget-boolean v0, p0, Lcom/example/User;->vip:Z # 从当前对象(p0)中读取 boolean 类型字段 vip,存入 v0
return v0 # 返回 vip 字段的值(true 或 false)
.end method # 方法结束
改为:
.method public isVip()Z
.locals 1
const/4 v0, 0x1 # 返回 true
return v0
.end method
效果
所有受 isVip()
控制的功能都被解锁
页面、权限、按钮等不再被限制
补充场景:VIP 等级判断(int)
.method public getVipLevel()I
.locals 1
const/4 v0, 0x1
return v0
.end method
改成:
const/16 v0, 0x5 # 返回 VIP 5
目标:在 App 某函数调用时,修改传入参数值,使行为被改变(如访问管理员界面、提高充值金额、设置身份等级等)。
示例 1:修改传入的等级
原始 smali:
const/4 v1, 0x1 # 将常数 1 存入寄存器 v1(0x1 表示整数 1)
invoke-static {v1}, Lcom/example/UserHelper;->setUserLevel(I)V
# 调用 UserHelper 类的静态方法 setUserLevel(int),传入参数 v1(即 1)
改为:
const/16 v1, 0x5
invoke-static {v1}, Lcom/example/UserHelper;->setUserLevel(I)V
示例 2:修改调用参数为 VIP 标志
原始:
const-string v1, "normal"
invoke-static {v1}, Lcom/example/Access;->setUserType(Ljava/lang/String;)V
改为:
const-string v1, "vip"
invoke-static {v1}, Lcom/example/Access;->setUserType(Ljava/lang/String;)V
效果
即便你不是 VIP,App 认为你是
即便你传入金额为 1,也可以改为 100
可以突破函数参数级别的功能控制
场景 | 定位方法 | 修改位置 | 示例改动 |
---|---|---|---|
免登录 | 搜 isLogin() / "请先登录" |
Smali 返回 true | const/4 v0, 0x1 |
去广告 | 搜 loadAd() / showAd() |
清空广告函数 | return-void |
解锁 VIP | 搜 isVip() / getVipLevel() |
Smali 返回 true / int | const/4 v0, 0x1 / const/16 v0, 0x5 |
改参数 | 查看函数前参数设置 | 改参数值常量 | const/4 v1, 0x1 → const/4 v1, 0x5 |
追踪程序运行到哪个函数、哪个分支
查看函数参数和返回值
判断代码执行路径
辅助定位关键代码逻辑
Android 的日志打印一般使用:
android.util.Log
常用方法:
Log.d(String tag, String msg);
Log.i(String tag, String msg);
Log.e(String tag, String msg);
在 Smali 里调用就是调用这个类的静态方法。
Java 对应代码
Log.d("MyTag", "this is a log message");
对应的 Smali 代码片段
const-string v0, "MyTag" # 将字符串 "MyTag" 加载到寄存器 v0(作为日志标签)
const-string v1, "this is a log message" # 将字符串 "this is a log message" 加载到寄存器 v1(作为日志内容)
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
# 调用 Log.d(String tag, String msg) 输出 debug 级别日志
这里 invoke-static
调用 Log.d 静态函数,两个参数是 String
,返回值是 int(一般是日志打印的长度,通常忽略)。
1)找到你想插入日志的函数 smali 代码
比如:
.method public isLogin()Z # 定义一个 public 的实例方法 isLogin,返回值为 boolean (Z)
.locals 1 # 声明 1 个局部变量(v0)
iget-boolean v0, p0, Lcom/example/UserManager;->login:Z # 从当前对象 p0 中读取 boolean 字段 login,赋值给 v0
return v0 # 返回 v0,也就是 login 的值(true 或 false)
.end method # 方法定义结束
2)在关键位置插入日志打印指令
目标:打印登录状态值
.method public isLogin()Z
.locals 4 # 声明该方法最多会使用 4 个局部寄存器(v0、v1、v2、v3)
iget-boolean v0, p0, Lcom/example/UserManager;->login:Z
# 从当前对象(p0,相当于 this)中读取 boolean 类型的 login 字段,赋值给 v0
# 准备日志打印
const-string v1, "MyApp" # 将字符串 "MyApp" 加载到寄存器 v1,作为日志的 TAG
new-instance v2, Ljava/lang/StringBuilder;
# 在寄存器 v2 中创建一个新的 StringBuilder 对象(构造日志内容)
invoke-direct {v2}, Ljava/lang/StringBuilder;->()V
# 调用 StringBuilder 的构造函数初始化对象(new StringBuilder())
const-string v3, "isLogin() called, login="
# 将字符串常量加载到 v3,作为日志前缀部分
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
# 把 v3 的字符串追加到 v2 的 StringBuilder 中(相当于 sb.append("isLogin() called, login="))
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder;
# 把 boolean 值(v0)追加到 v2 的 StringBuilder 中(即 sb.append(true/false))
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
# 把 v2 中构造好的字符串转换成一个 String 对象(调用 toString())
move-result-object v3 # 上一条指令的返回结果(String)存入 v3(用于日志打印)
invoke-static {v1, v3}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
# 调用 Android 的 Log.d(tag, msg) 方法,打印 debug 日志内容
return v0 # 返回从对象中取出的 login 字段的值(true 或 false)
.end method # 方法定义结束
3)重点解释
.locals 4
:多声明几个寄存器用来存字符串拼接和日志打印
创建了一个 StringBuilder
,方便拼接打印字符串和变量
打印内容是 "isLogin() called, login=" + login状态
调用 Log.d("MyApp", "isLogin() called, login=true")
这条日志会显示在 logcat 里,方便你调试
.method public someFunction()V
.locals 2 # 声明该方法最多使用 2 个局部寄存器(v0 和 v1)
const-string v0, "MyApp" # 将字符串常量 "MyApp" 加载到寄存器 v0,作为日志的 TAG
const-string v1, "someFunction invoked" # 将日志内容字符串 "someFunction invoked" 加载到寄存器 v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
# 调用 Android Log 类的静态方法 Log.d(tag, msg)
# 即 Log.d("MyApp", "someFunction invoked")
# 会在 logcat 中打印一条调试日志
return-void # 方法返回,没有返回值(void 类型方法)
.end method # 方法定义结束
打印整型:
const-string v1, "value="
invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; # v0 是整型
打印字符串:
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
步骤 | 说明 |
---|---|
1. 定位函数 | 找到需要插日志的 smali 函数 |
2. 添加 .locals | 预留足够寄存器(一般多预留 2~4 个寄存器) |
3. 生成字符串 | 通过 StringBuilder 拼接变量和字符串 |
4. 调用 Log.d | 调用 Log.d(tag, msg) 方法打印日志 |
5. 保存重打包 | APKTool 重新打包并签名运行 |
Hook 点 = 插入你自己的代码的“关键位置”,用来拦截、修改、替代 App 原本的函数行为。
在 Smali 实战中,Hook 点是:
在目标函数 开始处、关键逻辑处、返回前 插入自定义代码
记录参数、修改参数、控制返回值
注入日志、调用自己函数、跳过判断、阻止原逻辑执行
类型 | 示例 | 实战目的 |
---|---|---|
入口 Hook | 函数一开始插入代码 | 日志、参数抓取、流量记录 |
条件前 Hook | if 判断之前插入 | 修改判断、伪造状态 |
调用前 Hook | 在 invoke-virtual 之前 |
修改调用参数 |
返回值 Hook | return 前插入 |
修改返回值 |
替换 Hook | 整个函数替换为你写的逻辑 | 强制重定义函数行为 |
示例函数:我们分析这个 Smali 函数(用户登录状态判断):
.method public isLogin()Z # 声明一个名为 isLogin 的公共方法,返回值为 boolean (Z)
.locals 1 # 声明该方法中使用了1个本地变量(v0)
iget-boolean v0, p0, Lcom/example/UserManager;->login:Z # 从当前对象 (p0) 中获取 login 字段(boolean类型),存入 v0
return v0 # 返回 v0,也就是 login 字段的值
.end method # 方法结束
1)函数入口 Hook 点(函数一开始插入日志)
目的:记录函数被调用
.method public isLogin()Z
.locals 3
const-string v1, "Hook"
const-string v2, "isLogin() 函数被调用"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
iget-boolean v0, p0, Lcom/example/UserManager;->login:Z
return v0
.end method
2)条件判断前插入 Hook(伪造判断条件)
原函数:
iget-boolean v0, p0, Lcom/example/UserManager;->isVip:Z # 从当前对象 (p0) 获取 boolean 类型字段 isVip 的值,存入 v0
if-eqz v0, :cond_false # 如果 v0 等于 0(false),跳转到标签 :cond_false
Hook 改写:强制是 VIP(修改 v0)
const/4 v0, 0x1 # 强制设置为 true
if-eqz v0, :cond_false
或者更暴力:直接跳过判断
goto :cond_true
3)函数调用前 Hook(修改参数)
原始调用:
const-string v1, "normal"
invoke-static {v1}, Lcom/example/AccessManager;->setUserType(Ljava/lang/String;)V
插入 Hook:
const-string v1, "vip"
invoke-static {v1}, Lcom/example/AccessManager;->setUserType(Ljava/lang/String;)V
参数就被你“劫持”了!
4)返回值前 Hook(强制返回 true)
原始:
iget-boolean v0, p0, Lcom/example/UserManager;->login:Z # 从当前对象 (p0) 获取 boolean 类型字段 login 的值,存入 v0
return v0 # 返回 v0,也就是 login 字段的值
Hook 改写:
const/4 v0, 0x1
return v0
或者可以插日志再返回:
const-string v1, "Hook"
const-string v2, "强制返回已登录"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
const/4 v0, 0x1
return v0
5)替换整个函数逻辑(自定义返回)
原始函数替换为:
.method public isLogin()Z
.locals 2
const-string v0, "Hook"
const-string v1, "isLogin() 被完全 Hook"
invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
const/4 v0, 0x1
return v0
.end method
可以把日志、参数处理提取到你自建的类和方法中:
添加你自己的类:
.class public Lcom/hook/MyHook; # 声明公共类 com.hook.MyHook
.super Ljava/lang/Object; # 继承自 java.lang.Object
.method public static onLoginCheck()Z # 声明一个公共静态方法 onLoginCheck,返回 boolean (Z)
.locals 1 # 声明使用 1 个本地寄存器 v0
const/4 v0, 0x1 # 给 v0 赋值 1(即 true)
return v0 # 返回 v0,表示登录检查通过,返回 true
.end method # 方法结束
然后在目标函数中替代原有逻辑:
invoke-static {}, Lcom/hook/MyHook;->onLoginCheck()Z # 调用静态方法 onLoginCheck,参数为空,返回 boolean
move-result v0 # 将返回结果移动到本地变量 v0
return v0 # 返回 v0 的值
这就是“全功能 Hook 点注入”,可以做到模块化、复用、可控。
Hook 点类型 | 用途 | 典型语句 |
---|---|---|
入口 | 打日志、打印参数 | Log.d(...) |
判断前 | 修改状态、强制跳转 | const/4 v0, 0x1 |
调用前 | 篡改参数 | const-string v1, "vip" |
返回前 | 修改返回值 | const/4 v0, 0x1 |
整体替换 | 自定义行为 | invoke-static {}, MyHook;->func()Z |
Smali 文件通常对应一个 Java 类,包含以下主要结构:
.class public Lcom/example/MyClass; # 声明公共类 com.example.MyClass
.super Ljava/lang/Object; # 继承自 java.lang.Object
# 字段定义(可选)
.field private myField:I # 声明一个私有整型字段 myField
# 构造函数
.method public constructor ()V # 声明公共构造方法 ,无参数,返回 void
.locals 0 # 声明本地寄存器数量为0
invoke-direct {p0}, Ljava/lang/Object;->()V # 调用父类构造函数
return-void # 构造函数返回
.end method
# 其他方法
.method public myMethod()V # 声明公共方法 myMethod,无参数,返回 void
.locals 1 # 声明本地寄存器数量为1
# 方法体 # 这里是方法体,可以写具体代码
return-void # 返回 void
.end method
假设你想添加一个新的辅助类 Lcom/hook/Helper;
,实现一个静态方法打印日志。
新类示范
.class public Lcom/hook/Helper; # 声明公共类 com.hook.Helper
.super Ljava/lang/Object; # 继承自 java.lang.Object
# 无字段
# 构造方法,必须写
.method public constructor ()V # 声明公共构造函数 ,无参数,返回 void
.locals 0 # 使用0个本地寄存器
invoke-direct {p0}, Ljava/lang/Object;->()V # 调用父类构造函数
return-void # 返回 void
.end method
# 新增静态方法,打印日志
.method public static log(Ljava/lang/String;Ljava/lang/String;)V # 声明公共静态方法 log,参数为两个字符串,返回 void
.locals 2 # 使用2个本地寄存器
# 调用 Log.d(tag, msg)
invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I # 调用 Log.d 方法,传入 tag 和 msg,返回 int(日志优先级)
# 丢弃返回值
const/4 v0, 0x0 # 将 v0 设为0,实际这里不影响返回值处理
return-void # 返回 void
.end method
假设你想在已有的 Lcom/example/MyClass;
类中添加一个方法 public int add(int a, int b)
示例方法
.method public add(II)I # 声明公共方法 add,接收两个 int 参数,返回 int
.locals 1 # 声明 1 个本地寄存器 v0
# 将两个参数相加,存放到 v0
add-int v0, p1, p2 # v0 = p1 + p2
# 返回结果
return v0 # 返回 v0
.end method
组成部分 | 说明 |
---|---|
.method 声明 |
public 、static 、返回类型、参数列表 |
.locals |
局部寄存器数量,至少足够满足方法中寄存器使用量 |
p0 、p1 等 |
方法参数寄存器,p0 通常是 this 对象 |
方法体指令 | smali 指令实现具体逻辑 |
.end method |
结束方法定义 |
如果新类没有显式构造函数,Android 运行时会报错。
示例构造函数(必须):
.method public constructor ()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;->()V
return-void
.end method
假设在 Lcom/example/MyClass;
里调用上面 Lcom/hook/Helper;
的 log
方法:
.method public testLog()V # 声明公共方法 testLog,无参数,返回 void
.locals 2 # 声明使用 2 个本地寄存器 v0 和 v1
const-string v0, "MyTag" # 将字符串 "MyTag" 赋值给 v0,作为日志标签
const-string v1, "Hello from smali method" # 将字符串 "Hello from smali method" 赋值给 v1,作为日志内容
invoke-static {v0, v1}, Lcom/hook/Helper;->log(Ljava/lang/String;Ljava/lang/String;)V # 调用 Helper.log 静态方法打印日志
return-void # 返回 void
.end method
可以添加如下新方法,强制返回登录成功:
.method public static isLogin()Z # 声明公共静态方法 isLogin,返回 boolean (Z)
.locals 1 # 声明使用 1 个本地寄存器 v0
const/4 v0, 0x1 # 将常量 1(true)赋值给 v0
return v0 # 返回 v0,表示登录状态为 true
.end method
然后在原登录校验函数中,调用这个方法替代原逻辑:
invoke-static {}, Lcom/hook/Helper;->isLogin()Z # 调用静态方法 isLogin,参数为空,返回 boolean
move-result v0 # 将返回值移动到本地变量 v0
return v0 # 返回 v0 的值
步骤 | 说明 |
---|---|
1. 新建 .smali 文件 |
以类的完整路径命名(包名+类名),存放对应文件夹 |
2. 定义 .class 和 .super |
指定类名和父类 |
3. 添加构造函数 | 构造函数必不可少,调用父类构造函数 |
4. 添加新方法 | 定义方法名、参数、返回类型,写指令实现 |
5. 保存并重编译 | 用 apktool 重新打包、签名并安装测试 |
if
判断、跳转逻辑(劫持执行流)Smali 是 Android Dalvik 字节码的汇编语言,条件判断和跳转控制程序流程的执行。
常见条件判断指令:
指令 | 说明 | 例子 |
---|---|---|
if-eqz vX, :label |
当 vX == 0 ,跳转到 label |
if-eqz v0, :cond_false |
if-nez vX, :label |
当 vX != 0 ,跳转到 label |
if-nez v1, :cond_true |
if-eq vX, vY, :label |
当 vX == vY ,跳转到 label |
if-eq v0, v1, :equal |
if-ne vX, vY, :label |
当 vX != vY ,跳转到 label |
if-ne v2, v3, :notequal |
if-lt , if-ge , if-gt , if-le |
其他比较大小的跳转指令 |
无条件跳转指令:
指令 | 说明 | 例子 |
---|---|---|
goto :label |
无条件跳转到指定标签 | goto :start |
劫持执行流就是通过修改判断和跳转,使程序执行你想要的分支,常用目标包括:
免登录:绕过登录状态判断。
去广告:跳过广告逻辑。
解锁 VIP 功能:绕过权限检测。
修改参数:改变函数传入参数,影响逻辑分支。
1)直接强制条件寄存器,改变判断结果
例子:
原代码:
iget-boolean v0, p0, Lcom/example/Auth;->isLoggedIn:Z # 从对象 p0 中获取 boolean 类型字段 isLoggedIn 的值,存入 v0
if-eqz v0, :not_logged_in # 如果 v0 == 0(false),跳转到标签 :not_logged_in
修改为:
const/4 v0, 0x1 # 强制 v0 = true
if-eqz v0, :not_logged_in # 条件永远不成立,不跳转
效果: 免登录,永远走登录成功分支。
2)修改跳转指令,直接无条件跳转
原代码:
if-eqz v0, :not_logged_in
# 登录成功逻辑
修改为:
goto :logged_in # 无条件跳转登录成功分支
效果: 忽略所有条件判断,直接执行目标分支。
3)反转条件判断,颠倒逻辑分支
原代码:
if-eqz v0, :cond_false
改为:
if-nez v0, :cond_false
效果: 条件判断反转,走原本不走的分支。
4)删除判断指令,保留期望的跳转分支
如果不需要判断,直接删掉 if
指令,或者替换为跳转到你想要的标签。
例子:免登录
原始代码:
iget-boolean v0, p0, Lcom/example/Auth;->isLoggedIn:Z
if-eqz v0, :login_failed
# 登录成功逻辑
修改方案1:
const/4 v0, 0x1
if-eqz v0, :login_failed # 这里永远不跳转
修改方案2:
goto :login_success # 直接无条件跳转到登录成功分支
例子:去广告
原始代码:
iget-boolean v1, p0, Lcom/example/AdManager;->showAd:Z
if-eqz v1, :no_ad
# 显示广告逻辑
修改为:
goto :no_ad # 直接跳过广告逻辑
反编译 APK,找到目标 Smali 文件。
定位包含 if 判断的函数,结合反编译的 Java 代码分析逻辑。
找到条件判断指令,一般是 if-eqz
, if-nez
, if-eq
, if-ne
等。
根据目的修改指令,选择:
改寄存器值(const/4 v0, 0x1
)让判断永远为真或假;
替换 if
指令为 goto
实现无条件跳转;
反转条件跳转指令;
删除 if
,插入 goto
。
修改后保存 smali 文件。
重新打包 APK 并签名安装测试。
跳转标签名不能写错,保持一致。
保证 .locals
数量足够,否则寄存器超限会报错。
注意保持指令语法正确,否则编译失败。
反复测试,避免死循环或程序崩溃。
多用日志打印辅助调试。
操作 | 指令修改示例 | 效果说明 |
---|---|---|
强制条件寄存器 | const/4 v0, 0x1 |
判断结果固定,绕过判断 |
无条件跳转 | goto :label |
无条件执行指定代码段 |
条件反转 | if-eqz -> if-nez 或反之 |
颠倒判断分支 |
删除判断 | 删除 if ,插入 goto |
直接跳转绕过判断逻辑 |