chromatic API 实战指南:Chromium/V8 广谱注入的 Frida 兼容脚本接口全解
发布时间:2026/9/25 5:30:45来源:尧图网络
逆向工程调试器【免费下载链接】chromaticUniversal 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.archarm64或x64当前 CPU 架构Process.platformwindows、linux、darwin或android当前操作系统Process.pointerSize432 位或864 位原生指针字节数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 中这些属性直接透传到原生的NativeProcessNP.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}); });每个导出对象包含typefunction或variable、name、addressNativePointer。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字节内存返回NativePointerconst 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 Promiseconst 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函数地址NativePointerreturnType返回类型字符串argTypes参数类型数组abi可选从源码看默认值为default支持sysvSystem V AMD64、stdcallWindows、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返回undefinedpointer返回新的NativePointerfloat/double返回numberint64/uint64/long/ulong返回BigInt64 位值不能塞进 JS number脚本里做运算时注意typeof是bigint其余整型返回number。底层调用统一走NativeFFI.callFunctionC 绑定参数经字符串序列化后传递。NativeCallback API把 JS 函数交给原生侧NativeCallback创建一段原生可执行的回调供原生代码调用native-callback.tsconst 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();参数funcJS 回调、returnType、argTypes可选第 4 参为 ABI默认defaultcb.address回调的原生地址NativePointercb.destroy()销毁回调并释放资源。实现上 JS 回调被包装为(string[]) string的形式交给 C 侧NativeFFI.createCallback参数按类型声明还原为NativePointer/number回调抛出的异常会被捕获并返回0不会把宿主进程带崩——这意味着回调里做防御性编程仍然必要。Interceptor API内联函数拦截Interceptor是 chromatic 的核心能力之一实现在 interceptor/index.tsTS 层只是薄封装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 修改的代码replacetrampoline组合是“接管整个函数并保留原函数调用能力”的标准做法。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 APIINT3 / BRK软件断点在目标地址写入中断指令x86 上是INT3ARM64 上是BRKbreakpoint.tsconst 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();typeexecute执行断点、write写观察点、readwrite读写观察点size观察范围 1/2/4/8 字节execute类型忽略该参数。MemoryAccessMonitor API基于页面保护的访问监控MemoryAccessMonitor用于捕获对指定内存区域的读/写访问memory-access-monitor.tsconst 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页面基址、operationread/write/execute、rangeIndex触发的范围索引。两个关键实现细节值得注意从源码注释看监控通过mprotect 页面保护触发属于“访问时打异常再放行”的机制与ExceptionHandler见下节能力上是联动的每个范围是 one-shot 的回调触发后该范围保持可访问、不再产生后续事件。若要持续监控需要在回调内重新enable。ExceptionHandler API全局异常处理许多监控类功能硬件断点、内存访问监控依赖宿主进程捕获SIGSEGV/SIGTRAP等信号ExceptionHandler就是这一层的开关exception-handler.tsExceptionHandler.enable(); // 启用全局异常处理器幂等 console.log(ExceptionHandler.isEnabled); // boolean ExceptionHandler.disable(); // 禁用恢复原始信号处理器从源码注释看POSIX 平台安装SIGSEGV、SIGBUS、SIGTRAP、SIGILL的信号处理器Windows 上使用 VEHVectored 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兼容ArrayBufferconst 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 的底层行为。赞分享逆向工程调试器【免费下载链接】chromaticUniversal modifier for Chromium/V8 | 广谱注入 Chromium/V8 的通用修改器项目地址https://gitcode.com/gh_mirrors/be/chromatic点击查看免费下载相关推荐chromatic JavaScript API 详解Chromium/V8 广谱修改器的原生插桩接口chromatic JavaScript API 详解Chromium/V8 广谱修改器的原生插桩接口 chromatic 是一个面向 Chromium/V8逆向工程调试器GPT4All 本地 API Server 实战指南用 OpenAI 兼容 HTTP 接口驱动本地 LLM 并接入 LocalDocsGPT4All 本地 API Server 实战指南用 OpenAI 兼容 HTTP 接口驱动本地 LLM 并接入 LocalDocs 本篇技术指南基于 GP人工智能大模型本地部署AI 应用桌面应用RAG微调解决Frida版本混乱objection多版本兼容实战指南解决Frida版本混乱objection多版本兼容实战指南 你是否在使用objection时频繁遇到Frida版本不兼容错误是否因升级Frida导致原有渗透测试应用安全逆向工程上一篇pnpr Docker 镜像部署指南基于 pnpm 生态的 registry 服务器容器化实践下一篇NHibernate会话管理深度解析Session的生命周期与最佳实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网