xxxxxx
ARM64 Android 动态插桩框架。
~/Android/Sdk/ndk/)aarch64-linux-android target.cargo/config.toml 已配置交叉编译(仓库自带)首次 clone 后先拉取子仓库:
git submodule update --init --recursivequickjs-hook/third_party/tinycc 是 RF 的 CModule 编译器子仓库,默认跟随 https://github.com/kkkbbb/tinycc.git 的 rf/cmodule-runtime 分支。
最终产物 rustfrida 通过 include_bytes! 内嵌了 loader shellcode 和 agent SO,有严格的构建顺序:
loader shellcode ──┐
├──→ rustfrida (主程序)
agent (libagent.so) ┘python3 build_helpers.py
# 输出:
# loader/build/bootstrapper.bin
# loader/build/rustfrida-loader.binloader 是 bare-metal ARM64 shellcode,被 rustfrida 通过 include_bytes! 嵌入。修改 loader C 代码后需重新运行此步。
cargo build -p agent --release
# 输出: target/aarch64-linux-android/release/libagent.soagent 是注入到目标进程的动态库,包含 hook 引擎、QuickJS、Java hook 等。必须先于 rustfrida 构建,因为 rustfrida 通过 include_bytes! 嵌入 agent SO。
cargo build -p rust_frida --release
# 输出: target/aarch64-linux-android/release/rustfridarustfrida 内嵌了 bootstrapper.bin + rustfrida-loader.bin + libagent.so,是一个自包含的单文件。
这些不在 default-members 里,按需构建:
QBDI Trace 支持: 需要先构建 qbdi-helper SO,再用 --features qbdi 编译 agent 和 rustfrida:
cargo build -p qbdi-helper --release # → libqbdi_helper.so
cargo build -p agent --release --features qbdi # agent 启用 qbdi feature
cargo build -p rust_frida --release --features qbdi # rustfrida 嵌入 qbdi-helper SOeBPF SO 加载监控(--watch-so): ldmonitor 是 rustfrida 的编译依赖,默认构建已包含,--watch-so 无需额外步骤。如需独立使用 ldmonitor 命令行工具:
cargo build -p ldmonitor --release # → ldmonitor 独立二进制RF 的 CModule 功能依赖 quickjs-hook/third_party/tinycc。该目录是 git submodule,RF 定制修改维护在 rf/cmodule-runtime 分支:
git -C quickjs-hook/third_party/tinycc remote -v
# origin https://github.com/kkkbbb/tinycc.git
# upstream https://github.com/frida/tinycc.git
git -C quickjs-hook/third_party/tinycc status --short --branch同步上游时,在子仓库 rebase 后更新父仓库的 gitlink:
git -C quickjs-hook/third_party/tinycc fetch upstream
git -C quickjs-hook/third_party/tinycc rebase upstream/main
git -C quickjs-hook/third_party/tinycc push origin rf/cmodule-runtime
git add quickjs-hook/third_party/tinycc
git commit -m "Update tinycc submodule"如果修改了 TinyCC 本身,先在子仓库提交并 push,再回到父仓库提交 submodule 指针。
adb push target/aarch64-linux-android/release/rustfrida /data/local/tmp/
# PID 注入
./rustfrida --pid
./rustfrida --pid -l script.js
# Spawn 模式(启动时注入)
./rustfrida --spawn com.example.app
./rustfrida --spawn com.example.app -l script.js
# 等待 SO 加载后注入(eBPF)
./rustfrida --watch-so libnative.so
# 详细日志
./rustfrida --pid --verbose
# 同步输出日志到文件(终端仍正常输出,文件为纯文本)
./rustfrida --pid -l script.js -o /data/local/tmp/rustfrida.logjsinit # 初始化 JS 引擎
jseval # 求值表达式
loadjs # 执行脚本
jsrepl # 交互式 REPL(Tab 补全)
exit # 退出最常见的工作流是:写一个 script.js,用 -l 加载到目标进程,然后通过日志、RPC 或文件把结果带出来。
# 已运行的进程
./rustfrida --pid -l script.js
# 从启动阶段注入,适合抓 Application / ClassLoader 初始化
./rustfrida --spawn com.example.app -l script.js
# 先进入交互,再手动 loadjs / jseval
./rustfrida --pid 最小脚本:
console.log("agent loaded");
Java.ready(function() {
console.log("Java is ready");
});| 你想做什么 | 优先使用 | 典型入口 |
|---|---|---|
| Hook Java 方法、改参数/返回值 | Java.use() |
Class.method.impl = function (...) { ... } |
| 高频 Java 方法 Hook | Managed DSL 动态编译器 | method.dslImpl = script |
| Hook native 函数并继续跑原函数 | Interceptor.attach |
onEnter(args) / onLeave(retval) |
| 完全替换 native 函数 | hook() 或 Interceptor.replace() |
return value / 条件性 this.$orig() |
| 高频 native Hook | CModule + attachNative / hookNative |
void cb(HookContext *ctx, void *data) |
| 查找 so、符号、导入导出 | Module |
findExportByName() / enumerateSymbols() |
| 读写目标进程内存 | Memory / ptr() |
p.readU32() / p.writeBytes() |
| 监控 JNI 注册 | Jni + native hook |
Jni.addr("RegisterNatives") |
| 远程触发脚本能力 | HTTP RPC | rpc.exports = { ... } |
| 采集指令 trace 用于回放分析 | qbdi |
registerTraceCallbacks() |
适合看业务参数、绕过判断、替换返回值。Spawn 模式下务必放在 Java.ready() 里。
Java.ready(function() {
var Login = Java.use("com.example.LoginManager");
Login.checkPassword.impl = function(user, pass) {
console.log("checkPassword", user, pass);
return true; // 直接改返回值,不调原方法
};
});需要保留原逻辑时调用 $orig():
Java.ready(function() {
var Log = Java.use("android.util.Log");
Log.i.overload("java.lang.String", "java.lang.String").impl = function(tag, msg) {
console.log("[Log.i]", tag, msg);
return this.$orig(tag, msg);
};
});只改参数然后继续执行原函数,优先用 Interceptor.attach({ onEnter })。
var open = Module.findExportByName("libc.so", "open");
Interceptor.attach(open, {
onEnter(args) {
var path = args[0].readCString();
console.log("open", path);
if (path.indexOf("/proc/self/maps") >= 0) {
args[0] = Memory.allocUtf8String("/data/local/tmp/fake_maps");
}
}
});需要返回值时加 onLeave。
var getuid = Module.findExportByName("libc.so", "getuid");
Interceptor.attach(getuid, {
onLeave(retval) {
console.log("getuid =>", retval.toUInt32());
retval.replace(0);
}
});如果你需要“有时调原函数、有时直接返回”,用 hook() 更直接。
var getpid = Module.findExportByName("libc.so", "getpid");
hook(getpid, function() {
if (Date.now() & 1) {
return this.$orig(); // 调原函数,参数默认来自当前寄存器
}
return 12345; // 跳过原函数
});适合定位 Java native 方法和 so 内真实函数地址。
Interceptor.attach(Jni.addr("RegisterNatives"), {
onEnter(args) {
var cls = Jni.env.getClassName(args[1]);
var methods = Jni.structs.JNINativeMethod.readArray(args[2], Number(args[3]));
console.log("RegisterNatives:", cls);
methods.forEach(function(m) {
var mod = Module.findByAddress(m.fnPtr);
var where = mod ? mod.name + "+" + m.fnPtr.sub(mod.base) : m.fnPtr.toString();
console.log(" " + m.name + " " + m.sig + " -> " + where);
});
}
});当你希望工具常驻,然后由 host 脚本、UI 或自动化流程触发功能时,用 rpc.exports。
rpc.exports = {
ping: function() { return "pong"; },
app: function() {
var ActivityThread = Java.use("android.app.ActivityThread");
var app = ActivityThread.currentApplication();
return String(app.getPackageName());
}
};启动时加 --rpc-port,host 侧通过 curl 调用:
adb forward tcp:9191 tcp:9191
./rustfrida --pid -l script.js --rpc-port 9191
curl -X POST http://127.0.0.1:9191/rpc/0/pingJava.use().impl,稳定后再考虑 DSL。Interceptor.attach({ onEnter })。hook() / Interceptor.replace()。CModule 写 C callback,再用 attachNative / hookNative 安装。脚本里用 Frida 风格的 rpc.exports 注册方法,host 端通过 HTTP POST 调用,返回值会 JSON.stringify 后透传回来。适合把 agent 当成一个常驻服务用——UI、自动化脚本、测试框架都可以直接 curl 触发。
在 legacy 单会话或 --server 多会话模式下,加上 --rpc-port 即可启动 HTTP 服务器。参数可以是纯端口号(默认绑 0.0.0.0),也可以是完整地址:
# legacy 模式:attach + 加载脚本 + 开 RPC 端口
./rustfrida --pid 1234 -l rpc_test.js --rpc-port 9191
# server 模式:多 session 共享同一个 RPC 端口,按 session id 路由
./rustfrida --server --rpc-port 127.0.0.1:9191
# 本机访问通过 adb forward 最简单
adb forward tcp:9191 tcp:9191…rpc.exports 就是个普通 JS 对象,现场 lookup,不需要向 host 注册方法列表——你可以任意时刻增删改,下一次 HTTP 请求立刻生效。
| 方法 | 路径 | Body | 说明 |
|---|---|---|---|
GET |
/ / /health |
— | 健康检查 |
GET |
/sessions |
— | 列出所有 session(id/pid/label/status) |
POST |
/rpc// |
JSON 数组 | 调用 rpc.exports[method].apply(null, args);空 body 等价 [] |
`` 在 legacy 模式下固定为 0,在 --server 模式下对应 list 命令显示的 id。
…成功响应统一是 {"ok":true,"result":};失败是 {"ok":false,"error":""},HTTP 状态码 400(参数错)/404(session/method 不存在)/503(session 未连接)/500(JS 异常或超时)。
JSON.stringify 在 JS 侧执行,函数/循环引用/undefined 会被跳过。直接 return 一个 Java wrapper 只会得到指针字面量——请手动 String(obj.method()) 或构造 plain object。{"ok":false,"error":"rpc call timed out"}。长耗时任务请改用轮询接口。async / Promise——Promise 会被 JSON.stringify 成 {}。console, ptr(), Memory, File, Module, Interceptor, CModule, hook(), hookNative(), attachNative(), unhook(), callNative(), qbdi, Java, Jni
| 类型名 | 实际含义 |
|---|---|
AddressLike |
NativePointer | number | bigint | "0x..." |
NativePointer |
ptr() 创建的指针对象 |
JavaObjectProxy |
Java.use() / Java hook 中返回的 Java 对象代理 |
…Frida 风格:arguments = x0..x7(前 8 个整型参数,BigInt),this = register 上下文(含 x0-x30 / sp / pc / $orig)。
…Frida 兼容 API,任意参数数量(寄存器用完自动栈溢出,上限 256 个栈参数)。
var open = new NativeFunction(
Module.findExportByName("libc.so", "open"),
"int", // 返回类型
["pointer", "int"] // 参数类型
);
var fd = open(Memory.allocUtf8String("/tmp/foo"), 0);
var atan2 = new NativeFunction(
Module.findExportByName("libm.so", "atan2"),
"double",
["double", "double"]
);
atan2(1.0, 2.0);支持的类型:void, bool, char/uchar, int8/uint8, short/ushort, int16/uint16, int/uint, int32/uint32, long/ulong (64-bit), int64/uint64, size_t/ssize_t, pointer, float, double。
AAPCS64 调用约定:整数/指针先填 x0-x7,浮点先填 d0-d7(两队列独立),超出部分自动压栈。不支持 struct-by-value。
CModule 用内置 TinyCC 在目标进程里动态编译 C 代码,适合把高频 native hook 的热路径从 JS callback 下沉到 C callback。CModule 对象持有编译后的代码内存;只要 hook 还在使用其中的函数指针,就必须保留 JS 引用,避免 GC 释放代码。
var cm = new CModule(`
#include
void on_getuid(HookContext *ctx, void *user_data) {
uint64_t real = hook_invoke_trampoline(ctx, ctx->trampoline);
ctx->x[0] = (real == 0 ? 0 : 20000);
}
`);
globalThis.keep_getuid_cmodule = cm; // hook 存活期间必须保留引用
var getuid = Module.findExportByName("libc.so", "getuid");
var trampoline = hookNative(getuid, cm.on_getuid);hookNative(target, callbackPtr, userData?, mode?) 是 replace 语义:原函数不会自动执行。需要原函数时,在 C callback 里调用 hook_invoke_trampoline(ctx, ctx->trampoline);不需要原函数时直接改 ctx->x[0]。
…attachNative(target, { onEnter?, onLeave?, data?, mode? }) 是 attach 语义:hook engine 会自动执行原函数。只提供 onEnter 时走 tail-jump 快路径,不保留 leave 状态;提供 onLeave 时才会在原函数返回后进入 leave callback。onEnter 和 onLeave 的 C 函数签名相同:
void callback(HookContext *ctx, void *user_data);两者区别只在时机和 ctx 内容:
| 阶段 | ctx->x[0..] 含义 |
原函数 |
|---|---|---|
onEnter |
入参寄存器,可改参数 | 返回后自动执行 |
onLeave |
x0 是返回值,可改返回值 |
已经执行完 |
如果安装了 onLeave,onEnter 可用 ctx->intercept_leave = 0 跳过本次 leave;没有安装 onLeave 时设置这个字段没有意义。
CModule 默认注入这些头:stdint.h, stddef.h, stdbool.h, string.h, rfhook.h。rfhook.h 暴露 HookContext、RfHookCallback 和 hook_invoke_trampoline():
typedef struct {
uint64_t x[31];
uint64_t sp;
uint64_t pc;
uint64_t nzcv;
void *trampoline;
uint64_t d[8];
uint64_t intercept_leave;
} HookContext;
typedef void (*RfHookCallback)(HookContext *ctx, void *user_data);
uint64_t hook_invoke_trampoline(HookContext *ctx, void *trampoline);也可以把 JS 侧找到的 native s
暂无开放 Issues,或尚未同步最近议题。