1. Frida 安装
- 机型:小米6
- frida-server版本:16.2.1
- 下载列表:https://github.com/frida/frida/releases?page=13#release-16.2.1
- 版本链接:https://github.com/frida/frida/releases/download/16.2.1/frida-server-16.2.1-android-arm64.xz
2. Python包安装
bash
# 要与frida版本一致
pip install frida==16.2.1 frida-tools==12.0.0
3. Frida配置和启动服务
3.1 配置和启动服务
bash
# 1. 将本地的 frida-server 文件推送到手机的 /data/local/tmp/ 目录
adb push frida-server-16.2.1-android-arm64 /data/local/tmp/
# 2. 进入手机的交互式命令行界面(shell)
adb shell
# 3. 切换到 root 用户(获取最高权限)
su
# 4. 进入存放 frida-server 的目录
cd /data/local/tmp
# 5. 修改文件权限为 755(即属主可读写执行,同组和其他人只可读执行)
chmod 755 frida-server-16.2.1-android-arm64
# 6. 在后台启动 frida-server
./frida-server-16.2.1-android-arm64 &
3.2 关闭服务
bash
# 查看所有 frida 进程
ps -A | grep frida
# 输出:root 26472 ... frida-server-16.2.1-android-arm64
# 方式1:用完整名杀掉
killall frida-server-16.2.1-android-arm64
# 方式2:用 pkill 模糊匹配
pkill -f frida-server
# 方式3:用 PID 杀掉
kill -9 26472
# 验证是否已关闭
ps -A | grep frida
# 没有输出 = 已关闭

[1] + Done表示后台进程已经正常结束。
4. hook模板
4.1 py部分
python
import frida
import sys
def on_message(message, data):
"""
消息处理回调函数
当 JavaScript 脚本调用 send() 时,会触发此函数
Args:
message: 包含消息类型的字典
data: 附加数据
"""
# 如果是 send 发送的消息
if message["type"] == "send":
print("[*] {0}".format(message["payload"]))
# 如果是错误消息
elif message["type"] == "error":
print("[!] Error: {0}".format(message.get("stack", message)))
# 读取 JavaScript Hook 脚本文件
# mode="r": 只读模式
# encoding="utf-8": 指定编码格式,支持中文
with open("lession2_test_hook_rps.js", mode="r", encoding="utf-8") as f:
test_js = f.read()
# ============================================
# 方式一:启动应用之后附加(Attach 模式)
# ============================================
# 适用场景:应用已经在运行,需要动态注入
try:
# 获取通过 USB 连接的设备
device = frida.get_usb_device()
print("[*] Device: {0}".format(device))
# 方法1:通过 PID 附加(推荐,更稳定)
# windows下查看PID(adb shell "ps -A | grep rock_paper_scissors")
PID = 24201
print("[*] Attaching to PID: {0}".format(PID))
session = device.attach(PID)
# 方法2:通过包名附加(需要应用在前台,注释掉了,因为我是用这种方法会失败)
# session = device.attach("com.example.seccon2015.rock_paper_scissors")
print("[✓] Attached!")
# 创建脚本对象(将 JavaScript 代码加载到 Frida 中)
script = session.create_script(test_js)
# 注册消息回调
script.on("message", on_message)
# 注入并执行脚本
script.load()
print("[*] Script loaded! Click buttons on phone")
print("[*] Press Ctrl+C to exit")
# 阻塞主线程,保持脚本运行
# 等待用户输入,按任意键退出
sys.stdin.read()
except Exception as e:
print("[!] Error: {0}".format(e))
# ============================================
# 方式二:启动阶段注入(Spawn 模式)- 注释掉的代码
# ============================================
# 适用场景:应用未启动,需要在启动时就注入
#
# device = frida.get_usb_device(-1) # -1 表示默认设备
#
# # 启动应用(暂停在启动状态)
# pid = device.spawn(["com.example.seccon2015.rock_paper_scissors"])
#
# # 附加到新启动的进程
# process = device.attach(pid)
#
# # 创建并加载脚本
# script = process.create_script(test_js)
# script.on("message", on_message)
# print("[*] Script loaded!")
# script.load()
#
# # 恢复应用执行(spawn 后应用是暂停状态)
# device.resume(pid)
#
# # 阻塞,使 hook 脚本不退出
# sys.stdin.read()
4.2 js模板
javascript
/**
* Frida Hook 脚本模板
* 用于 Hook Android Java 方法
*/
// Java.perform 是 Frida 的入口函数,确保代码在 Java 虚拟机上下文中执行
Java.perform(function() {
console.log('start hook...');
// ============================================
// 1. 获取目标类
// ============================================
// Java.use() 获取 Java 类的引用
var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity");
// ============================================
// 2. Hook 方法
// ============================================
// 重写 onClick 方法
// v: 方法参数
MainActivity.onClick.implementation = function(v) {
// 调用原始方法(Frida 会自动保留原始方法引用,不会递归)
this.onClick(v);
// TODO: 在这里添加 Hook 逻辑
// 例如:打印参数、修改返回值等
};
// ============================================
// 3. Hook 内部类
// ============================================
// 内部类使用 $ 连接,如 MainActivity$1
var MainActivity1 = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity$1");
// 重写 run 方法
MainActivity1.run.implementation = function() {
// TODO: 在这里添加 Hook 逻辑
// 调用原始方法
this.run();
};
});
4.3 js示例
javascript
/**
* Frida Hook 脚本 - 剪刀石头布游戏
* 目标:通过修改 CPU 的出拳逻辑,让玩家总是获胜
* 包名:com.example.seccon2015.rock_paper_scissors
*/
// Java.perform 是 Frida 的入口函数
// 确保所有 Java 相关操作在 Java 虚拟机环境中执行
Java.perform(function() {
console.log('start hook...');
// ============================================================
// 1. Hook MainActivity 的 onClick 方法(玩家点击按钮时触发)
// ============================================================
// Java.use() 获取 Java 类的引用,返回一个包装对象
var MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity");
/**
* 重写 onClick 方法
* @param {View} v - 被点击的按钮视图(P/R/S 按钮)
*
* 注意:这里不会发生递归!
* this.onClick(v) 调用的是 Frida 内部保留的原始方法
* 而不是当前重写的 implementation,这是 Frida 的设计机制
*/
MainActivity.onClick.implementation = function(v) {
// 1. 调用原始 onClick 方法,保证游戏正常运行
// 包括:记录玩家选择、生成 CPU 选择、触发延迟显示等
this.onClick(v);
// 2. 打印玩家的选择
// m: 玩家的出拳 (0=布 Paper, 1=石头 Rock, 2=剪刀 Scissors)
console.log('[Hook] m', this.m.value);
// 3. 打印 CPU 的选择
// n: CPU 的出拳 (0=布 Paper, 1=石头 Rock, 2=剪刀 Scissors)
console.log('[Hook] n real', this.n.value);
// ============================================================
// 4. 胜负判定(与原始代码逻辑一致)
// 规则:布(0) > 石头(1) > 剪刀(2) > 布(0)
// ============================================================
// 情况1:CPU 出拳数值 - 玩家出拳数值 == 1
// 例如:玩家出布(0),CPU出石头(1) → 玩家赢
// 玩家出石头(1),CPU出剪刀(2) → 玩家赢
// 玩家出剪刀(2),CPU出布(0) → 此时差值为 -2,不满足此条件
if (this.n.value - this.m.value == 1) {
console.log('[Hook] You win!');
}
// 情况2:玩家出拳数值 - CPU 出拳数值 == 1
// 例如:玩家出石头(1),CPU出布(0) → CPU赢
// 玩家出剪刀(2),CPU出石头(1) → CPU赢
else if (this.m.value - this.n.value == 1) {
console.log('[Hook] You lose!');
}
// 情况3:玩家和 CPU 出拳相同 → 平局
else if (this.m.value == this.n.value) {
console.log('[Hook] Draw!');
}
// 情况4:玩家出拳数值小于 CPU 出拳数值
// 例如:玩家出布(0),CPU出剪刀(2) → CPU赢
else if (this.m.value < this.n.value) {
console.log('[Hook] You lose!');
}
// 情况5:其他情况 → 玩家赢
// 例如:玩家出剪刀(2),CPU出布(0) → 玩家赢
else {
console.log('[Hook] You win!');
}
};
// ============================================================
// 2. Hook 内部类 MainActivity$1 的 run 方法
// MainActivity$1 是 MainActivity 中的匿名内部类
// 实现了 Runnable 接口,用于延迟 1 秒后显示游戏结果
// ============================================================
var MainActivity1 = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity$1");
/**
* 重写 run 方法
* 核心破解逻辑:修改 CPU 的出拳,让玩家总是赢
*
* 注意:这里同样不会发生递归!
* this.run() 调用的是原始 run 方法,不是重写后的 implementation
*/
MainActivity1.run.implementation = function() {
console.log('[Hook] run');
// ============================================================
// 关键破解逻辑:修改 CPU 的出拳
// ============================================================
// this.this$0 是内部类持有的外部类 MainActivity 的引用
// 通过 this.this$0 可以访问外部类的成员变量
//
// 将 CPU 的出拳(n) 设置为 玩家的出拳(m) + 1
// 使得 CPU 总是出会被玩家打败的选项:
// 玩家出布(0) → CPU出石头(1) → 布包裹石头 → 玩家赢
// 玩家出石头(1) → CPU出剪刀(2) → 石头砸剪刀 → 玩家赢
// 玩家出剪刀(2) → CPU出布(0) → 剪刀剪布 → 玩家赢
// 注意:当 m=2 时,m+1=3,但数组索引只有 0-2
// 实际游戏中,3 会被映射为剪刀 vs 布的逻辑,需要看原始代码如何处理
this.this$0.value.n.value = this.this$0.value.m.value + 1;
console.log('[Hook] n change', this.this$0.value.n.value);
// 调用原始的 run 方法,显示游戏结果
// 此时 n 已经被修改,所以显示的结果是玩家赢
this.run();
};
});
感谢关注【遇事不決洛必達】!欢迎点赞收藏和交流指正,我会持续分享我的学习经验和心得。