关于 smali:4. Smali 实战:修改行为 / 注入逻辑
一、如何定位目标函数并修改返回值
1.1 整体流程概览
1. 逆向分析目标 App 行为(比如 VIP 限制、广告、登录检测)
2. 使用 jadx 等工具找到对应 Java 函数(如 isVip())
3. 找到其对应的 smali 文件(class 路径)
4. 修改 smali 中该函数逻辑(比如直接返回 true)
1.2 第一步:如何准确定位目标函数
方法一:根据界面/按钮/行为查找 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 所有类名
1.3 第二步:找到对应的 Smali 文件
在 APKTool 解包后的目录中,搜索 smali 文件路径:
-
Java 类
com.example.UserManager
-
smali 路径对应为:
smali/com/example/UserManager.smali
用 VSCode、Notepad++ 等打开,查找目标方法:
.method public isLogin()Z
1.4 第三步:修改 smali 返回值为你想要的结果
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),存入 v0return v0 # 返回 v0 的值(即 login 字段).end method # 方法结束
修改后:
.method public isLogin()Z.locals 1const/4 v0, 0x1 # 返回 truereturn v0
.end method
2)修改整型返回值(如等级、VIP 等级)
原始:
.method public getVipLevel()I.locals 1const/4 v0, 0x0return v0
.end method
修改:
const/16 v0, 0x3 # 修改为 VIP 3
3)修改字符串返回值(用户名、接口 token)
原始:
.method public getUserName()Ljava/lang/String;.locals 1const-string v0, "guest"return-object v0
.end method
修改:
const-string v0, "VIP_USER"return-object v0
.end method
1.5 修改后怎么重新打包与签名?
1)使用 APKTool 重打包:
apktool b yourapp -o yourapp_mod.apk
2)使用 apksigner 或 signapk 签名:
apksigner sign --ks mykey.jks --out signed.apk yourapp_mod.apk
1.6 实际案例
功能场景 | 查找方法 | 修改方式 |
---|---|---|
登录限制 | 搜 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()
也可以强改)
二、例子
2.1 免登录(跳过登录判断)
目标:让 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),赋值给 v0return v0 # 返回 login 字段的值.end method # 方法结束
第三步:修改返回值
直接改为返回 true
(登录状态):
.method public isLogin()Z.locals 1const/4 v0, 0x1return v0
.end method
效果
-
所有使用
isLogin()
判断的地方都会当作“已登录” -
登录弹窗不再出现
-
可以访问原本受限的页面
2.2 去广告(阻止广告显示)
目标:阻止开屏广告、插屏广告、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 0return-void # 什么都不做
.end method
或者更暴力:
.method public show()V.locals 0return-void
.end method
效果
-
广告永远不会显示
-
有些 App 可能会卡住(建议配合修改广告加载结果返回)
2.3 解锁 VIP 功能(跳过付费限制)
目标:访问“仅限 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 # 声明一个局部变量 v0iget-boolean v0, p0, Lcom/example/User;->vip:Z # 从当前对象(p0)中读取 boolean 类型字段 vip,存入 v0return v0 # 返回 vip 字段的值(true 或 false).end method # 方法结束
改为:
.method public isVip()Z.locals 1const/4 v0, 0x1 # 返回 truereturn v0
.end method
效果
-
所有受
isVip()
控制的功能都被解锁 -
页面、权限、按钮等不再被限制
补充场景:VIP 等级判断(int)
.method public getVipLevel()I.locals 1const/4 v0, 0x1return v0
.end method
改成:
const/16 v0, 0x5 # 返回 VIP 5
2.4 修改函数参数(伪造用户等级、身份、功能参数)
目标:在 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, 0x5invoke-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
-
可以突破函数参数级别的功能控制
2.5 小结
场景 | 定位方法 | 修改位置 | 示例改动 |
---|---|---|---|
免登录 | 搜 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 |
三、插入日志打印
3.1 为什么插日志?
-
追踪程序运行到哪个函数、哪个分支
-
查看函数参数和返回值
-
判断代码执行路径
-
辅助定位关键代码逻辑
3.2 Android Smali 日志打印基本原理
Android 的日志打印一般使用:
android.util.Log
常用方法:
Log.d(String tag, String msg);
Log.i(String tag, String msg);
Log.e(String tag, String msg);
在 Smali 里调用就是调用这个类的静态方法。
3.3 Smali 中调用 Log.d 的基本语法示例
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(一般是日志打印的长度,通常忽略)。
3.4 完整插入日志打印步骤
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,赋值给 v0return 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,作为日志的 TAGnew-instance v2, Ljava/lang/StringBuilder;# 在寄存器 v2 中创建一个新的 StringBuilder 对象(构造日志内容)invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()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 里,方便你调试
3.5 直接打印固定字符串
.method public someFunction()V.locals 2 # 声明该方法最多使用 2 个局部寄存器(v0 和 v1)const-string v0, "MyApp" # 将字符串常量 "MyApp" 加载到寄存器 v0,作为日志的 TAGconst-string v1, "someFunction invoked" # 将日志内容字符串 "someFunction invoked" 加载到寄存器 v1invoke-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 # 方法定义结束
3.6 打印其他类型变量
打印整型:
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;
3.7 小结
步骤 | 说明 |
---|---|
1. 定位函数 | 找到需要插日志的 smali 函数 |
2. 添加 .locals | 预留足够寄存器(一般多预留 2~4 个寄存器) |
3. 生成字符串 | 通过 StringBuilder 拼接变量和字符串 |
4. 调用 Log.d | 调用 Log.d(tag, msg) 方法打印日志 |
5. 保存重打包 | APKTool 重新打包并签名运行 |
四、函数 hook 点
4.1 什么是函数 Hook 点?
Hook 点 = 插入你自己的代码的“关键位置”,用来拦截、修改、替代 App 原本的函数行为。
在 Smali 实战中,Hook 点是:
-
在目标函数 开始处、关键逻辑处、返回前 插入自定义代码
-
记录参数、修改参数、控制返回值
-
注入日志、调用自己函数、跳过判断、阻止原逻辑执行
4.2 Hook 点常见场景
类型 | 示例 | 实战目的 |
---|---|---|
入口 Hook | 函数一开始插入代码 | 日志、参数抓取、流量记录 |
条件前 Hook | if 判断之前插入 | 修改判断、伪造状态 |
调用前 Hook | 在 invoke-virtual 之前 | 修改调用参数 |
返回值 Hook | return 前插入 | 修改返回值 |
替换 Hook | 整个函数替换为你写的逻辑 | 强制重定义函数行为 |
4.3 详细示范:5 种 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类型),存入 v0return v0 # 返回 v0,也就是 login 字段的值
.end method # 方法结束
1)函数入口 Hook 点(函数一开始插入日志)
目的:记录函数被调用
.method public isLogin()Z.locals 3const-string v1, "Hook"const-string v2, "isLogin() 函数被调用"invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)Iiget-boolean v0, p0, Lcom/example/UserManager;->login:Zreturn 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;)Iconst/4 v0, 0x1
return v0
5)替换整个函数逻辑(自定义返回)
原始函数替换为:
.method public isLogin()Z.locals 2const-string v0, "Hook"const-string v1, "isLogin() 被完全 Hook"invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)Iconst/4 v0, 0x1return v0
.end method
4.4 调用自定义函数当 Hook 函数
可以把日志、参数处理提取到你自建的类和方法中:
添加你自己的类:
.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 个本地寄存器 v0const/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 点注入”,可以做到模块化、复用、可控。
4.5 小结
Hook 点类型 | 用途 | 典型语句 |
---|---|---|
入口 | 打日志、打印参数 | Log.d(...) |
判断前 | 修改状态、强制跳转 | const/4 v0, 0x1 |
调用前 | 篡改参数 | const-string v1, "vip" |
返回前 | 修改返回值 | const/4 v0, 0x1 |
整体替换 | 自定义行为 | invoke-static {}, MyHook;->func()Z |
五、手动添加 smali 方法与类
5.1 Smali 类结构基础
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 <init>()V # 声明公共构造方法 <init>,无参数,返回 void.locals 0 # 声明本地寄存器数量为0invoke-direct {p0}, Ljava/lang/Object;-><init>()V # 调用父类构造函数return-void # 构造函数返回
.end method# 其他方法
.method public myMethod()V # 声明公共方法 myMethod,无参数,返回 void.locals 1 # 声明本地寄存器数量为1# 方法体 # 这里是方法体,可以写具体代码return-void # 返回 void
.end method
5.2 手动添加一个新类
假设你想添加一个新的辅助类 Lcom/hook/Helper;
,实现一个静态方法打印日志。
新类示范
.class public Lcom/hook/Helper; # 声明公共类 com.hook.Helper
.super Ljava/lang/Object; # 继承自 java.lang.Object# 无字段# 构造方法,必须写
.method public constructor <init>()V # 声明公共构造函数 <init>,无参数,返回 void.locals 0 # 使用0个本地寄存器invoke-direct {p0}, Ljava/lang/Object;-><init>()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
5.3 手动添加一个新方法到已有类
假设你想在已有的 Lcom/example/MyClass;
类中添加一个方法 public int add(int a, int b)
示例方法
.method public add(II)I # 声明公共方法 add,接收两个 int 参数,返回 int.locals 1 # 声明 1 个本地寄存器 v0# 将两个参数相加,存放到 v0add-int v0, p1, p2 # v0 = p1 + p2# 返回结果return v0 # 返回 v0
.end method
5.4 说明关键点
组成部分 | 说明 |
---|---|
.method 声明 | public 、static 、返回类型、参数列表 |
.locals | 局部寄存器数量,至少足够满足方法中寄存器使用量 |
p0 、p1 等 | 方法参数寄存器,p0 通常是 this 对象 |
方法体指令 | smali 指令实现具体逻辑 |
.end method | 结束方法定义 |
5.5 构造函数的添加
如果新类没有显式构造函数,Android 运行时会报错。
示例构造函数(必须):
.method public constructor <init>()V.locals 0invoke-direct {p0}, Ljava/lang/Object;-><init>()Vreturn-void
.end method
5.6 调用自己新写的方法
假设在 Lcom/example/MyClass;
里调用上面 Lcom/hook/Helper;
的 log
方法:
.method public testLog()V # 声明公共方法 testLog,无参数,返回 void.locals 2 # 声明使用 2 个本地寄存器 v0 和 v1const-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
5.7 实际应用示例:添加免登录方法
可以添加如下新方法,强制返回登录成功:
.method public static isLogin()Z # 声明公共静态方法 isLogin,返回 boolean (Z).locals 1 # 声明使用 1 个本地寄存器 v0const/4 v0, 0x1 # 将常量 1(true)赋值给 v0return v0 # 返回 v0,表示登录状态为 true
.end method
然后在原登录校验函数中,调用这个方法替代原逻辑:
invoke-static {}, Lcom/hook/Helper;->isLogin()Z # 调用静态方法 isLogin,参数为空,返回 boolean
move-result v0 # 将返回值移动到本地变量 v0
return v0 # 返回 v0 的值
5.8 小结
步骤 | 说明 |
---|---|
1. 新建 .smali 文件 | 以类的完整路径命名(包名+类名),存放对应文件夹 |
2. 定义 .class 和 .super | 指定类名和父类 |
3. 添加构造函数 | 构造函数必不可少,调用父类构造函数 |
4. 添加新方法 | 定义方法名、参数、返回类型,写指令实现 |
5. 保存并重编译 | 用 apktool 重新打包、签名并安装测试 |
六、修改 smali 中的 if
判断、跳转逻辑(劫持执行流)
6.1 Smali 中条件判断与跳转基础
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 |
6.2 劫持执行流的目的和思路
劫持执行流就是通过修改判断和跳转,使程序执行你想要的分支,常用目标包括:
-
免登录:绕过登录状态判断。
-
去广告:跳过广告逻辑。
-
解锁 VIP 功能:绕过权限检测。
-
修改参数:改变函数传入参数,影响逻辑分支。
6.3 修改 if 判断和跳转的常用技巧
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
指令,或者替换为跳转到你想要的标签。
6.4 实战示例讲解
例子:免登录
原始代码:
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 # 直接跳过广告逻辑
6.5 具体操作流程
-
反编译 APK,找到目标 Smali 文件。
-
定位包含 if 判断的函数,结合反编译的 Java 代码分析逻辑。
-
找到条件判断指令,一般是
if-eqz
,if-nez
,if-eq
,if-ne
等。 -
根据目的修改指令,选择:
-
改寄存器值(
const/4 v0, 0x1
)让判断永远为真或假; -
替换
if
指令为goto
实现无条件跳转; -
反转条件跳转指令;
-
删除
if
,插入goto
。
-
-
修改后保存 smali 文件。
-
重新打包 APK 并签名安装测试。
6.6 注意事项
-
跳转标签名不能写错,保持一致。
-
保证
.locals
数量足够,否则寄存器超限会报错。 -
注意保持指令语法正确,否则编译失败。
-
反复测试,避免死循环或程序崩溃。
-
多用日志打印辅助调试。
6.7 小结
操作 | 指令修改示例 | 效果说明 |
---|---|---|
强制条件寄存器 | const/4 v0, 0x1 | 判断结果固定,绕过判断 |
无条件跳转 | goto :label | 无条件执行指定代码段 |
条件反转 | if-eqz -> if-nez 或反之 | 颠倒判断分支 |
删除判断 | 删除 if ,插入 goto | 直接跳转绕过判断逻辑 |