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包安装
# 要与frida版本一致pipinstallfrida==16.2.1 frida-tools==12.0.03. Frida配置和启动服务
3.1 配置和启动服务
# 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(即属主可读写执行,同组和其他人只可读执行)chmod755frida-server-16.2.1-android-arm64# 6. 在后台启动 frida-server./frida-server-16.2.1-android-arm64&3.2 关闭服务
# 查看所有 frida 进程ps-A|grepfrida# 输出:root 26472 ... frida-server-16.2.1-android-arm64# 方式1:用完整名杀掉killallfrida-server-16.2.1-android-arm64# 方式2:用 pkill 模糊匹配pkill-ffrida-server# 方式3:用 PID 杀掉kill-926472# 验证是否已关闭ps-A|grepfrida# 没有输出 = 已关闭[1] + Done表示后台进程已经正常结束。
4. hook模板
4.1 py部分
importfridaimportsysdefon_message(message,data):""" 消息处理回调函数 当 JavaScript 脚本调用 send() 时,会触发此函数 Args: message: 包含消息类型的字典 data: 附加数据 """# 如果是 send 发送的消息ifmessage["type"]=="send":print("[*] {0}".format(message["payload"]))# 如果是错误消息elifmessage["type"]=="error":print("[!] Error: {0}".format(message.get("stack",message)))# 读取 JavaScript Hook 脚本文件# mode="r": 只读模式# encoding="utf-8": 指定编码格式,支持中文withopen("lession2_test_hook_rps.js",mode="r",encoding="utf-8")asf: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=24201print("[*] 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()exceptExceptionase: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模板
/** * Frida Hook 脚本模板 * 用于 Hook Android Java 方法 */// Java.perform 是 Frida 的入口函数,确保代码在 Java 虚拟机上下文中执行Java.perform(function(){console.log('start hook...');// ============================================// 1. 获取目标类// ============================================// Java.use() 获取 Java 类的引用varMainActivity=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$1varMainActivity1=Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity$1");// 重写 run 方法MainActivity1.run.implementation=function(){// TODO: 在这里添加 Hook 逻辑// 调用原始方法this.run();};});4.3 js示例
/** * 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 类的引用,返回一个包装对象varMainActivity=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赢elseif(this.m.value-this.n.value==1){console.log('[Hook] You lose!');}// 情况3:玩家和 CPU 出拳相同 → 平局elseif(this.m.value==this.n.value){console.log('[Hook] Draw!');}// 情况4:玩家出拳数值小于 CPU 出拳数值// 例如:玩家出布(0),CPU出剪刀(2) → CPU赢elseif(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 秒后显示游戏结果// ============================================================varMainActivity1=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();};});感谢关注【遇事不決洛必達】!欢迎点赞收藏和交流指正,我会持续分享我的学习经验和心得。