前言
AI逆向做了很久,但是之前一直都是靠模型能力(opus)+自己的逆向经验引导ai搞定的,让AI作为自己的“下手”,感觉没什么可写的。最近把之前的自动化框架彻底完善了,开始做补环境降低些自动化的机器成本,就准备搞一套靠谱的AI补环境工具,于是有了这篇文章。
原理
魔改v8给ai指路,配合cdp控制浏览器进行diff,几乎精准指向异常环境,兼顾效率与准确性。
魔改v8
魔改v8主要有如下功能:
属性读写的trace日志,辅助ai分析 完整覆盖度的异常抛出,ai补环境的核心切入点,包括:对象调用的空返回、主动或被动的throw error捕获、call/apply/bind等函数的空返回或false返回、==/instanceof/in等判断符、typeof/Object/Reflect等特殊原型检测、toString等常见检测等 watchpoint内存观测点,ai追踪变量改变或者调用的核心工具,为某内存对象套上point后,后续所有读写该内存的位置都会被记录
这套魔改逻辑的灵感来源为unidbg和unidbg-trace。虽然web也有很多基于jsdom或vm2的补环境、吐环境框架,但是大部分的核心问题是完善度不够高且结构太大,喂给ai上下文非常容易满,并且ai不会对这些vm的接口进行过多质疑。即使你写到skill里不要过于相信框架本身,上下文一旦过长ai也会忘记检查某些js是否能在框架中检测到异常。另一方面,app段有frida、xp、unHook等很多内存hook工具,但web基于内存的hook工具几乎没有。因此我并没有选择写js框架来吐环境,而是借助v8(感谢风和自由的指点,两句话排除了两个坑)。
v8迭代+补环境认知建立
代码基本都是ai写的,全程几乎没写过一段代码。这一阶段主要是告诉ai自己的想法,让它从零开始动手改造v8。我选择了阿里的_rand参数为靶场,因为rand那套js有着国内最顶级的浏览器完整性检测,能够高覆盖的迭代v8。这个过程用时两天,需要人盯着提出路线与决策,从而可以借助大厂的检测快速迭代出适合自己的工具,尽可能做到功能性的全面覆盖。另一方面也能建立ai对补环境的认知:不是“能过就行”,而是环境完全无异常,与浏览器彻底一致,且优先补机制而不是硬编码返回值,提高稳定性。
简单风控尝试
这一部分做了两个风控各花费半天:阿里的另外三个参数(fytoken/bx-et/bx-pp)和海外reese84。主要目的是带着ai升级,让它极可能多的学习node如何模拟浏览器机制,如事件模拟、特殊css、canvas渲染等。这里不使用任何skill来存储它的成果总结,而是让它在自己写的环境js中写好注释,后续遇到其它环境问题直接来这个js找相似就可以。中间也试了封装v8功能为mcp、提炼skill、规定agent流程,反倒幻觉更严重。本来想着这部分可能也要两三天,结果魔改后的v8给ai调试能力带来了巨大的提升,直接一天搞定两个风控。另外,浏览器调试mcp也是让ai搞的,市面上浏览器相关mcp要么偏浏览器控制,要么偏cdp底层,感觉很多功能和逆向不搭边,一些真正需要的功能又没有。于是让ai搞了个替换响应、三种断点、跑js、进url的简单cdp脚本,用来固定代码及调试。
shape F5
这里选的是一个海外物流网站usps,这个站的shape可能会比其它的风控低一些,因为少了一两个上报请求,但是vmp结构与参数流程应该和别的站完全一致。(纯猜测,在此之前只搞过自动化shape,完全没分析过流程)
这里反倒没什么卡点与迭代,全程ai动手零干预,上班开搞下班搞定,goal参考设置:看v8抛的异常+用point、debugger、trace找来源+强制与浏览器diff+禁止偷懒+补到0异常+禁止动不动就验证是否可用。goal设置完就让它自己瞎折腾,停了发个继续就不管了。由于shape的js是动态的,补完一次后再固定个别的版本继续迭代环境,多来两次就OK了。中间遇到的唯一一个ai没解决的问题是补完后一直请求不过,根据经验判断是请求头缺失,它mitm抓自己发的请求与浏览器的请求对比,补上了sec相关请求头,最终成功拿到usps物流数据。并发则需要环境的一致性,根据经验改些指纹就可以,略。
总结
几年前谁能想到海外的风控“天花板”未来会被一天搞定。逆向太简单了,美团外卖启动。
附:AI总结的Shape F5 JSVMP 检测手段分析
所有检测逻辑编译成自定义字节码,运行在 JS 实现的虚拟机(JSVMP)中。外部只能看到一个 ~550KB 的 opcode 解释器循环。核心回传 header 只有 X-jFuguZWB-a。
1. 异常模式指纹
不是避免异常,而是校验异常是否与真实浏览器一致。JSVMP 内部每次环境探测都被 try-catch 包裹,采集"哪些位置抛了、抛的消息是什么"的异常位图。
故意触发的异常:
o.__proto__ = o→ 必须抛Cyclic __proto__ valueDate.prototype.toString.call({})→ 必须抛 incompatible receiver对不存在的方法做 .apply()→ 必须抛 apply on undefined
真实 Chrome 在这些位置会抛错,你不抛反而有问题。
2. VM 内部加密常量表
字符串常量不是明文存储,运行时通过循环密钥解密:
[Function: wx].lN = "!ZB]I9_EIM!ZB]I9_EIM..."[Function: jIz].UZ = "f:VTkV)b]}dIVKfD1/)~..."即使拿到完整 trace,看到的也是密文中间值。
3. Node.js 环境泄露探测
扫描 process.env 中 Node.js 特有变量(62 次反复读取):
NODE_DISABLE_COLORSNO_COLORerrno、error
4. MutationObserver 行为探测
不检测 typeof MutationObserver,而是实际执行 observe:
new MutationObserver(cb).observe(target, {attributeFilter: ["sideexplayingflag"] // 随机生成})大多数补环境只给空构造函数没给 observe 方法。
5. toString 探测(多通道)
不只是 fn.toString() 检查 [native code]:
Function.prototype.toString.call(fn) | |
FTS = Function.prototype.toString,再 FTS.call(target),绕过对 FTS 本身的 hook | |
structuredClone(fn) | |
postMessage(fn) | |
String(fn)fn + '' | |
[native code],递归校验 |
6. 堆栈探测
new Error().stack | node:internal/modules/cjs/loader 暴露 Node.js |
e.stack | |
Error.captureStackTrace | |
Error.prepareStackTrace | |
at https://xxx.com/script.js:1:2 vs Node at /home/user/script.js:1:2 | |
7. 对象属性读取(多手段交叉)
Object.getOwnPropertyDescriptor(proto, key) | |
__lookupGetter____lookupSetter__ | |
Object.getOwnPropertyNamesObject.keys | |
Reflect.ownKeys | |
in | |
hasOwnPropertyin | |
propertyIsEnumerable | |
for...in |
8. 原型链探测(多维度)
Object.getPrototypeOf | |
__proto__ | |
constructor | obj.constructor === HTMLVideoElement |
constructor.name | "HTMLVideoElement" vs "ctor" vs "Object" |
Symbol.toStringTag | Object.prototype.toString.call(obj)[object HTMLVideoElement] 还是 [object Object] |
Symbol.hasInstance 骗 instanceof 但 walk 结果不同 = 矛盾 | |
getOwnPropertyDescriptor | |
getOwnPropertyNames | |
__proto__ 赋值 | |
HTMLElement.prototype.getAttribute.call(videoEl, 'src') |
9. prototype 属性类型检测
用 __lookupGetter__ / Object.getOwnPropertyDescriptor 区分属性是 accessor(getter/setter)还是 data property:
// 真实 Chrome: innerHTML 是 accessorObject.getOwnPropertyDescriptor(HTMLElement.prototype, 'innerHTML')// → {get: f, set: f, enumerable: true, configurable: true}// 补环境常见错误: innerHTML 是 data property// → {value: "", writable: true, ...}同样的检测用在 Screen.prototype.width、Navigator.prototype.userAgent 等所有浏览器属性上。
10. 原型链长度校验
不是 instanceof 检测(可被 Symbol.hasInstance 欺骗),而是手动 walk:
// 真实 Chrome: 7 层video → HTMLVideoElement.prototype → HTMLMediaElement.prototype → HTMLElement.prototype → Element.prototype → Node.prototype → EventTarget.prototype → Object.prototype → null// Node.js 补环境常见问题: 2-3 层video → Object.prototype → nullJSVMP 逐级 Object.getPrototypeOf(),数链条长度。链条短了就是假环境。
本次补环境修复记录
navigator.userAgentData | ||
window.chrome | {loadTimes(), csi(), app} | |
canPlayType | ||
MutationObserver.observe | ||
navigator.mediaSession | ||
[native code] |
Trace 结果:THROW 63→25(与浏览器一致),HTTP 302→200
夜雨聆风