frida 入门:给一个自建 demo App 做运行时 hook(看入参、改返回)

张 张站长 · 2026-09-15 19:16 · 技术-app · 47 次浏览 · 0 条评论

手机上没有控制台,怎么在运行时看一个 App 内部函数的入参出参、甚至改它的返回?用 frida。这篇从装环境到写出第一个能跑的 hook,全程用自己写的极简 demo App 当靶子。

前两篇讲的 hook,在浏览器里是"换掉 window 上的函数"。到了手机 App 上,你没有控制台,也改不了别人的安装包,怎么在运行时看一个函数的入参出参、甚至改它的返回?答案是 frida——一个运行时插桩(instrumentation)工具,它能把一个 JavaScript 引擎注入到目标进程里,让你用 JS 去包裹、替换 App 里的任意函数。

这篇从装环境到写出第一个能跑的 hook,全程用我自己写的一个极简 demo App 当靶子,纯净、可复现。对自己的 App 做插桩来学原理,是最干净的练法。

frida 是怎么把你的脚本送进 App 的

一、frida 的工作方式

看上面这张图,三块:

  • 电脑上的 frida 客户端(frida-tools):你在这写脚本、看输出、下命令。
  • 手机上的 frida-server:一个常驻进程,是电脑和 App 之间的代理。
  • 目标 App 进程:frida 把一个 JS 运行时注入进这个进程,你的脚本就在 App 内部执行,因此能直接够到它的类和方法。

关键词是"运行时"——你不改安装包、不重新编译,App 正常跑着,你在旁边挂上去、实时替换它的函数。

二、准备环境(Android 模拟器,最省事)

用一个带 root 的模拟器最方便(比如 Android Studio 自带的模拟器选 "Google APIs" 镜像,可 root;或用带 root 的第三方模拟器)。步骤:

1) 电脑装 frida 客户端:

pip install frida-tools
frida --version          # 记下这个版本号,比如 16.x.x

2) 下对应版本、对应架构的 frida-server 推到手机。
版本必须和上面客户端一致,架构看模拟器(一般是 x86_64 或 arm64)。从 frida 的 GitHub releases 下 frida-server-<版本>-android-<架构>.xz,解压后:

adb root                                    # 让 adb 以 root 运行
adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"  # 后台起 server

3) 验证连通:

frida-ps -U          # -U 走 USB。能列出手机上的进程就说明通了

能看到进程列表,环境就绪。连不上九成是版本/架构没对齐,这是新手最常卡的地方。

三、写一个可复现的靶子 App

为了不碰任何真实 App,我们自己写一个最小的。用 Android Studio 新建一个空项目,MainActivity 里放一个我们要 hook 的方法:

public class MainActivity extends AppCompatActivity {
    // 我们要 hook 的目标:一个简单的"校验"方法
    public boolean checkLicense(String user, int level) {
        Log.d("DEMO", "checkLicense called: " + user + " / " + level);
        return level >= 3;          // 只有 level>=3 才通过
    }

    @Override
    protected void onCreate(Bundle b) {
        super.onCreate(b);
        // 每隔 2 秒调一次,方便我们观察 hook 效果
        new android.os.Handler().postDelayed(new Runnable() {
            public void run() {
                boolean ok = checkLicense("zhang", 1);
                Log.d("DEMO", "结果 = " + ok);
                new android.os.Handler().postDelayed(this, 2000);
            }
        }, 2000);
    }
}

装到模拟器上跑起来。checkLicense("zhang", 1) 因为 level=1,正常会一直返回 false。我们的目标:hook 它,看到真实入参,并把返回改成 true。(记下包名,假设是 com.demo.hooktarget。)

四、第一个 hook:看入参、看返回

新建 hook.js:

Java.perform(function () {
    // 拿到目标类
    var MainActivity = Java.use("com.demo.hooktarget.MainActivity");

    // 重写 checkLicense —— 注意重载要写清参数类型:String + int
    MainActivity.checkLicense.overload('java.lang.String', 'int').implementation =
        function (user, level) {
            console.log("[hook] checkLicense 入参: user=" + user + ", level=" + level);

            // 调用原始实现,拿到真实返回
            var real = this.checkLicense(user, level);
            console.log("[hook] 原始返回: " + real);

            return real;   // 先原样返回,只看不改
        };
});

挂上去:

frida -U -f com.demo.hooktarget -l hook.js
# -f 表示由 frida 启动 App(能 hook 到最早期);不加 -f 就 attach 已经在跑的

现在 frida 的控制台里每 2 秒就会打印一次:入参: user=zhang, level=1、原始返回: false。你已经能看到一个 App 内部方法的真实参数和返回值了——这就是插桩最基础也最有用的能力。

五、改返回值

把 hook 里的 return real; 改成:

console.log("[hook] 把返回强制改成 true");
return true;

重新挂上去,你会看到 App 那边的日志从 结果 = false 变成了 结果 = true——你在运行时改变了这个方法的行为,而没动 App 一行源码。这就是"运行时插桩"最直观的效果。

这一步在自己的 demo 上做是为了理解机制。对别人的 App 改校验逻辑属于另一回事,不在本文讨论。

六、找不到类名/方法名怎么办

真实 App 你不知道类叫什么、方法叫什么。几个通用手段(这里只讲方法,不针对任何具体 App):

  • 枚举已加载的类:
Java.perform(function () {
    Java.enumerateLoadedClasses({
        onMatch: function (name) {
            if (name.indexOf("demo") !== -1) console.log(name);  // 按关键词过滤
        },
        onComplete: function () {}
    });
});
  • objection(基于 frida 的封装工具)能直接在命令行里搜类、搜方法、一键 hook,比手写脚本快:objection -g <包名> explore,然后 android hooking search classes <关键词>。
  • 静态先看一眼:用 jadx 反编译 APK,找到大概的类名/方法名,再回 frida 动态确认。动静结合是常态。

七、Native 层一句话

如果目标逻辑在 .so(C/C++)里,frida 也能 hook,用的是 Interceptor:

var addr = Module.findExportByName("libdemo.so", "native_check");
Interceptor.attach(addr, {
    onEnter: function (args) { console.log("native 入参:", args[0], args[1]); },
    onLeave: function (retval) { console.log("native 返回:", retval); }
});

onEnter 看/改入参、onLeave 看/改返回,思路和 Java 层完全一样,只是操作的是寄存器和内存指针。Native hook 细节能再写一整篇,这里先知道"能 hook、入口在 Interceptor.attach"就够了。

八、常见坑

  • 版本不匹配:frida-server 和 pip 装的 frida 版本号必须一致,差一个小版本就连不上或行为怪异。
  • 架构不匹配:模拟器是 x86_64 就别推 arm64 的 server。
  • 重载没写全:Java 方法重载(同名不同参)很常见,implementation 前一定要 .overload(参数类型...) 指明是哪一个,否则 frida 报"有多个重载"。
  • 权限/SELinux:非 root 或 SELinux 严格的机器上 frida-server 起不来,用可 root 的模拟器最省心。
  • App 反调试:有些 App 会检测 frida。这属于对抗话题,不在本文——本文的靶子是自己写的、不设防的 demo,专注把 hook 机制本身跑通。

到这里,运行时插桩的核心你就拿到了:frida 把 JS 引擎注入进程 → Java.use 拿到类 → 重写 implementation 看参数/改返回 → Native 用 Interceptor.attach。全程用自己的 demo App,每一步都能看到确定的效果。把上面的 demo App 建起来、把"看入参 → 改返回"跑通,你就有了在移动端做运行时分析的最基本手感。

评论 0

还没有评论
← 全部文章

找站长

站长微信二维码 扫码加微信
售后群二维码 扫码进售后群
站长收款码 建站不易,感谢打赏
微信 18581469390
本站不支持自助充值;需要充值或开通额度,请直接联系站长。
加微信可以找我重置密码、充值、问工具怎么用