ARM64 Android 动态插桩框架。
- Android NDK 25+(默认路径
~/Android/Sdk/ndk/) - Rust toolchain +
aarch64-linux-androidtarget - Python 3(构建 loader shellcode)
.cargo/config.toml已配置交叉编译(仓库自带)
最终产物 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 独立二进制adb push target/aarch64-linux-android/release/rustfrida /data/local/tmp/
# PID 注入
./rustfrida --pid <pid>
./rustfrida --pid <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 <pid> --verbosejsinit # 初始化 JS 引擎
jseval <expr> # 求值表达式
loadjs <script> # 执行脚本
jsrepl # 交互式 REPL(Tab 补全)
exit # 退出
脚本里用 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 = {
ping: function() { return "pong"; },
add: function(a, b) { return a + b; },
echo: function(obj) { return { received: obj, ts: Date.now() }; },
// 读取当前 App 的 package name + label
getAppName: function() {
var ActivityThread = Java.use("android.app.ActivityThread");
var app = ActivityThread.currentApplication();
var ctx = app.getApplicationContext();
var pm = ctx.getPackageManager();
return {
packageName: String(ctx.getPackageName()),
label: String(pm.getApplicationLabel(ctx.getApplicationInfo())),
};
}
};
// 或者单独追加
rpc.export('version', function() { return "1.0.0"; });rpc.exports 就是个普通 JS 对象,现场 lookup,不需要向 host 注册方法列表——你可以任意时刻增删改,下一次 HTTP 请求立刻生效。
| 方法 | 路径 | Body | 说明 |
|---|---|---|---|
GET |
/ / /health |
— | 健康检查 |
GET |
/sessions |
— | 列出所有 session(id/pid/label/status) |
POST |
/rpc/<session>/<method> |
JSON 数组 | 调用 rpc.exports[method].apply(null, args);空 body 等价 [] |
<session> 在 legacy 模式下固定为 0,在 --server 模式下对应 list 命令显示的 id。
# 简单调用
curl -X POST http://127.0.0.1:9191/rpc/0/ping
# → {"ok":true,"result":"pong"}
# 位置参数(JSON 数组)
curl -X POST http://127.0.0.1:9191/rpc/0/add -d '[3,4]'
# → {"ok":true,"result":7}
# 对象参数
curl -X POST http://127.0.0.1:9191/rpc/0/echo -d '[{"foo":1,"bar":"hi"}]'
# → {"ok":true,"result":{"received":{"foo":1,"bar":"hi"},"ts":1775806588866}}
# Java 集成
curl -X POST http://127.0.0.1:9191/rpc/0/getAppName
# → {"ok":true,"result":{"packageName":"com.android.settings","label":"设置"}}
# 列出 session
curl http://127.0.0.1:9191/sessions
# → [{"id":0,"pid":1234,"label":"PID:1234","status":"connected"}]成功响应统一是 {"ok":true,"result":<value>};失败是 {"ok":false,"error":"<msg>"},HTTP 状态码 400(参数错)/404(session/method 不存在)/503(session 未连接)/500(JS 异常或超时)。
- 返回值必须 JSON-safe:
JSON.stringify在 JS 侧执行,函数/循环引用/undefined会被跳过。直接return一个 Java wrapper 只会得到指针字面量——请手动String(obj.method())或构造 plain object。 - 并发串行化:同一 session 内 HTTP 请求排队执行;跨 session 完全并行。
- 超时 30 秒:超时返回
{"ok":false,"error":"rpc call timed out"}。长耗时任务请改用轮询接口。 - 仅同步:不支持
async/ Promise——Promise 会被JSON.stringify成{}。
console, ptr(), Memory, Module, Interceptor, hook(), unhook(), callNative(), qbdi, Java, Jni
| 类型名 | 实际含义 |
|---|---|
AddressLike |
NativePointer | number | bigint | "0x..." |
NativePointer |
ptr() 创建的指针对象 |
JavaObjectProxy |
Java.use() / Java hook 中返回的 Java 对象代理 |
type ModuleInfo = {
name: string; base: NativePointer; size: number; path: string
}
// Native / Java hook 回调都是 Frida 风格:arguments = 参数,this = 上下文载体
type NativeHookThis = {
x0 ~ x30: bigint // ARM64 通用寄存器(读/写)
sp: bigint
pc: bigint
trampoline: bigint
orig(...args: any[]): bigint // 调原函数;不传参用当前寄存器,传参按顺序覆盖 x0-xN
}
// native hook 写法:
// hook(addr, function(a, b, c) { // arguments[0..7] = x0..x7(BigInt)
// this.x0 = ptr("0x1234"); // 改寄存器
// return this.orig(); // 调原函数
// });
type JavaInstanceThis = JavaObjectProxy & {
// 继承 JavaObjectProxy: 字段 this.field.value / 方法 this.method(args) / this.$className / this.__jptr
$orig(...args: any[]): any // 调原方法,不传参用原始参数
}
type JavaStaticThis = {
$orig(...args: any[]): any
$className: string
$static: true
}
// hook 写法:
// Cls.method.impl = function(a, b, c) { // arguments = Java 参数(对象自动 Proxy)
// this.$className // 始终可读
// this.field.value // 实例方法: 直接读字段
// return this.$orig(a, b, c) // 调原方法
// }
// Interceptor.attach 双阶段:args 是 NativePointer 代理(args[0] = x0),
// retval 支持 .replace() / .toInt32();this 在 onEnter/onLeave 之间共享
type InterceptorArgs = {
[i: number]: NativePointer // args[0..30] ⇄ ctx.x0..x30(读/写)
}
type InterceptorRetval = NativePointer & {
replace(v: AddressLike): void // 改返回值
toInt32(): number
toUInt32(): number
}
type InterceptorThis = {
x0 ~ x30: bigint; sp: bigint; pc: bigint
lr: bigint; returnAddress: bigint
// + 用户自定义字段,onEnter/onLeave 跨阶段共享(Frida 兼容)
}
type InvocationListener = { detach(): boolean }
type JniEntry = { name: string; index: number; address: NativePointer }
type JNINativeMethodInfo = {
address: NativePointer; namePtr: NativePointer; sigPtr: NativePointer
fnPtr: NativePointer; name: string | null; sig: string | null
}Frida 风格:arguments = x0..x7(前 8 个整型参数,BigInt),this = register 上下文(含 x0-x30 / sp / pc / orig)。
// 基本 hook — 透传
hook(Module.findExportByName("libc.so", "open"), function(path, flags) {
console.log("open:", Memory.readCString(ptr(path)), "flags=" + flags);
return this.orig();
});
// 修改返回值(直接 return 覆盖)
hook(Module.findExportByName("libc.so", "getpid"), function() {
this.orig();
return 12345; // 调用方拿到 12345
});
// 修改参数 — 通过 this.xN
hook(target, function(a, b) {
this.x0 = ptr("0x1234"); // 改第一个参数
this.x1 = 100; // 改第二个参数
return this.orig(); // 用修改后的参数调原函数
});
// 修改参数 — 通过 orig() 传参(按顺序覆盖 x0-xN)
hook(target, function() {
return this.orig(ptr("0x1234"), 100);
});
// 不 return 也行 — this.x0 赋值会同步回 C 层
hook(Module.findExportByName("libc.so", "getuid"), function() {
this.orig();
this.x0 = 77777; // 调用方拿到 77777
});
// 移除 hook
unhook(Module.findExportByName("libc.so", "open"));
// 直接调用 native 函数(最多 6 个参数,走 x0-x5)
var pid = callNative(Module.findExportByName("libc.so", "getpid"));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。
Frida 原生语法。hook() 是 replace 单阶段;Interceptor.attach 自动执行原函数并提供 onEnter / onLeave 双阶段拦截,this 在两阶段之间共享。
// 双阶段 attach: onEnter 前置 + 自动调原函数 + onLeave 后置
var listener = Interceptor.attach(Module.findExportByName("libc.so", "open"), {
onEnter(args) {
// args[0..30] 是 NativePointer 代理,args[N] = value 会写回 xN
this.path = args[0].readCString();
this.t0 = Date.now();
},
onLeave(retval) {
// retval 是 NativePointer,.replace(v) 改返回值
console.log("open(" + this.path + ") = " + retval.toInt32()
+ " took " + (Date.now() - this.t0) + "ms");
if (retval.toInt32() < 0) retval.replace(0);
}
});
listener.detach();
// 仅 onEnter — 改参数后让原函数自己跑(C 侧走 tail-jump 快路径,无栈帧残留)
Interceptor.attach(target, {
onEnter(args) { args[1] = ptr(100); }
});
// Interceptor.replace — 完全替换(等价于 hook(),不跑原函数)
Interceptor.replace(Module.findExportByName("libc.so", "getpid"), function() {
return 1234;
});
// 清理:单个 / 全部
listener.detach();
Interceptor.detachAll();
Interceptor.flush(); // no-op,兼容脚本第三参数可选 stealth 模式(同 hook()):Interceptor.attach(target, cbs, Hook.WXSHADOW)。
hook(target, callback, Hook.NORMAL) // 0: mprotect 直写(默认)
hook(target, callback, Hook.WXSHADOW) // 1: 内核 shadow 页,/proc/mem 不可见
hook(target, callback, Hook.RECOMP) // 2: 代码页重编译,仅 4B patch
hook(target, callback, 1) // 数字也行
hook(target, callback, true) // true = WXSHADOW| API | 参数 | 返回 |
|---|---|---|
hook(target, callback, stealth?) |
AddressLike, Function, number? |
boolean |
unhook(target) |
AddressLike |
boolean |
Interceptor.attach(target, {onEnter?, onLeave?}, stealth?) |
AddressLike, Object, number? |
InvocationListener |
Interceptor.replace(target, replacement, stealth?) |
AddressLike, Function, number? |
boolean |
Interceptor.detachAll() |
— | undefined |
listener.detach() |
— | boolean |
callNative(func, ...args) |
AddressLike, ...AddressLike (最多6个) |
number | bigint |
new NativeFunction(addr, retType, argTypes) |
AddressLike, string, string[] |
Function (可调用,任意签名) |
diagAllocNear(addr) |
AddressLike |
undefined |
Frida 风格:this = 实例(静态方法时为 class 载体),arguments = Java 参数。
Java.ready(function() {
var Activity = Java.use("android.app.Activity");
// hook 实例方法
Activity.onResume.impl = function() {
console.log("onResume:", this.$className); // this = 实例 Proxy
return this.$orig(); // 调原方法
};
// hook 构造函数(参数走 arguments)
var MyClass = Java.use("com.example.MyClass");
MyClass.$init.impl = function(a, b) {
console.log("new MyClass, arg0 =", a);
return this.$orig(a, b);
};
// 修改参数传给原方法
MyClass.test.impl = function(arg) {
return this.$orig("patched_arg");
};
// 指定 overload(Java 类型名或 JNI 签名都行)
MyClass.foo.overload("int", "java.lang.String").impl = function(i, s) {
return this.$orig(i, s);
};
// 静态方法:this 没有实例 Proxy,但 $orig / $className / $static 可用
Java.use("android.util.Log").i
.overload("java.lang.String", "java.lang.String").impl = function(tag, msg) {
console.log("[static]", this.$className, this.$static, tag, msg);
return this.$orig(tag, msg);
};
// 直接返回值覆盖(不调 $orig)
MyClass.getCount.impl = function() { return 42; };
// 移除 hook
Activity.onResume.impl = null;
});var JString = Java.use("java.lang.String");
var s = JString.$new("hello"); // 创建对象
console.log(s.length()); // 调实例方法
console.log(s.$className); // 类名
var Process = Java.use("android.os.Process");
console.log(Process.myPid()); // 调静态方法
// $new 重载(Frida 兼容 .overload(...))
var bytes = [65, 66, 67];
var s2 = JString.$new.overload("[B")(bytes); // String(byte[])
var s3 = JString.$new.overload("java.lang.String")("copy"); // String(String)
// 方法重载
var Arr = Java.use("java.util.Arrays");
Arr.toString.overload("[I")([1, 2, 3]); // 锁定 int[] 版本
Arr.asList.overload("[Ljava.lang.Object;")([1, "mix", obj]);字段通过 .value 读写,每次直接走 JNI,无缓存锁:
// 静态字段
var Build = Java.use("android.os.Build");
console.log(Build.MODEL.value); // 读: "Pixel 6"
Build.MODEL.value = "FakeModel"; // 写
// 实例字段(hook 回调中 / $new 创建的对象)
var Point = Java.use("android.graphics.Point");
var p = Point.$new(10, 20);
console.log(p.x.value, p.y.value); // 读: 10, 20
p.x.value = 100; // 写: JVM 同步更新
console.log(p.toString()); // "Point(100, 20)"
// hook 中访问 this 字段
Activity.onResume.impl = function() {
var name = this.mComponent.value; // 读实例字段
console.log("resuming:", name);
return this.$orig();
};字段/方法同名:Java 允许同名字段和方法共存。此时返回 hybrid——既可调用(方法)又有 .value(字段):
var map = HashMap.$new();
map.size(); // 调用 size() 方法
map.size.value; // 读取 size 字段Spawn 模式下 app ClassLoader 未就绪,用 Java.ready 延迟执行。PID 注入模式下立即执行。
扫描 ART 堆,把目标类的所有存活实例交给 onMatch:
Java.choose("android.app.Activity", {
onMatch: function(instance) {
console.log(instance.$className, "=>", instance.toString());
// return "stop"; // 提前终止
},
onComplete: function() { console.log("done"); },
subtypes: true, // 包含子类(rustFrida 扩展)
maxCount: 1000 // 最多枚举数量,默认 16384;0 = 不限
});
// 第三参等价 subtypes(位置参数形式)
Java.choose("java.util.List", { onMatch: fn }, true);生命周期:传给 onMatch 的 wrapper 仅在 onMatch 执行期间有效。函数返回后 __jptr 被置 0。若要跨回调保留实例,请在 onMatch 内调 String(obj.method()) 拷字段,或自行 NewGlobalRef。
后端:Android ≤13 走 VMDebug.getInstancesOfClasses;API 36 自动降级为堆暴力扫描。
var loaders = Java.classLoaders(); // → 数组: app + boot + system
Java.setClassLoader(loaders[0]); // 切换 Java.use() 查找上下文
var MyCls = Java.findClassWithLoader(loaders[0], "com.example.MyClass");loader 参数接受 loader 对象、{__jptr} wrapper 或 NativePointer。Spawn 模式下 app loader 就绪前 Java.classLoaders() 可能只返回 boot loader,应在 Java.ready() 里调。
Java.setStealth(0); // Normal: mprotect 直写
Java.setStealth(1); // WxShadow: shadow 页,CRC 校验不可见
Java.setStealth(2); // Recomp: 代码页重编译
Java.getStealth(); // 查询当前模式 (0/1/2)须在 Java.use().impl 之前设置。
Java.deopt(); // 清空 JIT 缓存(InvalidateAllMethods)
Java.deoptimizeBootImage(); // boot image AOT 降级为 interpreter (API >= 26)
Java.deoptimizeEverything(); // 全局强制解释执行
Java.deoptimizeMethod("com.example.Test", "foo", "(I)V"); // 单方法降级手动调用的工具函数,hook 流程不自动使用。
Hook 回调的 arguments、$orig() / Class.method() 返回值、字段 .value 读写、Java.choose 的 onMatch 参数都走同一套 marshal 规则。
自动转换为原生 JS 值:
| Java 类型 | JNI 签名 | JS 值 | 说明 |
|---|---|---|---|
boolean |
Z |
boolean |
|
byte |
B |
number |
有符号 i8 |
char |
C |
string |
长度为 1 的字符串 |
short |
S |
number |
i16 |
int |
I |
number |
i32 |
long |
J |
BigInt |
u64 |
float |
F |
number |
|
double |
D |
number |
|
java.lang.String |
Ljava/lang/String; |
string |
走 GetStringUTFChars |
null |
— | null |
|
Java 原始类型数组 T[](T 为 Z/B/C/S/I/J/F/D) |
[T |
Array of 对应 JS 值 |
一次 GetXxxArrayRegion 批量拷贝,无装箱 |
Java 对象数组 T[] |
[LT; |
Array of wrapper(或 string 若 T=String) |
逐个 GetObjectArrayElement |
Java 嵌套数组 [[... |
[[X |
Array of Array(递归 marshal) |
深度不限 |
保留为 Java wrapper {__jptr, __jclass}(不自动转换,需手动处理):
- 装箱类型 NOT unboxed:
Integer/Long/Float/Double/Boolean/Byte/Short/Character全部返回 wrapper,不会自动变成 JS number/boolean。需要原始值手动转:var n = boxed.intValue(); // Integer → int var d = boxed.doubleValue(); // Double → number var s = String(boxed); // 走 toString
- 容器不展开:
List/Map/Set/ArrayList/HashMap等保留 wrapper,手动遍历:var list = obj.getList(); for (var i = 0; i < list.size(); i++) { var item = list.get(i); // 仍是 wrapper(除非是 String) } var keys = map.keySet().toArray(); // → JS Array of wrappers
- 其他任意对象类型:用户类、
Context、Activity、File等一律 wrapper,通过.method()/.field.value链式访问。
$new 强制 wrapper 特例:Java.use("java.lang.String").$new("hi") 即使构造出 String 也保留为 wrapper(便于链式 .length() / .charAt())——这是唯一跳过 String → JS string 自动转换的场景。
按目标参数的 JNI 签名 marshal:
| 目标签名 | 接受的 JS 值 |
|---|---|
Z |
boolean / number(非零即 true) |
B / S / I / J |
number / BigInt |
C |
string(取首字符)/ number |
F / D |
number |
Ljava/lang/String; 或任意 L...; 场景下的 JS string |
→ NewStringUTF |
任意 L...;(已是 Java 对象) |
{__jptr} wrapper / Proxy → 提取原始 jobject 指针 |
装箱类型 Ljava/lang/Integer; 等 |
JS number/boolean/bigint 走 autobox(JNI Xxx.valueOf()) |
[B / [Z / [C / [S / [I / [J / [F / [D |
JS Array → NewXxxArray + SetXxxArrayRegion 批量填 |
[Ljava/lang/String; |
JS Array of string → 逐个 NewStringUTF + SetObjectArrayElement |
[Lxxx; 任意引用数组 |
每个元素按 Lxxx; 递归 marshal(string / Proxy __jptr / autobox) |
[[X / [[Lxxx; 嵌套数组 |
递归进入 [X 分支创建内层 Java 数组 |
Ljava/lang/Object; / Ljava/io/Serializable; + JS Array |
自动降级 Object[](元素按 Ljava/lang/Object; 再 marshal) |
| 任意类型 | null / undefined → JNI null (0) |
autobox 规则:目标签名精确匹配时按目标类型装箱(Ljava/lang/Long; → Long.valueOf(J));无精确签名时按 JS 值推断 —— 整数 fit i32 → Integer,否则 → Double;boolean → Boolean。
多 overload 自动消歧(数组按元素范围打分):
void foo(byte[] b)
void foo(int[] i)
void foo(long[] l)| JS 输入 | [B 分 |
[S 分 |
[I 分 |
[J 分 |
选中 |
|---|---|---|---|---|---|
[1, 2, 3](都在 byte 范围) |
10 | 9 | 8 | 7 | byte[] |
[1, 200, 3](溢出 byte,在 short) |
-1 | 9 | 8 | 7 | short[] |
[1, 100000](溢出 short,在 int) |
-1 | -1 | 8 | 7 | int[] |
[5000000000](溢出 int) |
-1 | -1 | -1 | 7 | long[] |
[1n, 2n](全 BigInt) |
-1 | -1 | -1 | 10 | long[] |
[true, false] |
-1 | -1 | -1 | -1 | boolean[] |
[1.5, 2.5] |
-1 | -1 | -1 | -1 | float[] / double[] |
手动覆写用 .overload(sig):
obj.foo.overload("[I")([1, 2, 3]); // 强制 int[](否则自动选 byte[])
obj.foo.overload("[B")([1, 200, 3]); // 强制 byte[],200 按位截断为 -56常见陷阱:
- 传普通 JS object(非 wrapper、无
__jptr)给非数组L...;参数会 marshal 成 0 → Java 侧 NPE。 - 传
undefined等同null,别依赖默认行为——显式写null。 Map.put(Object, Object)传number会被 autobox 成Integer/Double,取出来仍是 wrapper,要.intValue()才能拿回 JS number。- JS string 会为所有
L...;目标类型创建java.lang.String(即使签名是Ljava/lang/Object;),不会抛类型错误。 - 强制
.overload("[B")传入越界元素(如 200)按as i8按位截断,不报错(和 Frida 一致)。
| API | 参数 | 返回 |
|---|---|---|
Java.use(className) |
string |
JavaClassWrapper |
Class.$new(...args) |
任意 | JavaObjectProxy |
Class.method.impl = fn |
function(...args) { this.$orig(...) }(this = 实例/static 载体) |
setter |
Class.method.impl = null |
— | setter |
Class.method.overload(...types) |
string... |
MethodWrapper |
Java.ready(fn) |
() => void |
void |
Java.choose(cls, callbacks, subtypes?) |
string, {onMatch,onComplete?,subtypes?,maxCount?}, bool? |
void |
Java.classLoaders() |
— | LoaderInfo[] |
Java.findClassWithLoader(loader, cls) |
Loader, string |
JavaClassWrapper |
Java.setClassLoader(loader) |
Loader |
— |
Java.deopt() |
— | boolean |
Java.deoptimizeBootImage() |
— | boolean |
Java.deoptimizeEverything() |
— | boolean |
Java.deoptimizeMethod(cls, method, sig) |
string, string, string |
boolean |
Java.setStealth(mode) |
number (0/1/2) |
— |
Java.getStealth() |
— | number |
obj.field.value |
— | any (读字段) |
obj.field.value = x |
— | — (写字段) |
Java.getField(objPtr, cls, field, sig) |
AddressLike, string, string, string |
any (低层 API) |
Jni.addr("RegisterNatives") // → NativePointer
Jni.FindClass // 属性直接取地址
Jni.find("FindClass") // → { name, index, address }
Jni.table // 整张 JNI 函数表
Jni.addr(envPtr, "FindClass") // 指定 JNIEnvJni.env.ptr // 当前线程 JNIEnv*
Jni.env.getClassName(jclass) // → "android.app.Activity"
Jni.env.getObjectClassName(jobject) // → 对象的类名
Jni.env.readJString(jstring) // → JS string
Jni.env.getObjectClass(obj) // → jclass
Jni.env.getSuperclass(clazz) // → jclass (Object 返 null)
Jni.env.isSameObject(a, b) // → boolean
Jni.env.isInstanceOf(obj, clazz) // → boolean
Jni.env.exceptionCheck() // → boolean
Jni.env.exceptionClear()
Jni.env.exceptionOccurred() // → jthrowable | null
// 构造/引用 (Rust 直路, 不走 callNative → dladdr, hook context 内安全)
Jni.env.findClass("java/lang/String") // → jclass | null
Jni.env.newStringUtf("hello") // → jstring | null
Jni.env.newLocalRef(obj) // → jobject | null
Jni.env.deleteLocalRef(obj) // → undefined
Jni.structs.JNINativeMethod.readArray(addr, count) // → JNINativeMethodInfo[]
Jni.structs.jvalue.readArray(addr, typesOrSig) // → any[]ref API 都接受:NativePointer / BigInt / 十六进制字符串 / {__jptr: ...} wrapper。所有方法都接受可选 env 首参:Jni.env.findClass(envPtr, "java/lang/String"),省略则走 ensure_jni_initialized 自动 attach 当前线程。所有 JNI 调用失败后异常被兜底 clear,不会串到下一次调用。
| API | 参数 | 返回 |
|---|---|---|
Jni.addr(name) |
string |
NativePointer |
Jni.addr(env, name) |
AddressLike, string |
NativePointer |
Jni.find(name) |
string |
JniEntry |
Jni.entries() |
— | JniEntry[] |
Jni.table |
— | Record<string, JniEntry> |
Jni.env.getClassName(clazz) |
AddressLike |
string | null |
Jni.env.readJString(jstr) |
AddressLike |
string | null |
Jni.env.findClass(name) |
string |
NativePointer | null |
Jni.env.newStringUtf(str) |
string |
NativePointer | null |
Jni.env.newLocalRef(obj) |
AddressLike |
NativePointer | null |
Jni.env.deleteLocalRef(obj) |
AddressLike |
true |
Jni.structs.JNINativeMethod.readArray(addr, count) |
AddressLike, number |
JNINativeMethodInfo[] |
hook(Jni.addr("RegisterNatives"), function(env, clazz, methods_ptr, count) {
var cls = Jni.env.getClassName(clazz);
var n = Number(count);
console.log(cls + " (" + n + " methods)");
var methods = Jni.structs.JNINativeMethod.readArray(ptr(methods_ptr), n);
for (var i = 0; i < methods.length; i++) {
var m = methods[i];
var mod = Module.findByAddress(m.fnPtr);
console.log(" " + m.name + " " + m.sig + " → " + mod.name + "+" + m.fnPtr.sub(mod.base));
}
return this.orig();
}, 1);双风格 Frida 兼容:Memory.readXxx(addr) ≡ addr.readXxx(),所有 read/write 方法同时挂在 Memory 和 NativePointer.prototype 上。
// Memory.* 风格
var pid = Memory.readU32(ptr("0x7f1234"));
Memory.writeU64(dst, 0xdeadbeefn);
var cls = Memory.readCString(ptr(this.x1));
// ptr.* 风格(推荐,支持链式)
var p = ptr("0x7f1234");
p.readU32();
p.writeU64(0xdeadbeefn);
p.add(8).readPointer().readCString(); // 解指针再读字符串
p.add(0x10).readByteArray(32); // → ArrayBuffer
// 写入代码后刷 I-cache
var code = Memory.alloc(16);
code.writeU32(0xd65f03c0); // ret
Memory.flushCodeCache(code, 16);| API | 参数 | 返回 |
|---|---|---|
| 读 | ||
Memory.readU8/U16(addr) / p.readU8/U16() |
AddressLike |
number |
Memory.readU32/U64(addr) / p.readU32/U64() |
AddressLike |
bigint |
Memory.readPointer(addr) / p.readPointer() |
AddressLike |
NativePointer |
Memory.readCString(addr) / p.readCString() |
AddressLike |
string (最多 4096B) |
Memory.readUtf8String(addr) / p.readUtf8String() |
AddressLike |
string |
Memory.readByteArray(addr, len) / p.readByteArray(len) |
AddressLike, number |
ArrayBuffer (≤1GB) |
| 写 | ||
Memory.writeU8/U16/U32(addr, v) / p.writeU8/U16/U32(v) |
AddressLike, number |
undefined |
Memory.writeU64(addr, v) / p.writeU64(v) |
AddressLike, bigint |
undefined |
Memory.writePointer(addr, v) / p.writePointer(v) |
AddressLike, AddressLike |
undefined |
Memory.writeBytes(addr, bytes, stealth?) / p.writeBytes(bytes, stealth?) |
AddressLike, ArrayBuffer|TypedArray|number[], 0|1 |
undefined |
Memory.writest(addr, bytes) / p.writest(bytes) |
AddressLike, 4B 倍数指令字节 |
undefined |
| 分配 / 维护 | ||
Memory.alloc(size) |
number (≤ 256MB) |
NativePointer (RWX, 零初始化) |
Memory.allocUtf8String(s) |
string |
NativePointer (RWX,末尾 \0) |
Memory.flushCodeCache(addr, size) |
AddressLike, number |
undefined |
约束:
- 无效地址抛
RangeError;readCString超过 4096B 抛 Memory.alloc是 RWX 堆内存,GC 时自动释放;勿munmap- 写入代码后必须
Memory.flushCodeCache刷 I-cache writeXxx不会自动 mprotect;只读段写入抛错,需先Memory.protect
| API | 适用段 | read 可见 | 用途 |
|---|---|---|---|
Memory.protect(addr, size, "rwx") |
任意 | — | 改页权限(页级 mprotect) |
p.writeBytes(bytes, 0) 默认 |
可写段 | 可见 | 覆盖 N 字节(数据/结构体) |
p.writeBytes(bytes, 1) |
r-x | 不可见 | wxshadow 覆盖 N 字节(短 patch,单页内) |
p.writest(bytes) |
r-x | 不可见 | 1 条指令 → N 条指令替换(PC-rel 自动 relocate) |
unhook(addr) 统一清理 hook / writest / writeBytes(1) 留下的 patch。
var addr = Module.findExportByName("libc.so", "getpid");
// 隐身短 patch: getpid() → 42, readByteArray 仍看原字节
addr.writeBytes(new Uint8Array([0x40,0x05,0x80,0xd2, 0xc0,0x03,0x5f,0xd6]), 1);
// 指令级替换: 原第一条指令被这 3 条顶替, 原第二条及以后保留
addr.writest(new Uint8Array([
0x80,0x46,0x82,0x52, // MOVZ W0, #0x1234
0xa0,0x79,0xb5,0x72, // MOVK W0, #0xABCD, LSL #16
0xc0,0x03,0x5f,0xd6, // RET
]));
// 写数据段: 先开写权限
Memory.protect(dataAddr, 8, "rwx");
dataAddr.writeU64(0xdeadbeefn);
Memory.protect(dataAddr, 8, "r--");writest 细节:patch 不带 RET/B 时末尾自动 fall-through 到 addr+4;ADR/ADRP/BL/LDR literal/CBZ/TBZ/B.cond 自动 relocate;patch 内部分支 ≤64 条指令有效;同地址重装需先 unhook。
| API | 参数 | 返回 |
|---|---|---|
Module.findExportByName(module, symbol) |
string, string |
NativePointer | null |
Module.findBaseAddress(module) |
string |
NativePointer | null |
Module.findByAddress(addr) |
AddressLike |
ModuleInfo | null |
Module.enumerateModules() |
— | ModuleInfo[] |
Module.enumerateExports(name) |
string |
{type, name, address}[] |
Module.enumerateImports(name) |
string |
{type, name, slot, address}[] |
Module.enumerateSymbols(name) |
string |
{type, name, address, isGlobal, isDefined}[] |
Module.enumerateRanges(name, prot?) |
string, "rwx" 风格 |
{base, size, protection, file:{path}}[] |
Module.load(path, flags?) |
string, int? |
ModuleInfo / 抛异常 |
// 导出:defined + global/weak 符号
Module.enumerateExports("libc.so").slice(0, 3);
// [{type:"function", name:"__cxa_finalize", address:"0x7200f0e0a0"}, ...]
// 按内存权限过滤 (prot 里 '-' 是通配, "r-x" 会匹配 r-x 和 rwx)
Module.enumerateRanges("libc.so", "r-x");
// 外部引用符号 + PLT/GOT slot 地址
Module.enumerateImports("libart.so").filter(i => i.type === "function");枚举的来源是模块的磁盘 ELF;memfd 或无文件支撑的合成模块返回空数组。
走 unrestricted linker (__loader_dlopen),绕开 namespace 限制 + hide_soinfo 的 caller 解析问题。加载成功后从 /proc/self/maps 解析 {name, base, size, path} 返回;失败抛带 dlerror 原始消息的 InternalError。
// 短名:走 linker 搜索路径
var m = Module.load("libz.so");
// { name: "libz.so", base: 0x7062dec000, size: 110592, path: "/vendor/lib64/libz.so" }
// 绝对路径
Module.load("/system/lib64/libsqlite.so");
// 自定义 flags(默认 RTLD_NOW = 2;RTLD_LAZY = 1)
Module.load("/data/local/tmp/mylib.so", 1);
// 错误处理
try {
Module.load("/does/not/exist.so");
} catch (e) {
console.log(e.message);
// → "Module.load: dlopen('/does/not/exist.so') failed: library \"...\" not found"
}
// 加载后立刻查符号
var m = Module.load("libcustom.so");
var addr = Module.findExportByName(m.name, "my_func");注意:
- 若模块被
hide_soinfo隐藏或通过 memfd 加载,/proc/self/maps可能查不到,此时返回{name, path, base: <dlopen handle>, size: 0}作 fallback。 Module.load不会重复加载同一个 SO — linker 对已加载模块返回现有 handle。
var p = ptr("0x7f12345678"); // hex string / number / BigInt / NativePointer
p.add(0x100).sub(0x10); // 算术,返回新 NativePointer
p.toString(); // → "0x7f12345678"
p.toInt(); // → bigint (等价 toNumber)
// Frida 兼容读写(完整 API 见上面 Memory 章节)
p.readU32(); // 等价 Memory.readU32(p)
p.writeU64(0xdeadbeefn); // 自动 mprotect
p.readPointer().readCString(); // 链式解引用| API | 参数 | 返回 |
|---|---|---|
ptr(value) |
number | bigint | string | NativePointer |
NativePointer |
p.add(offset) / p.sub(offset) |
AddressLike |
NativePointer |
p.toString() / p.toJSON() |
— | string ("0x...") |
p.toNumber() / p.toInt() |
— | bigint |
p.readU8/U16/U32/U64/Pointer() |
— | number | bigint | NativePointer |
p.readCString() / p.readUtf8String() |
— | string |
p.readByteArray(len) |
number |
ArrayBuffer |
p.writeU8/U16/U32/U64/Pointer(val) |
值 | undefined |
p.writeBytes(bytes, stealth?) |
ArrayBuffer|TypedArray|number[], 0|1 |
undefined |
p.writest(bytes) |
ArrayBuffer|TypedArray|number[] (4B 倍数) |
undefined |
所有读写方法的语义、错误处理、i-cache 约束与 Memory.* 完全一致;writeBytes / writest 的行为见 Memory 章节的表格。
console.log(...) / console.info(...) / console.warn(...) / console.error(...) / console.debug(...)
| API | 参数 | 返回 |
|---|---|---|
qbdi.newVM() |
— | number |
qbdi.destroyVM(vm) |
number |
boolean |
qbdi.addInstrumentedModuleFromAddr(vm, addr) |
number, AddressLike |
boolean |
qbdi.addInstrumentedRange(vm, start, end) |
number, AddressLike, AddressLike |
boolean |
qbdi.removeInstrumentedRange(vm, start, end) |
number, AddressLike, AddressLike |
boolean |
qbdi.removeAllInstrumentedRanges(vm) |
number |
boolean |
qbdi.allocateVirtualStack(vm, size) |
number, number |
boolean |
qbdi.simulateCall(vm, retAddr, ...args) |
number, AddressLike, ...AddressLike |
boolean |
qbdi.call(vm, target, ...args) |
number, AddressLike, ...AddressLike |
NativePointer | null |
qbdi.run(vm, start, stop) |
number, AddressLike, AddressLike |
boolean |
qbdi.getGPR(vm, reg) |
number, number |
NativePointer |
qbdi.setGPR(vm, reg, value) |
number, number, AddressLike |
boolean |
qbdi.registerTraceCallbacks(vm, target, outDir?) |
number, AddressLike, string? |
boolean |
qbdi.unregisterTraceCallbacks(vm) |
number |
boolean |
qbdi.lastError() |
— | string |
常用寄存器常量:qbdi.REG_RETURN, qbdi.REG_SP, qbdi.REG_LR, qbdi.REG_PC
var vm = qbdi.newVM();
qbdi.addInstrumentedModuleFromAddr(vm, target);
qbdi.allocateVirtualStack(vm, 0x100000);
qbdi.simulateCall(vm, 0, arg0, arg1);
qbdi.registerTraceCallbacks(vm, target);
qbdi.run(vm, target, 0);
var ret = qbdi.getGPR(vm, qbdi.REG_RETURN);
qbdi.unregisterTraceCallbacks(vm);
qbdi.destroyVM(vm);Trace 文件默认输出到 /data/data/<package>/trace_bundle.pb,配合 qbdi-replay + IDA 插件回放。
- Native hook 回调签名:
function(a, b, c) { ... },arguments[0..7]= x0..x7 (BigInt)、this= register 上下文(this.x0..x30/this.sp/this.pc/this.orig());改参数this.x0 = v或this.orig(newArg0, newArg1);return value覆盖返回值 - Java hook 回调签名:
function(a, b, c) { ... },this= 实例(静态方法为 class 载体)、arguments= Java 参数、this.$orig(...)= 原方法;return value改返回值 - Java 字段访问必须用
.value:obj.field返回 FieldWrapper,obj.field.value才是真实值 Java.choose的 wrapper 仅在onMatch内有效,跨回调保留需要自己提取字段值- Spawn 模式下 Java hook 必须放在
Java.ready(fn)里(Java.classLoaders()/Java.choose同理) Java.setStealth()必须在Java.use().impl之前调用callNative()仅支持整数/指针参数(最多 6 个),需要浮点/任意签名用NativeFunction- 自修改代码后需
Memory.flushCodeCache(addr, size)清 I-cache
本项目仅供安全研究、逆向工程学习和授权测试用途。使用者应确保在合法授权范围内使用本工具,遵守所在地区的法律法规。作者不对任何滥用、非法使用或由此造成的损失承担责任。使用本项目即表示您同意自行承担所有风险。