☰
chromatic API 实战指南:Chromium/V8 广谱注入的 Frida 兼容脚本接口全解
2026/9/25 5:30:40 网站建设 项目流程
  • 逆向工程
  • 调试器

【免费下载链接】chromatic

Universal modifier for Chromium/V8 | 广谱注入 Chromium/V8 的通用修改器

项目地址:https://gitcode.com/gh_mirrors/be/chromatic
点击查看免费下载

chromatic 是一个广谱注入 Chromium/V8 的通用修改器,它向注入脚本暴露了一组与 Frida 脚本习惯高度兼容的全局 API:进程/模块枚举、内存读写与模式扫描、原生函数调用与回调、函数内联拦截(Interceptor)、指令反汇编与交叉引用查找、软/硬件断点、内存访问监控以及全局异常处理。本文基于 API 文档 完整梳理这些接口的签名、参数与用法,并结合 TypeScript 脚本层实现 与 C++ 绑定,解释每个 API 背后的调用链,帮助你在编写注入脚本时既能照抄可用的示例,又清楚每一步在底层是怎么执行的。

全局 API 的注册方式

从 src/core/typescript/src/main.ts 可以看到,chromatic 把所有 API 统一挂到globalThis上,形成一组 Frida 风格的全局对象:

  • 核心类:NativePointer、Int64、UInt64、NativeFunction、NativeCallback、CModule;
  • 命名空间单例:Memory、Process、Module、Instruction、Interceptor、ExceptionHandler、SoftwareBreakpoint、HardwareBreakpoint、MemoryAccessMonitor;
  • 脚本生命周期:Script(见 script-lifecycle.ts);
  • 工具函数:ptr、NULL、hexdump。

从源码结构看,JS 层是 TypeScript 薄封装(位于 src/core/typescript/src/),重活全部下放到 C++ 原生绑定(NativeMemory、NativeProcess、NativeInterceptor、NativeDisassembler等,对应 src/core/bindings/generated_bindings/ 生成的绑定层)。对脚本作者来说只需要记住一点:这些对象在脚本里直接可用,无需 import。

Process API:进程信息

Process提供进程级的元信息与枚举能力。

属性

属性返回值说明
Process.arch'arm64'或'x64'当前 CPU 架构
Process.platform'windows'、'linux'、'darwin'或'android'当前操作系统
Process.pointerSize4(32 位)或8(64 位)原生指针字节数
Process.pageSize字节数,通常 4096 或更大虚拟内存页面大小
const arch = Process.arch; // 'arm64' 或 'x64' const platform = Process.platform; // 'darwin' / 'linux' / ... const size = Process.pointerSize; // 8 const pageSize = Process.pageSize; // 4096

在 process.ts 中,这些属性直接透传到原生的NativeProcess(NP.architecture、NP.platform、NP.pointerSize、NP.pageSize),另有两个文档未列出的可用属性:Process.id(当前进程 PID)和Process.getCurrentThreadId()(当前线程的 OS 线程 ID),在需要线程相关判断的钩子逻辑里很有用。

方法

Process.enumerateModules()

枚举进程中加载的所有模块,返回Module[]:

const modules = Process.enumerateModules(); modules.forEach(m => { console.log(`${m.name}: ${m.base} (${m.size} bytes)`); });

每个模块对象包含name(模块名)、base(基址,NativePointer)、size(字节大小)、path(文件路径)。这与 module.ts 中Module类的四个字段一一对应。

Process.enumerateRanges(protection)

枚举具有指定保护属性的内存范围:

const ranges = Process.enumerateRanges('r--'); ranges.forEach(r => { console.log(`${r.base} - ${r.protection}`); });
  • 参数:protection—— 保护属性字符串,如'r--'、'rw-'、'r-x'。从 process.ts 的注释看,语义与 Frida 一致:某个字符表示“必须具备该权限”,-表示“不关心”,因此'r-x'会匹配所有只读可执行的范围。
  • 返回值:范围对象数组,每项含base(基址)、size、protection、filePath(若该范围来自映射文件)。
