- 逆向工程
- 调试器
【免费下载链接】chromatic
Universal modifier for Chromium/V8 | 广谱注入 Chromium/V8 的通用修改器
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.pointerSize | 4(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(触发的范围索引)。两个关键实现细节值得注意:
- 从源码注释看,监控通过mprotect 页面保护触发,属于“访问时打异常再放行”的机制,与
ExceptionHandler(见下节)能力上是联动的; - 每个范围是 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 的通用修改器
相关推荐
chromatic JavaScript API 详解:Chromium/V8 广谱修改器的原生插桩接口
chromatic JavaScript API 详解:Chromium/V8 广谱修改器的原生插桩接口 chromatic 是一个面向 Chromium/V8
逆向工程调试器GPT4All 本地 API Server 实战指南:用 OpenAI 兼容 HTTP 接口驱动本地 LLM 并接入 LocalDocs
GPT4All 本地 API Server 实战指南:用 OpenAI 兼容 HTTP 接口驱动本地 LLM 并接入 LocalDocs 本篇技术指南基于 GP
人工智能大模型本地部署AI 应用桌面应用RAG微调解决Frida版本混乱:objection多版本兼容实战指南
解决Frida版本混乱:objection多版本兼容实战指南 你是否在使用objection时频繁遇到"Frida版本不兼容"错误?是否因升级Frida导致原有
渗透测试应用安全逆向工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考