quickJSAPI

quickjs-hook JS API

本文档描述的是当前 quickjs-hook runtime 里可直接使用的 JS API。文档只写当前分支已经实现的能力,不把原始 Frida 的完整 index.d.ts 直接当成事实。

1. 全局对象

当前脚本默认可见:

  1. console
  2. ptr()
  3. hexdump()
  4. Memory
  5. Module
  6. Process
  7. Thread
  8. hook() / unhook()
  9. Interceptor
  10. callNative()
  11. Java
  12. Jni
  13. send() / recv()
  14. qbdi
  • 仅在启用 qbdi feature 时存在

2. AddressLike

很多地址参数都接受 AddressLike

  1. NativePointer
  2. number
  3. bigint
  4. 十六进制字符串,如 0x7f12345678

2.1 hexdump()

支持 Frida 风格的全局 hexdump(target, options?)

target 当前支持:

  1. NativePointer
  2. ArrayBuffer

options 当前支持:

  1. address
    • 自定义显示地址,支持 NativePointer / number / bigint
  2. offset
  3. length
  4. header
  5. ansi

默认行为:

  1. 指针目标未传 length 时,默认 dump 256 字节
  2. header 默认 true
  3. ansi 默认 false
1
2
3
4
5
var openPtr = Module.getExportByName("libc.so", "open");
console.log(hexdump(openPtr, { length: 0x40 }));

var bytes = openPtr.readByteArray(0x20);
console.log(hexdump(bytes, { header: false }));

3. NativePointer

3.1 创建

1
2
3
var p1 = ptr(0x1234);
var p2 = ptr("0x7f12345678");
var p3 = ptr(0x7f12345678n);

3.2 指针运算

支持:

  1. add(offset)
  2. sub(offset)
  3. isNull()
  4. equals(other)
  5. compare(other)
  6. and(mask)
  7. or(mask)
  8. xor(mask)
  9. not()
  10. shl(bits)
  11. shr(bits)
1
2
3
4
var base = Module.getExportByName("libc.so", "open");
var next = base.add(4);
console.log(base.equals(next));
console.log(base.and(0xfffn).toString());

3.3 读写实例方法

支持:

  1. readU8()
  2. readU16()
  3. readU32()
  4. readU64()
  5. readPointer()
  6. readCString(maxLen?)
  7. readUtf8String(maxLen?)
  8. readByteArray(length)
  9. writeU8(value)
  10. writeU16(value)
  11. writeU32(value)
  12. writeU64(value)
  13. writePointer(value)
  14. writeUtf8String(value)
1
2
3
4
var buf = Memory.allocUtf8String("hello");
console.log(buf.readCString());
buf.writeUtf8String("world");
console.log(buf.readCString());

3.4 转换

支持:

  1. toString()
  2. toJSON()
  3. toNumber()
  4. toInt()

toNumber() / toInt() 当前返回 BigInt,用于保留 64 位地址精度。

4. Memory

4.1 读取

支持:

  1. Memory.readU8(addr)
  2. Memory.readU16(addr)
  3. Memory.readU32(addr)
  4. Memory.readU64(addr)
  5. Memory.readPointer(addr)
  6. Memory.readCString(addr, maxLen?)
  7. Memory.readUtf8String(addr, maxLen?)
  8. Memory.readByteArray(addr, length)

4.2 写入

支持:

  1. Memory.writeU8(addr, value)
  2. Memory.writeU16(addr, value)
  3. Memory.writeU32(addr, value)
  4. Memory.writeU64(addr, value)
  5. Memory.writePointer(addr, value)
  6. Memory.writeUtf8String(addr, value)

4.3 分配与复制

支持:

  1. Memory.alloc(size, options?)
  2. Memory.allocUtf8String(value)
  3. Memory.copy(dst, src, size)
  4. Memory.dup(addr, size)
  5. Memory.protect(addr, size, protection)
1
2
3
4
5
6
7
8
var buf = Memory.alloc(0x20);
Memory.writeU32(buf, 0x41414141);

var copy = Memory.dup(buf, 0x20);
console.log(copy.readU32());

var s = Memory.allocUtf8String("android_dlopen_ext");
console.log(s.readCString());

说明:

  1. alloc() / allocUtf8String() 返回的都是 NativePointer
  2. 当前实现支持基础所有权释放;不要依赖超复杂的生命周期语义。
  3. protect() 目前只提供底层 mprotect() 风格能力。
  4. 当前没有实现 Memory.scan()Memory.patchCode()

5. Module

支持:

  1. Module.findExportByName(moduleName, symbolName)
  2. Module.getExportByName(moduleName, symbolName)
  3. Module.findGlobalExportByName(symbolName)
  4. Module.findBaseAddress(moduleName)
  5. Module.findByAddress(addr)
  6. Module.enumerateModules()
1
2
3
4
5
var openPtr = Module.getExportByName("libc.so", "open");
var dlopenPtr = Module.findGlobalExportByName("android_dlopen_ext");

console.log(openPtr.toString());
console.log(Module.findByAddress(openPtr).name);

返回的模块对象结构:

1
2
3
4
5
6
{
name: "libc.so",
base: ptr("0x..."),
size: 123456,
path: "/apex/.../libc.so"
}

6. Process

当前实现的是 Frida 高频只读子集。

属性:

  1. Process.id
  2. Process.arch
  3. Process.platform
  4. Process.pointerSize
  5. Process.pageSize

方法:

  1. Process.findModuleByName(name)
  2. Process.getModuleByName(name)
  3. Process.findModuleByAddress(addr)
  4. Process.getModuleByAddress(addr)
  5. Process.enumerateModules()
  6. Process.myPid()
    • 兼容别名,等价于 Process.id
1
2
3
4
5
console.log("pid =", Process.id);
console.log("pid alias =", Process.myPid());

var libc = Process.getModuleByName("libc.so");
console.log(libc.base.toString());

6.1 Thread

当前只实现了最小可用子集:

  1. Thread.sleep(delay)

说明:

  1. delay 单位是秒,和 Frida 一致。
  2. 支持小数,例如 Thread.sleep(0.05) 表示睡眠 50 ms。
  3. 当前实现是阻塞当前线程,不包含 Thread.backtrace()
1
2
3
console.log("before");
Thread.sleep(0.05);
console.log("after");

7. Native Hook

当前有两套语义,必须区分:

  1. hook() / unhook()
    • replace 模式
  2. Interceptor.attach() / listener.detach() / Interceptor.detachAll()
    • attach 模式

7.1 hook()

1
2
3
4
hook(Module.getExportByName("libc.so", "getpid"), function (ctx) {
console.log("getpid called");
return 1234;
}, false);

说明:

  1. hook() 会接管目标函数的执行流。
  2. 是否调用原函数由 ctx.orig() 决定。
  3. unhook() 只对应 hook() 安装的 replace hook。

7.2 Interceptor.attach()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var target = Module.getExportByName("libdl.so", "android_dlopen_ext");

var listener = Interceptor.attach(target, {
onEnter: function (args) {
this.path = args[0].readCString();
console.log("dlopen:", this.path);
},
onLeave: function (retval) {
console.log("retval =", retval.toString());
}
}, true);

listener.detach();
// 或 Interceptor.detachAll();

说明:

  1. 当前 callback 继续沿用本项目的 ctx 形状。
  2. 本轮没有实现 Interceptor.replace() / revert() / flush()

7.3 callNative()

1
2
var getpidPtr = Module.getExportByName("libc.so", "getpid");
console.log(callNative(getpidPtr));

callNative() 适合简单整数参数/返回值的场景,不等价于 Frida 的 NativeFunction

8. Java

8.1 Java.perform()

Java.perform(fn) 是当前推荐主路径。现在已经对齐到 Frida 风格的 pending-loader 语义:

  1. ClassLoader 已就绪时立即执行
  2. 未就绪时排队,并通过 ActivityThread.currentApplication()handleBindApplication()getPackageInfo()LoadedApk.makeApplication() 等路径尽早拿到 app loader
1
2
3
Java.perform(function () {
console.log("Java is ready");
});

8.2 Java.ready()

Java.ready(fn) 仍然保留,行为与 Java.perform(fn) 一致。为了 Frida 脚本迁移,推荐新脚本优先写 Java.perform(fn)

8.3 Java.performNow()

Java.performNow(fn) 已可用。它会立即附着到 VM 并执行回调,同时尽力探测当前 app loader,但不保证 app loader 一定已经 ready。

1
2
3
4
Java.performNow(function () {
var ActivityThread = Java.use("android.app.ActivityThread");
console.log(ActivityThread.currentApplication());
});

8.4 其他 Frida 风格 Java helper

当前还支持这些主路径 helper:

  1. Java.classFactory.loader
  2. Java.cast(obj, Klass)
  3. Java.retain(obj)
  4. Java.array(type, elements)
  5. Java.isMainThread()

8.5 Java.use()

支持:

  1. Java.use(className)
  2. Class.$new(...)
  3. Class.method.overload(...)
  4. Class.method.overloads
  5. Class.method.implementation = fn
  6. Class.method.impl = fn
    • 兼容旧写法
1
2
3
4
5
6
7
8
Java.perform(function () {
var HashMap = Java.use("java.util.HashMap");

HashMap.put.implementation = function (key, value) {
console.log("key =", key);
return this.put(key, value);
};
});

指定 overload:

1
2
3
4
5
6
7
Java.perform(function () {
var Demo = Java.use("com.example.Demo");

Demo.test.overload("java.lang.String").implementation = function (value) {
return this.test("patched");
};
});

遍历 overload:

1
2
3
4
5
6
7
Java.perform(function () {
var Demo = Java.use("com.example.Demo");

Demo.test.overloads.forEach(function (ov) {
console.log("sig =", ov.toString ? ov.toString() : "[callable overload]");
});
});

.impl 路径下的 Java hook ctx 常见字段:

  1. thisObj
  2. args
  3. env
  4. orig(...)

说明:

  1. .implementation = function(arg0, arg1...) {} 走 Frida 主路径,回调参数就是 Java 实参,this.method(...) 会调用当前 hook 对应的原方法。
  2. .impl = function(ctx) {} 保留旧语义,ctx.orig(newArgs...) 仍支持带新参数调用原方法。
  3. Java hook 直接 return 即作为 Java 返回值。
  4. 当前已实现 Java.castJava.retainJava.performNowJava.arrayJava.classFactory.loaderJava.isMainThread()
  5. 当前仍然没有实现 Java.chooseJava.scheduleOnMainThread()Java.openClassFile()Java.registerClass()

9. Jni

Jni 仍然分两层:

  1. 底层 Rust 注册的 _xxx
  2. jni_boot.js 补上的高层包装

业务脚本优先使用:

  1. Jni.addr(...)
  2. Jni.find(...)
  3. Jni.entries(...)
  4. Jni.table
  5. Jni.helper
1
2
3
4
hook(Jni.addr("FindClass"), function (ctx) {
console.log("FindClass:", ptr(ctx.x1).readCString());
ctx.orig();
});

10. send / recv

当前实现支持基本 GumJS 风格消息接口:

  1. send(message, data?)
  2. recv(callback)
  3. recv(type, callback)
  4. recv(...).wait()
  5. recv(...).cancel()
1
2
3
4
5
send({ type: "hello", value: 1 });

recv("input", function (message, data) {
console.log(JSON.stringify(message));
}).wait();

说明:

  1. send/recv 依赖宿主的消息转发。
  2. 当前没有实现 Frida 风格定时器,因此不要假设 setTimeout() / setInterval() 可用。

11. qbdi

仅在构建时启用 qbdi feature 才可用。常见接口仍包括:

  1. newVM()
  2. destroyVM(vm)
  3. addInstrumentedRange(vm, start, end)
  4. addInstrumentedModule(vm, name)
  5. addInstrumentedModuleFromAddr(vm, addr)
  6. run(vm, start, stop)
  7. call(vm, target, ...args)
  8. getGPR(vm, reg)
  9. setGPR(vm, reg, value)
  10. lastError()
  11. shutdown()

12. 迁移建议

  1. Frida 里的 Interceptor.attach() 优先直接迁移到当前的 Interceptor.attach()
  2. Frida 里的 Java.perform() 直接迁移到当前的 Java.perform()
  3. Frida 里的 .implementation 可以直接用;旧文档里的 .impl 继续兼容。
  4. NativeFunction / NativeCallback 当前还没有,不要硬套。
  5. setTimeout() / setInterval() 当前还没有,不要把 GumJS 定时逻辑直接搬过来。
0:00 /0:00
暧昧合伙人
遗憾