Process.findModuleByAddress(address)/findModuleByName(name)

根据地址或名称定位模块:

const addr = Module.findExportByName(null, 'malloc'); const mod = Process.findModuleByAddress(addr); // 模块对象或 null console.log(mod.name); // 'libc.so.6' 或类似 const mod2 = Process.findModuleByName('libSystem.B.dylib');

源码中findModuleByAddress返回的是真正的Module实例(而非原始对象),所以拿到后可以直接调用findExportByName、scan等实例方法。

Module API:模块查找与导出

Module既是类(实例方法)也提供静态方法(module.ts)。

静态方法

Module.findExportByName(moduleName, exportName)

查找导出函数地址;moduleName传null表示全局搜索所有模块:

const malloc = Module.findExportByName(null, 'malloc'); console.log(malloc); // NativePointer 对象,未找到时为 null

实现上会调用NativeProcess.findExportByName,若返回空指针则统一转换为 JS 的null。

Module.enumerateExports(moduleName)

枚举指定模块的全部导出符号:

const exports = Module.enumerateExports('libc.so.6'); exports.forEach(e => { console.log(`${e.type} ${e.name}: ${e.address}`); });

每个导出对象包含type('function'或'variable')、name、address(NativePointer)。

Module.load(moduleName)

按名称查找(获取)已加载模块:

const mod = Module.load('mylib.so'); console.log(mod.base);

另有一个未在 API 文档单列但源码提供的Module.findBaseAddress(moduleName),直接返回模块基址NativePointer,适合只需要基址的场合。

实例方法

持有Module实例后,还有几个非常实用的方法:

const mod = Module.load('libchrome.so') ?? Module.findExportByName(null, 'malloc') && Process.findModuleByAddress(0); // 更常见的写法: const mod = Process.findModuleByAddress(Module.findExportByName(null, 'malloc')); mod.findExportByName('symbol'); // 本模块内查导出 mod.enumerateExports(); // 本模块全部导出 mod.scan('48 8b ?? 00'); // 本模块内同步模式扫描(等价 Memory.scanModule) await mod.scanAsync('48 8b ?? 00'); // 异步版 mod.findXrefs(targetAddr); // 本模块内查找指向 targetAddr 的交叉引用 await mod.findXrefsAsync(targetAddr);

模块级扫描与 xref 查找本质上委托给Memory.scanModule和Instruction.findXrefsInModule(见下文),作用范围自动限定在该模块的[base, base + size)。

Memory API:分配、保护与模式扫描

Memory 实现 是修改类脚本使用最频繁的部分:分配、拷贝、改保护、字节模式扫描都在这里。

分配与释放

Memory.alloc(size)

分配size字节内存,返回NativePointer:

const buf = Memory.alloc(64); buf.writeU32(0xCAFEBABE);

文档称其为“可执行内存”,从 memory.ts 注释看更准确的描述是“以读写权限分配”;如需可执行权限,配合Memory.protect打开'x'位即可。配套还有两个文档未列出、源码确实存在的方法:

  • Memory.allocUtf8String(str):分配内存并写入 null 结尾的 UTF-8 C 字符串(内部完整实现了 UTF-8 编码,包括代理对),适合给原生函数传字符串参数;
  • Memory.free(address, size):释放Memory.alloc分配的内存。
Memory.copy(dst, src, size)
const src = Memory.alloc(16); src.writeU32(0x12345678); const dst = Memory.alloc(16); Memory.copy(dst, src, 4);
Memory.protect(address, size, protection)

修改内存保护属性,protection 取值如'r--'、'rw-'、'rwx'。注意:TS 层的实现(memory.ts)会捕获底层异常并返回 boolean 表示是否成功,而不是文档口径中“返回旧保护属性”——写关键路径的脚本建议显式判断返回值:

