Skip to content

kkkbbb/rustFrida

Repository files navigation

rustFrida

ARM64 Android 动态插桩框架。

环境要求

  • Android NDK 25+(默认路径 ~/Android/Sdk/ndk/
  • Rust toolchain + aarch64-linux-android target
  • Python 3(构建 loader shellcode)
  • .cargo/config.toml 已配置交叉编译(仓库自带)

构建

最终产物 rustfrida 通过 include_bytes! 内嵌了 loader shellcode 和 agent SO,有严格的构建顺序

loader shellcode  ──┐
                    ├──→  rustfrida (主程序)
agent (libagent.so) ┘

1. 构建 loader shellcode(bootstrapper + rustfrida-loader)

python3 build_helpers.py
# 输出:
#   loader/build/bootstrapper.bin
#   loader/build/rustfrida-loader.bin

loader 是 bare-metal ARM64 shellcode,被 rustfrida 通过 include_bytes! 嵌入。修改 loader C 代码后需重新运行此步。

2. 构建 agent(libagent.so)

cargo build -p agent --release
# 输出: target/aarch64-linux-android/release/libagent.so

agent 是注入到目标进程的动态库,包含 hook 引擎、QuickJS、Java hook 等。必须先于 rustfrida 构建,因为 rustfrida 通过 include_bytes! 嵌入 agent SO。

3. 构建 rustfrida(主程序)

cargo build -p rust_frida --release
# 输出: target/aarch64-linux-android/release/rustfrida

rustfrida 内嵌了 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 SO

eBPF 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> --verbose

REPL 命令

jsinit              # 初始化 JS 引擎
jseval <expr>       # 求值表达式
loadjs <script>     # 执行脚本
jsrepl              # 交互式 REPL(Tab 补全)
exit                # 退出

HTTP RPC 远程调用

脚本里用 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

JS 侧注册

// 整体替换
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 请求立刻生效。

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-safeJSON.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{}

JS API 参考

全局对象一览

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
}

Native Hook

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"));

NativeFunction(任意签名调用)

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。

Interceptor(Frida 兼容双阶段)

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)

Stealth 模式

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 速查

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

Java Hook

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;
});

Java.use 对象操作

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]);

字段访问(Frida 兼容 .value 模式)

字段通过 .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 字段

Java.ready

Spawn 模式下 app ClassLoader 未就绪,用 Java.ready 延迟执行。PID 注入模式下立即执行。

Java.choose 枚举存活实例(Frida 兼容)

扫描 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 自动降级为堆暴力扫描。

ClassLoader 控制

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() 里调。

Stealth 模式(Java hook)

Java.setStealth(0);  // Normal: mprotect 直写
Java.setStealth(1);  // WxShadow: shadow 页,CRC 校验不可见
Java.setStealth(2);  // Recomp: 代码页重编译
Java.getStealth();   // 查询当前模式 (0/1/2)

须在 Java.use().impl 之前设置。

Deopt API

Java.deopt();                  // 清空 JIT 缓存(InvalidateAllMethods)
Java.deoptimizeBootImage();    // boot image AOT 降级为 interpreter (API >= 26)
Java.deoptimizeEverything();   // 全局强制解释执行
Java.deoptimizeMethod("com.example.Test", "foo", "(I)V");  // 单方法降级

手动调用的工具函数,hook 流程不自动使用。

类型 Marshal 规则(Java ↔ JS 自动转换)

Hook 回调的 arguments$orig() / Class.method() 返回值、字段 .value 读写、Java.chooseonMatch 参数都走同一套 marshal 规则。

Java → JS(参数 / 返回值 / 字段读)

自动转换为原生 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 unboxedInteger / 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
  • 其他任意对象类型:用户类、ContextActivityFile 等一律 wrapper,通过 .method() / .field.value 链式访问。

$new 强制 wrapper 特例Java.use("java.lang.String").$new("hi") 即使构造出 String 也保留为 wrapper(便于链式 .length() / .charAt())——这是唯一跳过 String → JS string 自动转换的场景。

JS → Java($orig(args) / Class.method(args) / 字段写 / $new(args)

按目标参数的 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 ArrayNewXxxArray + 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 速查

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 API

Jni.addr("RegisterNatives")       // → NativePointer
Jni.FindClass                     // 属性直接取地址
Jni.find("FindClass")             // → { name, index, address }
Jni.table                         // 整张 JNI 函数表
Jni.addr(envPtr, "FindClass")     // 指定 JNIEnv

Jni.env / Jni.structs

Jni.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 速查

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[]

实战:监控 RegisterNatives

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);

Memory

双风格 Frida 兼容Memory.readXxx(addr)addr.readXxx(),所有 read/write 方法同时挂在 MemoryNativePointer.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

约束

  • 无效地址抛 RangeErrorreadCString 超过 4096B 抛
  • Memory.alloc 是 RWX 堆内存,GC 时自动释放;勿 munmap
  • 写入代码后必须 Memory.flushCodeCache 刷 I-cache
  • writeXxx 不会自动 mprotect;只读段写入抛错,需先 Memory.protect

Memory.protect / writeBytes / writest

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+4ADR/ADRP/BL/LDR literal/CBZ/TBZ/B.cond 自动 relocate;patch 内部分支 ≤64 条指令有效;同地址重装需先 unhook

Module

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 或无文件支撑的合成模块返回空数组。

Module.load — 运行时加载 SO

走 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。

ptr / NativePointer

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

console.log(...) / console.info(...) / console.warn(...) / console.error(...) / console.debug(...)

QBDI Trace

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 = vthis.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

免责声明

本项目仅供安全研究、逆向工程学习和授权测试用途。使用者应确保在合法授权范围内使用本工具,遵守所在地区的法律法规。作者不对任何滥用、非法使用或由此造成的损失承担责任。使用本项目即表示您同意自行承担所有风险。

About

xxxxx

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors