【安卓逆向】Frida配置和简单hook

1. Frida 安装

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();
    };
});

感谢关注【遇事不決洛必達】!欢迎点赞收藏和交流指正,我会持续分享我的学习经验和心得。