const p = Memory.alloc(4096); const ok = Memory.protect(p, 4096, 'rwx'); // 成功为 true

模式扫描

Memory.scanSync(address, size, pattern)/Memory.scan(address, size, pattern)

在指定内存区域中扫描十六进制字节模式。模式为空格分隔的字节序列,??表示通配符;源码注释标明底层算法为Boyer-Moore-Horspool,异步版(scan)把 C++ 协程转换为 JS Promise:

const results = Memory.scanSync(buf, 64, 'ef be ad de'); results.forEach(r => console.log(`Found at ${r.address}`)); const results2 = await Memory.scan(buf, 64, 'ef be ad de');
Memory.scanModule(moduleName, pattern)/scanModuleAsync

直接按模块名扫描,内部查模块基址与大小后委托给同一个扫描引擎:

const results = Memory.scanModule('libc.so.6', '48 8b ?? 00');

Memory.patchCode(address, size, apply)

补丁代码的便捷入口,文档未列但源码提供(memory.ts):把原代码拷贝到可写缓冲区,交给你的apply回调修改,再把改后的字节写回原地址,并自动处理保护切换与指令缓存刷新:

Memory.patchCode(addr, 4096, (code) => { // code 是可写的“原代码副本”,修改后自动写回 code.add(0x10).writeU32(0x90909090); // 例:NOP 掉某段指令 });

NativePointer API:指针运算与内存读写

NativePointer是贯穿所有 API 的基础类型(C++ 绑定直接实现的类,TS 层只做转发)。构造函数接受数值或十六进制字符串:

const p1 = new NativePointer(0x1234); const p2 = ptr('0xdeadbeef'); const p3 = ptr(0);

ptr()工厂函数(native-pointer.ts)兼容NativePointer、number、bigint、'0x...'字符串;全局还导出了NULL(零指针)常量。

常用方法:

ptr(0).isNull(); // boolean,是否为空 ptr(100).add(50); // 150,返回新的 NativePointer ptr(200).sub(50); // 150 ptr(0xFF00).and(ptr(0x0FF0)); // 0x0F00,位运算 or()/xor() 同理 const a = ptr(100), b = ptr(200); a.compare(b); // -1(小于)、0(等于)、1(大于) a.equals(b); // boolean ptr(0x1234).toString(); // '0x1234' ptr('0xdeadbeef').toUInt32(); // 3735928559

读写方法(配合Memory.alloc使用最直观):

const p = Memory.alloc(64); // 写入 p.writeU8(0xFF); p.writeU16(0x1234); p.writeU32(0xDEADBEEF); p.writeU64(0x123456789ABCDEF0n); // 读取 const u8 = p.readU8(); const u16 = p.readU16(); const u32 = p.readU32(); const u64 = p.readU64();

NativeFunction API:调用原生函数

NativeFunction让你把任意原生地址包装成可调用的 JS 函数(native-function.ts)。

构造与类型

const fn = new NativeFunction(address, returnType, argTypes, abi);
  • address:函数地址(NativePointer);
  • returnType:返回类型字符串;
  • argTypes:参数类型数组;
  • abi:可选,从源码看默认值为'default',支持'sysv'(System V AMD64)、'stdcall'(Windows)、'win64'。

支持的类型字符串:'void'、'int'/'uint'、'long'/'ulong'、'int8'–'uint64'系列、'float'/'double'、'pointer'。

const malloc = Module.findExportByName(null, 'malloc'); const fn = new NativeFunction(malloc, 'pointer', ['size_t']); const buf = fn(1024); const add = new NativeFunction(addr, 'int', ['int', 'int']); const result = add(3, 4); // 7

从_call的实现可以看到返回值的类型还原规则:'void'返回undefined;'pointer'返回新的NativePointer;'float'/'double'返回number;'int64'/'uint64'/'long'/'ulong'返回BigInt(64 位值不能塞进 JS number,脚本里做运算时注意typeof是'bigint');其余整型返回number。底层调用统一走NativeFFI.callFunction(C++ 绑定),参数经字符串序列化后传递。

NativeCallback API:把 JS 函数交给原生侧

NativeCallback创建一段原生可执行的回调,供原生代码调用(native-callback.ts):

const cb = new NativeCallback(function(a, b) { return a + b; }, 'int', ['int', 'int']); const fn = new NativeFunction(cb.address, 'int', ['int', 'int']); const result = fn(10, 20); // 30 cb.destroy();
  • 参数:func(JS 回调)、returnType、argTypes,可选第 4 参为 ABI(默认'default');
  • cb.address:回调的原生地址(NativePointer);
  • cb.destroy():销毁回调并释放资源。

实现上 JS 回调被包装为(string[]) => string的形式交给 C++ 侧(NativeFFI.createCallback),参数按类型声明还原为NativePointer/number,回调抛出的异常会被捕获并返回'0',不会把宿主进程带崩——这意味着回调里做防御性编程仍然必要。

Interceptor API:内联函数拦截

Interceptor是 chromatic 的核心能力之一,实现在 interceptor/index.ts:TS 层只是薄封装,trampoline 生成与代码重定位(code relocation,见 src/core/bindings/internal/code_relocator.cc)都在 C++ 的NativeInterceptor中完成。

Interceptor.attach(target, callbacks)

const target = Module.findExportByName(null, 'malloc'); const listener = Interceptor.attach(target, { onEnter(args) { console.log('malloc called with size:', args[0]); args[0] = ptr(2048); // 可修改参数 }, onLeave(retval) { console.log('malloc returned:', retval); retval.replace(newPtr); // 可修改返回值 } });

回调语义(与源码实现对照后需要修正 API 文档中的一处描述):

  • onEnter(args):args是参数代理,按下标读写(args[0]、args[1]…)。从 interceptor/index.ts 的实现看,读args[i]会去 CPU 上下文结构中取对应寄存器值:arm64 上取栈槽i * 指针大小,x64 上按寄存器偏移表[7, 6, 3, 2, 8, 9](即 rdi、rsi、rdx、rcx、r8、r9)读取——这与 System V AMD64 / AAPCS64 的传参顺序一致。写入args[i]会写回对应位置,从而在函数入口改参。
  • onLeave(retval):retval是当前返回值(NativePointer),通过retval.replace(value)重写返回值(arm64 直接改写上下文中的返回寄存器槽,x64 改写rax槽位)。API 文档提到的this.returnValue在当前实现中对应的就是这个retval对象及其replace方法。

拦截回调中的异常会被捕获吞掉(避免拖垮宿主),但这也意味着脚本错误不会以异常形式暴露,建议回调内自行打日志。

其他方法

listener.detach(); // 分离单个拦截器 Interceptor.detachAll(); // 分离全部拦截器

源码还额外提供了两个文档未列的静态方法:

// 完全替换目标函数,返回一个调用“原函数”的 trampoline 地址 const trampoline = Interceptor.replace(target, replacement); Interceptor.revert(target); // 恢复被 replace/attach 修改的代码

replace+trampoline组合是“接管整个函数并保留原函数调用能力”的标准做法。

Instruction API:反汇编与分析

Instruction由 C++ 绑定NativeDisassembler支撑,从 instruction.ts 的注释看底层反汇编引擎为Capstone。

Instruction.parse(address)

解析单条指令:

const addr = Module.findExportByName(null, 'malloc'); const insn = Instruction.parse(addr); console.log(insn.mnemonic, insn.opStr);

指令对象包含mnemonic(助记符)、opStr(操作数字符串)、size(指令长度)、address、bytes(指令字节,十六进制字符串)。

Instruction.disassemble(address, count)

const insns = Instruction.disassemble(addr, 5); insns.forEach(insn => { console.log(`${insn.address}: ${insn.mnemonic} ${insn.opStr}`); });

Instruction.analyze(address)

分析控制流特性:

const analysis = Instruction.analyze(addr); console.log('Is branch:', analysis.isBranch); console.log('Is call:', analysis.isCall);

结果对象含isBranch、isCall、isRelative、target(目标地址,十六进制字符串)、isPcRelative、size。

Instruction.filterInstructions(address, count, filter)/ 异步版

const calls = Instruction.filterInstructions(addr, 100, (insn) => { return insn.mnemonic === 'call'; }); const results = await Instruction.filterInstructionsAsync(addr, 100, filter);

Instruction.findXrefs(rangeStart, rangeSize, targetAddr)

在[rangeStart, rangeStart + rangeSize)内逐条反汇编,找出操作数解析结果等于targetAddr的指令(call、分支或 PC 相对数据引用):

const xrefs = Instruction.findXrefs(addr, 256, target); xrefs.forEach(xref => { console.log(`${xref.address}: ${xref.type}`); // type: 'call' | 'branch' | 'data' });

另有Instruction.findXrefsInModule(moduleName, targetAddr)(及异步版),把范围自动限定到模块内——这就是上文Module.findXrefs的底层实现。

SoftwareBreakpoint API:INT3 / BRK

软件断点在目标地址写入中断指令:x86 上是INT3,ARM64 上是BRK(breakpoint.ts):

const target = Module.findExportByName(null, 'malloc'); const bp = SoftwareBreakpoint.set(target, () => { console.log('Breakpoint hit!'); }); bp.remove(); // 移除单个断点 SoftwareBreakpoint.removeAll(); // 移除全部

从源码注释看,断点命中后回调在正常执行上下文中触发(而不是裸的信号处理器环境),原指令会被透明地重新执行,因此回调里可以放心使用常规 API。

HardwareBreakpoint API:调试寄存器

硬件断点占用调试寄存器,数量极少(通常 4 个),但不修改代码,对反调试敏感场景更有价值:

const max = HardwareBreakpoint.maxBreakpoints; // 通常为 4,不支持时为 0 const count = HardwareBreakpoint.activeCount; // 当前活动数量
HardwareBreakpoint.set(address, type, size, callback)
// 执行断点 const bp1 = HardwareBreakpoint.set(addr, 'execute', 1, () => { console.log('Execute breakpoint hit!'); }); // 写入观察点 const buf = Memory.alloc(8); const bp2 = HardwareBreakpoint.set(buf, 'write', 4, () => { console.log('Write watchpoint hit!'); }); bp1.remove(); HardwareBreakpoint.removeAll();
  • type:'execute'(执行断点)、'write'(写观察点)、'readwrite'(读写观察点);
  • size:观察范围 1/2/4/8 字节,'execute'类型忽略该参数。

MemoryAccessMonitor API:基于页面保护的访问监控

MemoryAccessMonitor用于捕获对指定内存区域的读/写访问(memory-access-monitor.ts):

const buf = Memory.alloc(4096); const handle = MemoryAccessMonitor.enable( [{ address: buf, size: 4096 }], (details) => { console.log('Access at:', details.address); console.log('Operation:', details.operation); // 'read' | 'write' | 'execute' console.log('Range index:', details.rangeIndex); } ); handle.disable(); MemoryAccessMonitor.disableAll(); const count = MemoryAccessMonitor.drain(); // 处理待处理事件,返回数量

回调details含address(访问地址)、pageBase(页面基址)、operation('read'/'write'/'execute')、rangeIndex(触发的范围索引)。两个关键实现细节值得注意:

  1. 从源码注释看,监控通过mprotect 页面保护触发,属于“访问时打异常再放行”的机制,与ExceptionHandler(见下节)能力上是联动的;
  2. 每个范围是 one-shot 的:回调触发后该范围保持可访问、不再产生后续事件。若要持续监控,需要在回调内重新enable。

ExceptionHandler API:全局异常处理

许多监控类功能(硬件断点、内存访问监控)依赖宿主进程捕获SIGSEGV/SIGTRAP等信号,ExceptionHandler就是这一层的开关(exception-handler.ts):

ExceptionHandler.enable(); // 启用全局异常处理器(幂等) console.log(ExceptionHandler.isEnabled); // boolean ExceptionHandler.disable(); // 禁用,恢复原始信号处理器

从源码注释看,POSIX 平台安装SIGSEGV、SIGBUS、SIGTRAP、SIGILL的信号处理器,Windows 上使用 VEH(Vectored Exception Handler),使得上述调试特性可以“捕获异常而不崩溃”。如果你的脚本要使用断点/监控类 API,先ExceptionHandler.enable()是稳妥的初始化顺序。

工具函数

ptr(value)

创建NativePointer的快捷方式,兼容数字、十六进制字符串与已有指针:

const p = ptr(0x1234); const q = ptr('0xdeadbeef'); const n = NULL; // 全局零指针常量

hexdump(address, options)

生成带地址列、十六进制列和 ASCII 列的多行转储字符串(hexdump.ts),兼容ArrayBuffer:

const buf = Memory.alloc(32); const dump = hexdump(buf, { length: 32 }); console.log(dump);

选项对象比文档列出的更完整:offset(起始偏移,默认 0)、length(转储长度,默认 256 而非全部)、header(是否打印列头,默认true)、ansi(预留,默认false)。读取失败时会返回(inaccessible)字符串而非抛异常。

一个组合示例:从模块定位到行为监控

把上面的 API 串起来,是一个典型的“定位目标 → 挂钩 → 改参 → 观察副作用”脚本骨架:

ExceptionHandler.enable(); const mod = Process.findModuleByName('libchrome.so') ?? Process.findModuleByAddress(Module.findExportByName(null, 'malloc')); // 1) 看函数长什么样 Instruction.disassemble(mod.base, 8).forEach(i => { console.log(`${i.address}: ${i.mnemonic} ${i.opStr}`); }); // 2) 拦截并改参 const listener = Interceptor.attach(Module.findExportByName(null, 'malloc'), { onEnter(args) { if (args[0].toUInt32() < 8) args[0] = ptr(8); }, onLeave(retval) { /* retval.replace(...) 可改写返回值 */ } }); // 3) 监控一块内存的首次写入 const buf = Memory.alloc(4096); const mon = MemoryAccessMonitor.enable([{ address: buf, size: 4096 }], (d) => { console.log('touched at', d.address, 'op:', d.operation); });

对应的测试基础设施在 src/test/ 下按 API 逐一覆盖(如 test_interceptor.cc、test_memory.cc、test_breakpoint.cc、test_page_access.cc),压力测试见 scripts/stress-test.sh,可作为验证脚本行为的参照。

小结

chromatic 的脚本 API 以 Frida 的脚本心智模型为蓝本:Process/Module负责“找”,Memory/NativePointer负责“读写”,NativeFunction/NativeCallback负责“互调”,Interceptor负责“拦”,Instruction/断点/MemoryAccessMonitor/ExceptionHandler负责“看与控”。所有重逻辑都在 C++ 绑定层(src/core/bindings/),TS 层(src/core/typescript/src/)负责把原生结果整理成脚本友好的对象——读源码时按“TS 薄封装 → 原生绑定 → 内部实现(如 code_relocator.cc)”这条链路找,基本可以快速定位任何 API 的底层行为。

  • 逆向工程
  • 调试器

【免费下载链接】chromatic

Universal modifier for Chromium/V8 | 广谱注入 Chromium/V8 的通用修改器

项目地址:https://gitcode.com/gh_mirrors/be/chromatic
点击查看免费下载

相关推荐

上一篇:pnpr Docker 镜像部署指南:基于 pnpm 生态的 registry 服务器容器化实践
下一篇:NHibernate会话管理深度解析:Session的生命周期与最佳实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询