2020-02-18
把动态插桩、方法观测和 Python 驱动串起来。如果你是在调试自己的 Android 应用,或者想做一套更灵活的动态观测方案,`Frida` 非常适合干这类活:
一句话概括,Frida 就是在目标进程里塞进一层可编程的运行时,让你用脚本在运行时观察、替换、增强函数行为。
你可以把它拆成三块来看:
Frida 运行时JavaScript 做 hook 和消息传递CLI 或 Python 去启动、附加、收集输出所以它不是传统意义上“改源码再重编译”,而更像“运行起来以后再插进去看”。
这就是它最迷人的地方。很多平时藏在运行时里的行为,比如参数是怎么变的、返回值哪里开始歪掉了、某个 native 调用是不是被走到了,用 Frida 都能很快摸出来。
如果只是打印几行流程日志,直接改代码通常更快。但下面这些场景,Frida 会明显更省劲:
说白了,它特别适合那种“我现在就想看看,代码到底跑成啥样了”的时刻。
如果你要把 Frida 用顺,记住这条链就够了:
这条线跑通以后,你后面补的都只是细节。
最常见的方式有两类:
frida-serverFrida Gadget前者比较适合开发调试机,后者更适合你想把运行时能力打进自己的测试包里。两种方式本质上都在解决一个问题:让 Frida 能进到目标进程里。
如果只是调自己的 debug 包,通常建议:
这听起来像废话,但真到现场,很多“为什么 attach 不上”的问题都出在环境没对齐。
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 非常适合排查:
当问题落到 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 层出来却变成了一个奇怪结果,那问题范围瞬间就被你收紧了。
很多人第一次接触 Frida 时,只在终端里用命令行。其实 Python 一接上,体验会顺很多。你可以:
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()
这段代码虽然短,但已经把主链路串起来了:
如果你想更工程化一点,还可以继续加:
如果你的目标不是“看一眼”,而是“跑很多次看看稳定性”,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))
这种写法不炫,但很实用。尤其是你在排查“偶发格式化异常”或“同一批输入为什么会出不同输出”时,它能比肉眼盯日志舒服很多。
真正把 Frida 用到工程里以后,常见问题反而不是“能不能 hook”,而是“hook 完以后是不是还稳”。
几个特别常见的坑:
Android 方法重载一多,overload() 选错参数类型,脚本看起来像加载了,实际上根本没进目标方法。
有些类还没加载,你就去 Java.use(),自然会报错。还有些场景是 App 逻辑已经跑过去了,你才 attach,上半段调用早没了。
如果你对高频函数疯狂 send(),性能抖动会很明显,日志也会把自己淹没。观测点越精,脚本越耐用。
JNI 函数参数不是你肉眼看名字就能全猜对,尤其到复杂结构体、指针偏移这些地方,先确认签名比盲打重要得多。
Frida 很灵活,但它不是正式埋点系统,也不是线上观测平台。你拿它做动态诊断很香,拿它顶替完整工程方案就会开始别扭。
如果你想让这套东西别只停在“偶尔用一下”,可以按下面这个思路整理:
tag / method / args / result这样后面你查 UI、网络封装、JNI 桥接、序列化逻辑,基本都能沿着同一套姿势往前推。