当前位置: 首页 > news >正文

关于 smali:4. Smali 实战:修改行为 / 注入逻辑

一、如何定位目标函数并修改返回值

1.1 整体流程概览

1. 逆向分析目标 App 行为(比如 VIP 限制、广告、登录检测)
2. 使用 jadx 等工具找到对应 Java 函数(如 isVip())
3. 找到其对应的 smali 文件(class 路径)
4. 修改 smali 中该函数逻辑(比如直接返回 true)

1.2 第一步:如何准确定位目标函数

方法一:根据界面/按钮/行为查找 Java 函数

场景举例 1:点击「登录」后触发某行为

操作:

  • 先运行 App,点击「登录」按钮,看是否出现登录限制或跳转。

  • 回 jadx,搜索关键词:

    • onClickLoginActivitycheckLogin()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 广告的显示或加载。

第一步:定位广告函数或类

  • 搜索关键词:

    • AdManagerSplashAdInterstitialAdloadAd()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 返回 trueconst/4 v0, 0x1
去广告loadAd() / showAd()清空广告函数return-void
解锁 VIPisVip() / getVipLevel()Smali 返回 true / intconst/4 v0, 0x1 / const/16 v0, 0x5
改参数查看函数前参数设置改参数值常量const/4 v1, 0x1const/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函数一开始插入代码日志、参数抓取、流量记录
条件前 Hookif 判断之前插入修改判断、伪造状态
调用前 Hookinvoke-virtual 之前修改调用参数
返回值 Hookreturn 前插入修改返回值
替换 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 声明publicstatic、返回类型、参数列表
.locals局部寄存器数量,至少足够满足方法中寄存器使用量
p0p1方法参数寄存器,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, :labelvX == 0,跳转到 labelif-eqz v0, :cond_false
if-nez vX, :labelvX != 0,跳转到 labelif-nez v1, :cond_true
if-eq vX, vY, :labelvX == vY,跳转到 labelif-eq v0, v1, :equal
if-ne vX, vY, :labelvX != vY,跳转到 labelif-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 具体操作流程

  1. 反编译 APK,找到目标 Smali 文件。

  2. 定位包含 if 判断的函数,结合反编译的 Java 代码分析逻辑。

  3. 找到条件判断指令,一般是 if-eqz, if-nez, if-eq, if-ne 等。

  4. 根据目的修改指令,选择:

    • 改寄存器值(const/4 v0, 0x1)让判断永远为真或假;

    • 替换 if 指令为 goto 实现无条件跳转;

    • 反转条件跳转指令;

    • 删除 if,插入 goto

  5. 修改后保存 smali 文件

  6. 重新打包 APK 并签名安装测试。

6.6 注意事项

  • 跳转标签名不能写错,保持一致。

  • 保证 .locals 数量足够,否则寄存器超限会报错。

  • 注意保持指令语法正确,否则编译失败。

  • 反复测试,避免死循环或程序崩溃。

  • 多用日志打印辅助调试。

6.7 小结

操作指令修改示例效果说明
强制条件寄存器const/4 v0, 0x1判断结果固定,绕过判断
无条件跳转goto :label无条件执行指定代码段
条件反转if-eqz -> if-nez 或反之颠倒判断分支
删除判断删除 if,插入 goto直接跳转绕过判断逻辑

相关文章:

  • STM32中自动生成Flash地址的方法
  • 上传、下载功能 巧实现
  • 记录一次 apt-key curl导入失败的处理方式
  • 通过SAE实现企业应用的云上托管
  • Python中while 1和while True有何区别?深入解析无限循环的写法选择
  • C++11 中 final 和 override 从入门到精通
  • 什么时候应该使用 DDD?什么时候不适合?
  • 驶向智能未来:车载 MCP 服务与边缘计算驱动的驾驶数据交互新体验
  • 某寿险公司多分支设备监控实践:如何通过SAAS租用优化成本?
  • leetcode 1061. 按字典序排列最小的等效字符串 中等
  • 【芯片仿真中的X值:隐藏的陷阱与应对之道】
  • PHP 打印扩展开发:从易联云到小鹅通的多驱动集成实践
  • 山东大学深度学习2025年期末考试
  • 测试 FreeSWITCH 的 mod_loopback
  • nodejs里面的http模块介绍和使用
  • 斐波那契数列------矩阵幂法
  • C++自定义简单的内存池
  • 服务虚拟化HoverFly
  • 实验科学中策略的长期效应评估学习笔记
  • css实现文字颜色渐变
  • 单网页网站如何做/百度地图疫情实时动态
  • 做汽配批发做那个网站比较好/seo运营工作内容
  • 商用自适应网站建设/贵阳seo网站推广
  • 下载重庆人社app/百度关键词优化
  • 网站开发时什么时间适合创建视图/企业网站建设报价表
  • 做网站的是什么/新闻报道最新消息今天