文章目录
⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。
1 基础概念
前面两篇文章我们只对 Frida 的概念粗略地了解了下,本章我们需要正式进行了解。
1.1 什么是 Frida
Frida是一款跨平台的动态插桩工具(可以简单理解为“运行时修改程序行为的工具”),支持Android、iOS、Windows等多个系统。它的核心优势是不需要修改程序源码,也不需要重新编译APP,就能在程序运行过程中“插入”我们自己的代码,实现对程序行为的监控、修改甚至替换。
对于Android 来说,Frida是入门级的利器——它允许我们用JavaScript(或Python、Swift等)编写脚本,直接操作Android 应用的Java层代码,非常适合调试和破解简单的验证逻辑。
1.2 什么是 Hook
“Hook”中文常译为“钩子”,形象地说就是在程序的某个“关键点”(比如一个函数、一个按钮点击事件)上“挂”一个自己的逻辑。当程序执行到这个关键点时,会先触发我们挂的逻辑,从而实现:
- 监控:查看函数的输入参数、返回值;
- 修改:改变函数的参数或返回结果;
- 替换:用自己的逻辑完全替代原函数的功能。
在本教程中,我们就是通过Hook技术,修改APP中“验证用户输入是否正确”的逻辑,让任意输入都能通过验证。
1.3 为什么要学Java层Hook
Android应用的核心逻辑(比如登录验证、数据处理)大多写在Java层(通过Java或Kotlin编写)。掌握Java层Hook,就能直接对这些核心逻辑“动手脚”,是逆向分析Android应用的基础技能。
接下来,我们通过一个具体案例,带你一步步实操Java层Hook的全过程。
2 案例实操
本章示例应用的链接:
https://pan.baidu.com/s/16EE2XE-OZS_xBRPlWUODbw?pwd=n2vb
提取码: n2vb
使用APK:Challenge 0x1.apk
2.1 代码分析
模拟器安装 Challenge 0x1.apk,打开应用后输入任意数字后点击 submit 按钮,提示不通过。

