码到成功
Frida Hook Android
Frida 到底在做什么
一句话概括,Frida 就是在目标进程里塞进一层可编程的运行时,让你用脚本在运行时观察、替换、增强函数行为。
你可以把它拆成三块来看:
- 设备侧:目标进程里有
Frida运行时 - 脚本侧:你写
JavaScript做 hook 和消息传递 - 控制侧:你用
CLI或Python去启动、附加、收集输出
所以它不是传统意义上“改源码再重编译”,而更像“运行起来以后再插进去看”。
这就是它最迷人的地方。很多平时藏在运行时里的行为,比如参数是怎么变的、返回值哪里开始歪掉了、某个 native 调用是不是被走到了,用 Frida 都能很快摸出来。
什么时候 Frida 比日志更顺手
如果只是打印几行流程日志,直接改代码通常更快。但下面这些场景,Frida 会明显更省劲:
- 你不想频繁改包、打包、安装
- 问题只在某几个运行时分支里出现
- Java 层和 Native 层要连起来一起看
- 想快速确认某个方法的真实入参和出参
- 你只想做临时观测,不想把调试代码留在工程里
说白了,它特别适合那种“我现在就想看看,代码到底跑成啥样了”的时刻。
一张够用的工作流
如果你要把 Frida 用顺,记住这条链就够了:
- 准备测试设备和自有调试包
- 让 Python 或 CLI 连上设备
- 启动或附加目标进程
- 注入 JavaScript 脚本
- 在脚本里 hook Java / Native 方法
- 把消息发回宿主侧做展示或保存
这条线跑通以后,你后面补的都只是细节。
Android 侧通常怎么接
最常见的方式有两类:
- 在测试设备上跑
frida-server - 在自己的应用里接
Frida Gadget
前者比较适合开发调试机,后者更适合你想把运行时能力打进自己的测试包里。两种方式本质上都在解决一个问题:让 Frida 能进到目标进程里。
如果只是调自己的 debug 包,通常建议:
- 版本对应别乱套,客户端和设备侧尽量匹配
- 把环境固定在测试机,不要和正式环境混着用
- 用清晰的包名和构建标记区分 debug / release
这听起来像废话,但真到现场,很多“为什么 attach 不上”的问题都出在环境没对齐。
Java 层 hook:先从最有手感的地方开始
Frida 在 Android 上最容易上手的一段,通常是 Java 层。因为类名、方法名、参数签名只要能确认下来,脚本就比较直接。
下面这个例子假设你在调试自己的应用 com.example.debugdemo,想看看某个格式化方法在运行时到底收到了什么:
Java.perform(function () {
const DebugUtils = Java.use("com.example.debugdemo.DebugUtils");
DebugUtils.formatProfile
.overload("java.lang.String", "int")
.implementation = function (name, level) {
send({
tag: "java-enter",
method: "DebugUtils.formatProfile",
args: {
name: name,
level: level
}
});
const result = this.formatProfile(name, level);
send({
tag: "java-leave",
method: "DebugUtils.formatProfile",
result: result
});
return result;
};
});
这段代码的重点不在“替换行为”,而在“把方法观测干净地插进去”:
- 进入方法时打印参数
- 调原始实现
- 出来以后打印返回值
这类 hook 非常适合排查:
- 某个字段在进入业务层前是不是就错了
- UI 组装逻辑是否把数据拼坏了
- ViewModel 到 Repository 之间有没有参数漂移
Native 层 hook:把 C/C++ 调用也看见
当问题落到 JNI 或 so 里,Java 层日志通常就不够用了。这时候 Interceptor.attach 会很顺手。
下面还是以自有测试样例为前提,假设你自己的 libdemo.so 导出了一个 JNI 函数:
const target = Module.findExportByName(
"libdemo.so",
"Java_com_example_debugdemo_NativeBridge_add"
);
if (target) {
Interceptor.attach(target, {
onEnter(args) {
this.left = args[2].toInt32();
this.right = args[3].toInt32();
send({
tag: "native-enter",
symbol: "NativeBridge.add",
left: this.left,
right: this.right
});
},
onLeave(retval) {
send({
tag: "native-leave",
symbol: "NativeBridge.add",
result: retval.toInt32()
});
}
});
}
这个思路非常朴素:
- 进函数时记下参数
- 出函数时拿到返回值
- 把整条调用链补完整
如果 Java 层看到的是 5 + 7,Native 层出来却变成了一个奇怪结果,那问题范围瞬间就被你收紧了。
用 Python 把整套流程接起来
很多人第一次接触 Frida 时,只在终端里用命令行。其实 Python 一接上,体验会顺很多。你可以:
- 自动连接测试设备
- 自动 spawn / attach 指定包名
- 统一收集
send()回来的消息 - 顺手把结果写文件、做过滤、分级展示
一个简洁的 Python 驱动脚本可以像这样:
import frida
import sys
from pathlib import Path
PACKAGE_NAME = "com.example.debugdemo"
SCRIPT_PATH = Path("observe_debugutils.js")
def on_message(message, data):
if message["type"] == "send":
payload = message["payload"]
print(f"[hook] {payload}")
elif message["type"] == "error":
print("[error]", message["stack"])
def main():
device = frida.get_usb_device(timeout=5)
pid = device.spawn([PACKAGE_NAME])
session = device.attach(pid)
script = session.create_script(SCRIPT_PATH.read_text(encoding="utf-8"))
script.on("message", on_message)
script.load()
device.resume(pid)
sys.stdin.read()
if __name__ == "__main__":
main()
这段代码虽然短,但已经把主链路串起来了:
- 连接设备
- 启动 App
- 注入脚本
- 接收消息
- 保持会话
如果你想更工程化一点,还可以继续加:
- 日志分级
- JSON 落盘
- 多脚本切换
- 白名单类名过滤
Python 再往前走一步:批量观测和自动化采样
如果你的目标不是“看一眼”,而是“跑很多次看看稳定性”,Python 就更有用了。
比如你可以在宿主侧做一个轻量采样器,把某个方法的返回值分布简单统计出来:
from collections import Counter
counter = Counter()
def on_message(message, data):
if message["type"] != "send":
return
payload = message["payload"]
if payload.get("tag") == "java-leave":
result = str(payload.get("result"))
counter[result] += 1
print("top results:", counter.most_common(5))
这种写法不炫,但很实用。尤其是你在排查“偶发格式化异常”或“同一批输入为什么会出不同输出”时,它能比肉眼盯日志舒服很多。
Hook 脚本写起来,真正容易踩的坑在哪
真正把 Frida 用到工程里以后,常见问题反而不是“能不能 hook”,而是“hook 完以后是不是还稳”。
几个特别常见的坑:
1. 签名没对上
Android 方法重载一多,overload() 选错参数类型,脚本看起来像加载了,实际上根本没进目标方法。
2. 时机太早或太晚
有些类还没加载,你就去 Java.use(),自然会报错。还有些场景是 App 逻辑已经跑过去了,你才 attach,上半段调用早没了。
3. 打印太猛
如果你对高频函数疯狂 send(),性能抖动会很明显,日志也会把自己淹没。观测点越精,脚本越耐用。
4. Native 参数理解错位
JNI 函数参数不是你肉眼看名字就能全猜对,尤其到复杂结构体、指针偏移这些地方,先确认签名比盲打重要得多。
5. 忘了它只是调试工具
Frida 很灵活,但它不是正式埋点系统,也不是线上观测平台。你拿它做动态诊断很香,拿它顶替完整工程方案就会开始别扭。
一个更稳的实践姿势
如果你想让这套东西别只停在“偶尔用一下”,可以按下面这个思路整理:
- 给常用 hook 脚本按模块分类
- 宿主侧 Python 做统一入口
- 约定统一消息格式,比如
tag / method / args / result - 先观测,再决定是否临时改行为
- 每次只盯一小段链路,不贪多
这样后面你查 UI、网络封装、JNI 桥接、序列化逻辑,基本都能沿着同一套姿势往前推。