我们此时使用 jadx-gui 反编译该 apk ,查看核心代码 MainActivity,我们在写 hook 脚本前,先要大致分析下核心代码逻辑。
package com.ad2001.frida0x1;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Random;
/* loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {
TextView t1;
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
final EditText editText = (EditText) findViewById(R.id.editTextTextPassword);
Button button = (Button) findViewById(R.id.button);
this.t1 = (TextView) findViewById(R.id.textview1);
final int i = get_random();
button.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.frida0x1.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
String string = editText.getText().toString();
if (TextUtils.isDigitsOnly(string)) {
MainActivity.this.check(i, Integer.parseInt(string));
} else {
Toast.makeText(MainActivity.this.getApplicationContext(), "Enter a valid number !!", 1).show();
}
}
});
}
int get_random() {
return new Random().nextInt(100);
}
/* JADX WARN: Removed duplicated region for block: B:13:0x0031 A[PHI: r0
0x0031: PHI (r0v7 char) = (r0v5 char), (r0v11 char) binds: [B:19:0x0040, B:12:0x002f] A[DONT_GENERATE, DONT_INLINE]] */
/*
Code decompiled incorrectly, please refer to instructions dump.
To view partially-correct code enable 'Show inconsistent code' option in preferences
*/
void check(int r4, int r5) {
/*
r3 = this;
int r4 = r4 * 2
int r4 = r4 + 4
r0 = 1
if (r4 != r5) goto L53
android.content.Context r4 = r3.getApplicationContext()
java.lang.String r5 = "Yey you guessed it right"
android.widget.Toast r4 = android.widget.Toast.makeText(r4, r5, r0)
r4.show()
java.lang.StringBuilder r4 = new java.lang.StringBuilder
r4.<init>()
r5 = 0
L1a:
r0 = 20
if (r5 >= r0) goto L49
java.lang.String r0 = "AMDYV{WVWT_CJJF_0s1}"
char r0 = r0.charAt(r5)
r1 = 97
if (r0 < r1) goto L35
r2 = 122(0x7a, float:1.71E-43)
if (r0 > r2) goto L35
int r0 = r0 + (-21)
char r0 = (char) r0
if (r0 >= r1) goto L43
L31:
int r0 = r0 + 26
char r0 = (char) r0
goto L43
L35:
r1 = 65
if (r0 < r1) goto L43
r2 = 90
if (r0 > r2) goto L43
int r0 = r0 + (-21)
char r0 = (char) r0
if (r0 >= r1) goto L43
goto L31
L43:
r4.append(r0)
int r5 = r5 + 1
goto L1a
L49:
android.widget.TextView r5 = r3.t1
java.lang.String r4 = r4.toString()
r5.setText(r4)
goto L60
L53:
android.content.Context r4 = r3.getApplicationContext()
java.lang.String r5 = "Try again"
android.widget.Toast r4 = android.widget.Toast.makeText(r4, r5, r0)
r4.show()
L60:
return
*/
throw new UnsupportedOperationException("Method not decompiled: com.ad2001.frida0x1.MainActivity.check(int, int):void");
}
}
这段代码是该应用的主活动类(MainActivity),功能是一个简单的猜数字游戏,猜对后会展示一个经过字符转换的字符串。以下是详细分析:
1. 类结构与核心组件
- 继承自
AppCompatActivity,是Android应用的主界面活动。 - 核心UI组件:
EditText:用于用户输入数字。Button:触发猜数字的检查逻辑。TextView (t1):用于展示猜对后的结果字符串。
2. 核心方法解析
(1)onCreate方法
初始化界面布局,绑定UI组件(输入框、按钮、文本框)。
生成一个随机数(通过get_random()方法),并为按钮设置点击事件:
- 点击时获取用户输入的字符串,检查是否为纯数字。
- 若为数字,调用
check方法,传入随机数和用户输入的数字;否则提示“Enter a valid number !!”。
(2)get_random方法
功能:生成一个0~99之间的随机整数(通过Random().nextInt(100)实现)。
(3)check方法(核心逻辑)
作用:验证用户输入的数字是否正确,并在正确时解密展示字符串。
逻辑拆解:
校验逻辑是从check方法的反编译代码中推导出来的,核心是分析方法内对参数的处理和条件判断逻辑。具体步骤如下:
(1)反编译代码中,check方法的定义是void check(int r4, int r5),其中:
r4:是传入的第一个参数,结合上下文可知,它是get_random()生成的随机数(在onCreate中调用check(i, ...),i就是get_random()的返回值)。r5:是传入的第二个参数,即用户输入的数字(Integer.parseInt(string)的结果)。
(2)分析check方法内对r4的处理
反编译的伪代码中,明确提到对r4的运算:
int r4 = r4 * 2; // 随机数先乘以2
int r4 = r4 + 4; // 再加上4
这两步运算后,r4的值变成了“随机数×2 + 4”。
(3)分析条件判断逻辑
紧接着运算后,代码进行了条件判断:
if (r4 != r5) goto L53; // 如果运算后的r4不等于用户输入的r5,跳转到失败逻辑
// 否则执行成功逻辑(显示正确提示、解密字符串)
即:如果“随机数×2 + 4”等于用户输入的r5,则校验通过,显示“Yey you guessed it right”提示;否则失败,显示“Try again”提示。。
3. 总结
通过拆解check方法对参数的运算(r4*2+4)和条件判断(r4 == r5),可以直接得出校验逻辑:用户输入的数字必须等于“随机数×2 + 4”才能通过校验。
2.2 脚本编写
代码结构与之前保持一致,参考上一章内容 4.1 章节部分3:【Frida Android】基础篇2:Frida基础操作模式详解
hook.js
import Java from 'frida-java-bridge';
Java.perform(function () {
// 定位目标类
var MainActivity = Java.use('com.ad2001.frida0x1.MainActivity');
// Hook check方法,替换用户输入为“正确答案”
MainActivity.check.implementation = function (randomNum, userInput) {
// 计算正确答案:随机数*2 + 4
var correctAnswer = randomNum * 2 + 4;
// 调用原始check方法,但传入正确答案(忽略用户输入)
this.check(randomNum, correctAnswer);
};
});
run.py
逻辑不变,就修改目标应用包名PACKAGE_NAME。
import frida
import sys
import time
def on_message(message, data):
if message['type'] == 'send':
print(f"[Hook 日志] {message['payload']}")
elif message['type'] == 'error':
print(f"[错误] {message['stack']}")
# 目标应用包名
PACKAGE_NAME = "com.ad2001.frida0x1"
def main():
try:
device = frida.get_usb_device(timeout=10)
print(f"已连接设备:{device.name}")
print(f"启动进程 {PACKAGE_NAME}...")
pid = device.spawn([PACKAGE_NAME])
device.resume(pid)
time.sleep(2)
process = device.attach(pid)
print(f"已附加到进程 PID: {pid}")
with open("./js/compiled_hook.js", "r", encoding="utf-8") as f:
js_code = f.read()
script = process.create_script(js_code)
script.on('message', on_message)
script.load()
print("JS 脚本注入成功,开始监控...(按 Ctrl+C 退出)")
sys.stdin.read()
except frida.TimedOutError:
print("未找到USB设备")
except frida.ProcessNotFoundError:
print(f"应用 {PACKAGE_NAME} 未安装")
except FileNotFoundError:
print("未找到 js 脚本,请检查路径")
except Exception as e:
print(f"异常:{str(e)}")
finally:
if 'process' in locals():
process.detach()
print("程序退出")
if __name__ == "__main__":
main()
2.3 注入脚本
# 终端1:编译 hook.js
npm run watch
# 终端2:启动 frida_server
adb devices
adb shell
su
cd /data/local/tmp
./frida-server &
# 终端3:启动 run.py
python run.py
# 退出注入
# 终端3 按 Ctrl+C



上述步骤正常执行后,在 APK 界面中输入任意 1 个数字后,点击 submit 按钮后显示成功。

3 技术总结
通过这个案例,我们完成了一次简单的Java层Hook实操,核心知识点可以总结为以下3点:
3.1 逆向分析是Hook的前提
在写Hook脚本前,必须先搞清楚目标APP的核心逻辑:
- 用
jadx-gui反编译APK,找到关键类(如本案例的MainActivity); - 分析核心方法(如
check方法)的逻辑:输入什么参数、做了什么计算、如何判断结果(本案例中是“随机数×2+4”与用户输入的对比)。
小白提示:反编译工具(如jadx)是逆向的“眼睛”,先看懂代码逻辑,才能知道该“钩”哪里。
3.2 Frida脚本的核心逻辑:定位→修改
Frida操作Java层的步骤很固定:
- 定位目标:用
Java.use('类名')找到要Hook的类(如com.ad2001.frida0x1.MainActivity); - Hook方法:通过
类名.方法名.implementation = function(参数) {...}替换原方法的实现; - 自定义逻辑:在替换的函数中,我们可以修改参数(如本案例中将用户输入改为“正确答案”),再调用原方法。
Frida的JavaScript API很直观,核心就是“找到谁→改什么→怎么改”。
3.3 脚本注入的完整流程
让Hook生效的步骤:
- 手机端运行
frida-server(让Frida能控制手机里的APP); - 电脑端用Python脚本(
run.py)启动目标APP并注入Frida脚本; - 操作APP,Hook逻辑自动生效(如本案例中输入任意数字都提示“正确”)。
浙公网安备 33010602011771号