dsh-js-reverse-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/cordis.patch.yml +14 -0
  2. package/index.js +61 -0
  3. package/package.json +20 -0
  4. package/skills/iv8-env-runtime/SKILL.md +159 -0
  5. package/skills/iv8-env-runtime/assets/entry-report-template.txt +23 -0
  6. package/skills/iv8-env-runtime/assets/env_starter.py +100 -0
  7. package/skills/iv8-env-runtime/references/case-function-iwencai.md +96 -0
  8. package/skills/iv8-env-runtime/references/case-intercept-douyin.md +133 -0
  9. package/skills/iv8-env-runtime/references/config-defaults-cheatsheet.md +75 -0
  10. package/skills/iv8-env-runtime/references/cross-platform-install.md +108 -0
  11. package/skills/iv8-env-runtime/references/iv8-api-contract.md +97 -0
  12. package/skills/iv8-env-runtime/references/network-and-eventloop.md +113 -0
  13. package/skills/iv8-env-runtime/references/part1-entry-location.md +138 -0
  14. package/skills/iv8-env-runtime/references/part1-mcp-tool-map.md +77 -0
  15. package/skills/iv8-env-runtime/references/part2-discovery.md +80 -0
  16. package/skills/iv8-env-runtime/references/patching-principles.md +91 -0
  17. package/skills/iv8-env-runtime/references/trap-blacklist.md +60 -0
  18. package/skills/iv8-env-runtime/references/version-gotchas.md +128 -0
  19. package/skills/iv8-env-runtime/scripts/consistency_lint.py +211 -0
  20. package/skills/iv8-env-runtime/scripts/defaults_audit.py +143 -0
  21. package/skills/iv8-env-runtime/scripts/monitor_env.py +185 -0
  22. package/skills/iv8-env-runtime/scripts/network_bridge.py +147 -0
  23. package/skills/iv8-env-runtime/scripts/parity_check.py +175 -0
  24. package/skills/iv8-env-runtime/scripts/preflight.py +144 -0
  25. package/skills/iv8-env-runtime/scripts/reproduce_stub_error.py +165 -0
  26. package/skills/iv8-env-runtime/scripts/run_target.py +150 -0
  27. package/skills/iv8-jsvmp-recovery/SKILL.md +129 -0
  28. package/skills/iv8-jsvmp-recovery/agents/openai.yaml +4 -0
  29. package/skills/iv8-jsvmp-recovery/references/case-study-douyin-a-bogus.md +320 -0
  30. package/skills/iv8-jsvmp-recovery/references/controlled-oracle-and-equivalence.md +247 -0
  31. package/skills/iv8-jsvmp-recovery/references/static-trace-evidence.md +289 -0
  32. package/skills/iv8-jsvmp-recovery/references/vmp-domain-model.md +212 -0
  33. package/skills/iv8-jsvmp-recovery/scripts/verify-equivalence.py +281 -0
  34. package/skills/js-ast-deobfuscation/SKILL.md +140 -0
  35. package/skills/js-ast-deobfuscation/agents/openai.yaml +4 -0
  36. package/skills/js-ast-deobfuscation/references/anti-patterns.md +325 -0
  37. package/skills/js-ast-deobfuscation/references/first-pass.md +255 -0
  38. package/skills/js-ast-deobfuscation/references/interaction-and-stopping.md +758 -0
  39. package/skills/js-ast-deobfuscation/references/iv8-runtime-observation.md +773 -0
  40. package/skills/js-ast-deobfuscation/references/jsvmp-identification.md +328 -0
  41. package/skills/js-ast-deobfuscation/references/methods-and-evidence.md +460 -0
  42. package/skills/js-ast-deobfuscation/references/patterns-and-priority.md +516 -0
  43. package/skills/js-ast-deobfuscation/references/validation-contract.md +604 -0
  44. package/skills/js-ast-deobfuscation/scripts/prepare-source.mjs +921 -0
@@ -0,0 +1,80 @@
1
+ # P2 · 起手跑 + 发现缺口 —— 要配/补什么
2
+
3
+ > 阶段定位:入口已定位(P1 完成),现在最小 environment 零配跑一遍,
4
+ > 让目标 JS 自己告诉你"它读了什么、缺了什么"。
5
+ > 退出 P2 = 你手上有一张"要配的清单 + 要补的 stub 清单",进 P3 落地。
6
+ > P1↔P2↔P3 可来回迭代,不是单向。
7
+
8
+ ---
9
+
10
+ ## 1. 四个发现手段(主力是 ①×②)
11
+
12
+ | 手段 | 发现什么 | 工具 |
13
+ |---|---|---|
14
+ | **① debug 监控** | 它**读/调了哪些**字段(= 要配的清单);摸了哪些缺失 API | `mode='debug'` + `scripts/monitor_env.py` |
15
+ | **② get_defaults 体检** | 它读的字段里,iv8 默认是否占位(about:blank/空)→ 占位就配 | `scripts/defaults_audit.py`(见 `references/config-defaults-cheatsheet.md`) |
16
+ | **③ 报错栈** | 缺的 API / stub 空壳窟窿 | 跑崩看 `TypeError` 栈,`scripts/reproduce_stub_error.py` 复现最小栈 |
17
+ | **④ 对账** | token 算出但服务器不认 → 哪个指纹值没对上 | `scripts/parity_check.py`,逐字段/逐字符对比真机 |
18
+
19
+ > **主力 = ①×②交集**:监控列出"它访问了什么",体检看"默认值对不对",**交集 = 你要配的**。
20
+ > 例:monitor 输出里出现 `canvas.toDataURL`/`fillText`/`webgl.getParameter` → 立刻知道在用 canvas+webgl 指纹 → 这些值要配真机的。
21
+ > ③ 只在跑崩时用(stub 窟窿);④ 是 P4 验收工具,但"算出被拒"时回头也靠它定位。
22
+
23
+ ---
24
+
25
+ ## 2. debug 监控读什么(monitor_env.py 输出)
26
+
27
+ `mode='debug'` 把每次访问实时打出,五类 + 缺失:
28
+ `getter`(读) / `setter`(写) / `call`(调用) / `query`(typeof/in) / `反射层`(Object.keys/getOwnPropertyDescriptor/Reflect.*) / `疑似缺失API`。
29
+
30
+ 读报告重点:
31
+ - **① getter / ③ call** → 它在乎哪些字段/指纹(= 要配的清单)。
32
+ - **⑥ 疑似缺失 API** → 区分"陷阱(别补)"和极少数"真机有但 iv8 缺(对齐才补)",判据见 `references/trap-blacklist.md`。
33
+
34
+ ---
35
+
36
+ ## 3. DOM 内容何时填 · 用 innerHTML 还是 page.load
37
+
38
+ 先分清:DOM 的 **API**(createElement/querySelector…)iv8 自洽不用补;DOM 的**内容**(页面元素/`<meta>`/`title`)默认空骨架。读页面 DOM 的反爬 JS 在空 DOM 上读到 null/空 → 签名错。
39
+
40
+ | 方式 | 行为 | 何时用 |
41
+ |---|---|---|
42
+ | `document.documentElement.innerHTML = html` | **只建 DOM 树**,不执行脚本、不派发事件、不同步 URL(轻量) | 目标只读 DOM 结构(读 `<meta>` token、读某节点、读 title) |
43
+ | `__iv8__.page.load({baseURL, html, resources, headers})` | 流式解析 + **执行 `<script>`** + 派发 DOMContentLoaded/load + 同步 URL | 目标依赖页面内 `<script>` 执行 / 生命周期事件 |
44
+
45
+ 怎么发现要不要填:debug 监控里出现 `document.querySelector`/`getElementById`/`document.title`/`<meta>` 访问,或签名跑出来不对 → 试填真实 HTML。**看站而定**:读页面 DOM 的站必填,只算指纹的站不填。
46
+
47
+ ---
48
+
49
+ ## 4. 起手 SOP:最小 environment → 零配跑 → 按结果分支
50
+
51
+ 起手 environment(覆盖大多数案例,模板见 `assets/env_starter.py`):
52
+ ```python
53
+ environment = {
54
+ "location": { ... 真实页面 URL 的各字段 ... }, # 必配(默认 about:blank 一定错)
55
+ "navigator": {"userAgent": "..."}, # 常配(只指定 UA,入签名)
56
+ }
57
+ ```
58
+
59
+ 零配跑之后按结果分支:
60
+
61
+ | 跑出来 | 诊断 | 动作 |
62
+ |---|---|---|
63
+ | 崩 `TypeError` | 缺 API / **stub 空壳窟窿** | 看栈(reproduce_stub_error)→ 替换那一个(MessageChannel 式,见 trap-blacklist §3) |
64
+ | 跑通、签名/token **算出但服务器不认** | 指纹值没配对/不一致 | monitor 看读了啥 → 体检对照 → 配 `canvas`/`webgl`/`screen`/DOM 内容,并过 `consistency_lint`;回 P3 迭代 |
65
+ | 跑通、**翻 0 / 200 / code=0** | 够用 | 收工(P4 已过),别画蛇添足 |
66
+ | 本地算对但请求被拒(403 等) | **多半非环境**(cookie/IP/参数过期);或真机指纹偏差 | 先排除非环境因素,别在 env 上死磕 |
67
+
68
+ > ⚠️ "算出被拒"和"403"是两回事:前者回 P3 配指纹对齐,后者往往根本不是环境问题。判决权在 P4——**服务器 200/code=0 是唯一真相,本地自洽不算**。
69
+
70
+ ---
71
+
72
+ ## 5. CHECKPOINT(退出 P2 前自查)
73
+
74
+ - [ ] 最小 environment 已零配跑过一遍,看过 debug 监控输出。
75
+ - [ ] ①×② 交集已列出"要配的字段清单"。
76
+ - [ ] 报错栈里的 stub 窟窿已单独登记(区别于陷阱)。
77
+ - [ ] "疑似缺失 API"已按 trap-blacklist 判据分流(陷阱别补 / 真机有才补)。
78
+ - [ ] DOM 内容是否要填已判定(monitor 有无页面 DOM 访问)。
79
+
80
+ 清单齐 → 进 P3 配环境 + 补漏 + 跑通(见 `references/patching-principles.md`)。
@@ -0,0 +1,91 @@
1
+ # P3 · 补环境原则 —— 配/补时守什么
2
+
3
+ > 服务于 **P3 配环境 + 补漏 + 跑通**。来自 hexin-v 等实战,压成两个元价值 + 5 条正交原则。
4
+ > 一句话:**我们不搬整个浏览器,只做一件事——和真机保持一致。目标检测什么就补什么,没检测的一概不补。**
5
+
6
+ ---
7
+
8
+ ## 0. 造 / 配 / 补 三分(iv8 各包多少)
9
+
10
+ | 层 | 内容 | 谁干 |
11
+ |---|---|---|
12
+ | **造环境** | window/document/DOM/事件/canvas/webgl/crypto/timer 全套 API | **iv8 全包**,零手写 |
13
+ | **配环境** | 指纹值(UA/屏幕/canvas…)+ 字段间一致性 | **你**(iv8 给旋钮,值你给) |
14
+ | **补漏** | 自带但实现是空的 stub 窟窿 | **你**(跑崩才知,替换;判据见 `references/trap-blacklist.md`) |
15
+
16
+ 体力活(手写 window/DOM)没了;脑力活(配真值、对齐真机)还是你的。实测佐证:同花顺 chameleon 零 API 补 token 算出、BOSS zhipin 读 1259 字段零补、抖音 bdms 只替换 1 个 stub。
17
+
18
+ ---
19
+
20
+ ## 1. 两个元价值(统领全篇)
21
+
22
+ - **一致**:凡要补的,一律照真机抄到**分辨不出**。基准 = js-reverse MCP 取到的真机值(它是处理过、过检测的真机环境,照抄不发明)。
23
+ - **最小**:边界 = **目标真读到的检测面**,不是"完整浏览器"。只补目标真读的,不复刻整个 window/DOM/BOM。给不存在的威胁上盔甲 = 过度工程。
24
+
25
+ ---
26
+
27
+ ## 2. 五条原则
28
+
29
+ | 轴 | 原则 | 一句话 |
30
+ |---|---|---|
31
+ | A 标准 | **1 一致性** | 凡补的都与真机一致(值/形态/undefined 三维度),基准 = MCP,产物逐字符验收 |
32
+ | B 发现 | **2 报错驱动** | 缺什么靠报错 + MCP 核对,不读源码枚举;**绝不吞异常** |
33
+ | C 节奏 | **3 逐个核对** | 一次只补那一处,先核对再写,可排查(慢就是快) |
34
+ | D 取舍 | **4 语义驱动** | 不机械、不过度;语义预判 + 对账定必要,行为类按链构造 |
35
+ | E 不侵入 | **5 原始 JS** | 加载原始未格式化 JS、零修改,hook 靠目标外新增 |
36
+
37
+ ### 原则 1 一致性 —— 三维度 + 两个静默暗坑
38
+
39
+ 一致分三维度:**值 / 形态 / undefined**,基准以 MCP 为准,最终以产物逐字符等价验收。
40
+
41
+ | 维度 | 含义 | 缺/错时 |
42
+ |---|---|---|
43
+ | **值** | 字符串/数字/结构(userAgent、plugins 列表) | 多数缺了**抛错** → 报错驱动能抓 ✅ |
44
+ | **形态** ⚠️ | `toString=[native code]`、属性在原型链哪层、descriptor、typeof、`'x' in obj` | **不抛错**,静默走错分支 → 暗坑 |
45
+ | **undefined** ⚠️ | 该有值 vs 本就没有 | **不抛错**,漏补静默 → 暗坑 |
46
+
47
+ - **暗坑一 形态**:默认最小不上来套盔甲;便宜的形态对齐补时顺手做(成员放真机所在原型层、给对 descriptor);贵的盔甲(全局伪造 toString、Proxy 拦 Function.prototype.toString)只在对账证明"确实被查"那处才做。iv8 里用 `__iv8__.wrapNative(fn,name)` 做 `[native code]` 伪装。
48
+ - **暗坑二 undefined**:读到 undefined **绝不因"没崩"就放过**。两种 undefined 长得一样意义相反,唯一区分 = 真机核对:真机也 undefined → 合法保持;真机有值 → 照真机补(判据同 `references/trap-blacklist.md` §0)。
49
+
50
+ ### 原则 2 报错驱动 —— 绝不吞异常
51
+
52
+ 缺什么只靠"报错 + MCP 核对"闭环发现,**禁止 grep/读源码枚举要补的字段**(大量目标是 VMP/WASM,读不了)。前提是**绝不吞异常**:任何补环境代码不许 try/catch 把目标的错吞掉续跑——吞了则报错驱动失效、执行流带偏、制造"JS 跑完但产物没出"的假成功。唯一例外:最外层探针"记录 + 原样重抛"。
53
+
54
+ ### 原则 3 逐个核对
55
+
56
+ 一次只补报错指出的那一处,先在 MCP 核对过才写进 env。批量脑补一串相关属性 = 多变量同时引入,一个不符就无法二分是谁害的。预判 ≠ 现在就写(预判只进"待核对清单")。慢就是快。
57
+
58
+ ### 原则 4 语义驱动
59
+
60
+ 补每个属性前先问语义意图。行为/采集类(鼠标/键盘/传感器/时间序列)顺链追问:产物是否依赖 → 需要什么数据 → 怎么构造 → 怎么注入。用语义预判可能性、用对账定必要性,既不机械漏补也不过度构造。iv8 侧注入靠 `__iv8__.input.dispatchMouseEvent/dispatchPointerEvent`(可信事件)+ `eventLoop` 推进。
61
+ > 注:本 skill **不涉及** Akamai 类真实鼠标轨迹/行为采集式的构造边界(用户明确排除)。
62
+
63
+ ### 原则 5 原始 JS 零修改
64
+
65
+ 一律加载**原始、未格式化**的目标 JS,一个字节都不改(目标常带自校验:toString 比对、控制流校验和、文件 hash,改动即触发惩罚分支)。要 hook 靠**目标之外新增**:包 prototype 方法、`defineProperty` 装 getter、沙箱外层拦截、`ctx.expose` 截产物。格式化版只供人眼阅读。对应 `references/iv8-api-contract.md` §3 的"改 JS 插 console → 外部 hook"。
66
+
67
+ ---
68
+
69
+ ## 3. 验收 = 逐字符对账 + 两道闸
70
+
71
+ 取真值是**手段不是验收**(可能某值抄对了、却漏了另一个目标也读的字段)。最终判据永远是:
72
+ 1. **两道闸都过**:① 目标 JS 跑完 ② 产物真产出(不是"无错跑完"就算成功)。
73
+ 2. **本地产物与真机逐字符等价**(`scripts/parity_check.py`)。
74
+ 3. **但真相在服务器**:200/code=0 才算翻,本地自洽不算(见 `references/part2-discovery.md` §4 分支)。
75
+
76
+ 会变的熵源(计数器、时间、随机数)不照搬瞬时快照,要冻结/对齐。
77
+
78
+ ---
79
+
80
+ ## 4. iv8 原生补丁契约
81
+
82
+ 目标 JS 的运行底座只能是 iv8。不要加载通用浏览器 mock 框架,也不要手写整套 window/DOM。确认 stub 空壳后,按以下契约在目标外部写一个最小补丁:
83
+
84
+ 1. 先用报错栈证明 iv8 的现有实现阻断了目标路径,再用真实 Chrome 核对该 API 的值、形态和必要行为。
85
+ 2. 补丁通过 `stub_setup_js` 在原始目标 JS 之前注入;不得修改、格式化或重写原始目标文件。
86
+ 3. 只实现 Trace 已触达的属性和方法;保持函数名、构造方式、原型层级、descriptor、`typeof` 和合法的 `undefined` 与真机一致。
87
+ 4. 函数和构造器使用 `__iv8__.wrapNative(fn, name)`;不要全局改写 `Function.prototype.toString`。
88
+ 5. 异步替身使用 iv8 Timer,并在收割产物前调用 `__iv8__.eventLoop.advance/drain`。
89
+ 6. 补丁前后分别运行相同输入;只有原报错消失、反射表面一致且目标输出对账不引入新分叉,才能保留补丁。
90
+
91
+ WASM 不使用跨目标通用桥接模板:保留目标自带的 glue 和真实 WASM 字节,只针对 iv8 中被动态证据证明不一致的宿主 binding 做最小修正。Canvas、Audio、字体等真实渲染物理量由浏览器采集并作为声明的环境输入注回 iv8,不能把浏览器产出的最终 token 当成 iv8 执行结果。
@@ -0,0 +1,60 @@
1
+ # 陷阱黑名单 · API 三态 —— 补不补一个 API
2
+
3
+ > 服务于 **P3 补漏**。核心问题:debug 监控吐出一堆"疑似缺失 API",哪些该补、哪些绝不能碰。
4
+
5
+ ---
6
+
7
+ ## 0. ★ 首要判据(通则,先记这一条)
8
+
9
+ > **未知的缺失 API,一律保持 `undefined`;除非去真机(js-reverse MCP)核对,确认真 Chrome 里确有值,才补。**
10
+
11
+ 下面的"黑名单"只是**举例**,不是穷举清单——新站会冒出你没见过的探测名。
12
+ 不要背清单,背**通则**:默认不补,补之前必须有"真机确有此值"的证据。理由同 patching-principles 原则 1.4 暗坑二(两种 undefined 长得一样、意义相反,唯一区分手段 = 真机核对)。
13
+
14
+ 黑名单举例(真 Chrome 也没有,补了 = 自曝是自动化环境):
15
+ `process` · `_phantom` · `__webdriver_script_fn` · `__nightmare` · `StyleMedia` · `Float16Array`(旧 Chrome 无)· `domAutomation` · `callPhantom` · 各种 `__driver_*` / `__selenium_*` / `__fxdriver_*`。
16
+
17
+ 它们分三类来源:反爬黑名单探针(webdriver 家族)/ IE 或非 Chrome 旧接口(StyleMedia)/ 新版特性存在性探测(Float16Array)。**共同点:真 Chrome 读到 undefined,你也就该 undefined。**
18
+
19
+ ---
20
+
21
+ ## 1. API 三态表(决定"补不补 API")
22
+
23
+ iv8 上的 API 不是"有/无"二元,是**三态**:
24
+
25
+ | 状态 | 例子 | 表现 | 怎么办 |
26
+ |---|---|---|---|
27
+ | **真实现** | navigator / document / crypto / XHR / canvas 2D 上下文对象 | 能用,行为对齐 Chrome | **不动** |
28
+ | **stub 空壳** | `MessageChannel`(`new().port1 === null`) | `typeof=function`、`toString=[native code]` 骗过存在性检测,但一**用**就崩 | **替换/补全**(见 §3) |
29
+ | **陷阱 / 正确缺失** | `process` / `_phantom` / `StyleMedia` / `Float16Array` | `undefined` | **绝不能补**(补了自曝) |
30
+
31
+ 关键:**stub 空壳 ≠ 陷阱**。前者要补,后者绝不能补。区分靠下面两类缺口的两种找法。
32
+
33
+ ---
34
+
35
+ ## 2. 两类缺口、两种找法
36
+
37
+ | 缺口 | 该怎样 | 症状 | 找法 |
38
+ |---|---|---|---|
39
+ | **陷阱 / 正确缺失** | 保持 `undefined` | 目标读它只为"存在即判死",读到 undefined 正是你要的 | 看 **debug 的"疑似缺失 API"日志**(typeof/in 探测会记进反射链)——绝大多数在这里的是陷阱,**别补** |
40
+ | **stub 空壳窟窿** | 替换真实现 | 运行时 `TypeError`(如 `Cannot set properties of null`、`x is not a function`) | **抓不到**在缺失日志里(typeof 正常、`[native code]` 齐全),只能靠**运行时报错栈**定位(`scripts/reproduce_stub_error.py` 复现最小栈) |
41
+
42
+ > 一句话:**缺失日志里的 = 大概率陷阱(别补);报错栈里的 = stub 窟窿(要补)。** 陷阱不抛错、stub 抛错,这是最快的分流信号。
43
+
44
+ ---
45
+
46
+ ## 3. stub 空壳补法
47
+
48
+ 典型 `MessageChannel`:iv8 给了空壳,`port1/port2 = null`,core-js 会用它降级 microtask 调度链,一用就崩。补法要点:配 `wrapNative` 伪装 + `eventLoop.advance`/`drain` 推进任务。具体见 `references/version-gotchas.md`(E2 条目,版本门控)。
49
+
50
+ 任何新 stub 都按 `references/patching-principles.md` 的 iv8 原生补丁契约处理:报错坐实、真机核形、目标外注入、`wrapNative`、一次一处、补后对账。禁止引入整套通用 mock,也禁止为了“更像浏览器”批量补 API。
51
+
52
+ ---
53
+
54
+ ## 4. 自查口诀
55
+
56
+ 1. 报错栈点名某 API → **stub 窟窿,补**(先 reproduce 最小栈)。
57
+ 2. 只在"疑似缺失"日志里、没抛错 → **默认别补**,登记待真机核对。
58
+ 3. 真机 js-reverse MCP 读它 = undefined → **合法保持**(顺带确认 typeof/in 一致)。
59
+ 4. 真机读它有真实值、iv8 缺 → 这才是"真机有 iv8 缺"的少数派,照真机补上。
60
+ 5. 任何"让它更像浏览器"的顺手补全 → **停**,违反最小原则(见 `references/patching-principles.md`)。
@@ -0,0 +1,128 @@
1
+ # iv8 专坑与版本门控(version-gotchas)
2
+
3
+ > **版本 BANNER(必读)**:本文件每条坑都**绑定 iv8 版本**。iv8 是闭源编译 wheel,
4
+ > 一个补丁在旧版是必需、在新版可能已被引擎修掉。**唯一权威版本 = `scripts/preflight.py`
5
+ > 打印的实测版本**(读它,别读本文标题)。凡标 **[已修 @ x.y.z]** 的坑:若 preflight
6
+ > 打印版本 ≥ 该版本,**别照抄补丁**——照抄反而制造与真浏览器的差异,自曝。
7
+ >
8
+ > 单一真相源:版本从 preflight 消费,不在本文写死"当前是 0.1.3"当结论。
9
+ > 目标 JavaScript 强制在 iv8 中运行。所有修正必须遵循 `references/patching-principles.md` 的 iv8 原生补丁契约;本文只写 iv8 专属差异。
10
+
11
+ ---
12
+
13
+ ## 门控总则(先读再往下)
14
+
15
+ 1. 每条坑三段:**现象 / 代码骨架 / 版本状态+适用边界**。
16
+ 2. 遇到崩溃先查 preflight 版本 → 对照本表判断"这条坑在你的版本还存不存在"。
17
+ 3. 补一个 stub 前先问:它是 §三态里的哪一态?只有 **stub 空壳**该补,**陷阱缺失绝不补**(见 references/trap-blacklist.md)。
18
+ 4. 修完必须回到验收:本地不崩 ≠ 成功,**服务器 200/code=0 才是唯一真相**(见 SKILL.md P4 / scripts/parity_check.py)。
19
+
20
+ ---
21
+
22
+ ## E1. `btoa()` Latin1 判定过窄 — **[已修 @ 0.1.3]**
23
+
24
+ - **现象(旧版)**:`btoa(String.fromCharCode(0xE9))` 等 charCode 128–255 的 Latin1 输入被错误拒绝抛异常。
25
+ sensor/collect 里含二进制字节 → btoa 失败 → 提交体空/`solution=null` → 服务端罐头错误。
26
+ - **代码骨架(仅旧版注入,跑原版 SDK **之前**覆盖 `window.btoa`,`&0xff` 只取低字节绕字符校验)**:
27
+ ```javascript
28
+ (function(){ var B="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
29
+ window.btoa=function(d){ var s=String(d),o=""; for(var i=0;i<s.length;i+=3){
30
+ var a=s.charCodeAt(i)&0xff,b=i+1<s.length?s.charCodeAt(i+1)&0xff:0,c=i+2<s.length?s.charCodeAt(i+2)&0xff:0;
31
+ o+=B[a>>2]+B[((a&3)<<4)|(b>>4)]+(i+1<s.length?B[((b&15)<<2)|(c>>6)]:'=')+(i+2<s.length?B[c&63]:'='); } return o; };
32
+ })();
33
+ ```
34
+ - **版本状态/边界**:**0.1.3 已修**(README 更新记录明列)。preflight 版本 ≥ 0.1.3 **勿注入**——原生 btoa 已正确,
35
+ 盖一层纯 JS 版会让 `btoa.toString()` 变成 `function(){...}`(非 `[native code]`),凭空制造检测点。只有 <0.1.3 才注入。
36
+
37
+ ---
38
+
39
+ ## E2. `MessageChannel`/`Worker` 的 port 为 null — core-js microtask 降级链
40
+
41
+ - **现象**:iv8 内置 `MessageChannel` 是 **stub 空壳**(typeof=function、toString=[native code] 过检测),
42
+ 但 `new MessageChannel().port1 === null`。core-js(bdms 等字节系 SDK 的 microtask polyfill)探测
43
+ MessageChannel 做快速异步调度,对 null port 调 `.postMessage`/设 `.onmessage` →
44
+ `Cannot set properties of null (setting 'onmessage')` → 事件循环断裂,`data()`/采集 Promise 永挂。
45
+ - **代码骨架(覆盖成"非 null 但行为为空"的 port,让 core-js 安全降级到 setTimeout;再配 advance+drain 推进)**:
46
+ ```javascript
47
+ window.Worker=function Worker(){this.onmessage=null;this.onerror=null;this.postMessage=function(){};this.terminate=function(){};};
48
+ window.MessageChannel=function MessageChannel(){
49
+ var mk=function(){return{onmessage:null,onmessageerror:null,postMessage:function(){},start:function(){},close:function(){}};};
50
+ this.port1=mk(); this.port2=mk();
51
+ };
52
+ // 装完后驱动:ctx.eval("window.__iv8__.eventLoop.advance(50)"); ctx.eval("...drain()")
53
+ ```
54
+ - **版本状态/边界**:属 **stub 空壳窟窿**(三态之一),未随版本消失——每次跑新 SDK 仍可能撞。**用 `wrapNative` 包装保 `[native code]`**。
55
+ 依赖 core-js microtask 降级链的 SDK 通用;若覆盖后 async 采集仍不推进,是"Promise 不推进"更深问题,见 references/network-and-eventloop.md 的 drain 章节。
56
+
57
+ ---
58
+
59
+ ## E4. WASM 反调试:instanceof 校验 + ~410MB OOM 炸弹
60
+
61
+ - **现象**:wasm-bindgen 型签名器内置多层"是否真实浏览器"检测,任一不过即分配 ~410MB ArrayBuffer 做
62
+ OOM 炸弹/panic。典型五关:① `global` 必须 `undefined`;② `self instanceof Window===true`;
63
+ ③ `location instanceof Location`;④ `documentElement instanceof HTMLElement`;⑤ direct `eval` 语义。
64
+ - **代码骨架(`Object.create(Proto.prototype)` 伪造满足 instanceof 的实例;`global=undefined`;跑真 WASM,**不跑 wasm2js fallback**——fallback 才含炸弹)**:
65
+ ```javascript
66
+ function Location(){} function HTMLElement(){} function Window(){}
67
+ var location=Object.create(Location.prototype); // ③ ✓
68
+ var documentElement=Object.create(HTMLElement.prototype); // ④ ✓
69
+ var win=Object.create(Window.prototype); // ② self instanceof Window ✓
70
+ win.self=win;win.window=win;win.globalThis=win;win.top=win;win.parent=win;
71
+ s.global=undefined; // ① 浏览器无 global ✓
72
+ var inst=new WebAssembly.Instance(new WebAssembly.Module(wasmBytes),{wbg:glue}); // 真 WASM
73
+ ```
74
+ - **版本状态/边界**:与 iv8 版本无关(是目标 WASM 的反调试,不是 iv8 引擎坑)。带 instanceof+OOM 的 wasm-bindgen 签名器通用。
75
+ 保留目标原始 glue、真实 WASM 字节及其 `__wbg_set_wasm`/`__wbindgen_start` 初始化顺序;只修正动态证据坐实的宿主 binding,不套跨目标桥接模板。iv8 侧不额外注入 eval/Function。
76
+
77
+ ---
78
+
79
+ ## E5. audio 指纹金标准 buffer — 真机抓一次注入
80
+
81
+ - **现象**:SDK 采 audio 指纹(`OfflineAudioContext(1,5000,44100)` → triangle@10kHz → DynamicsCompressor →
82
+ 渲染求 channel0`[4500:5000]` 绝对值和)。iv8 社区版**砍掉了真实渲染引擎**,算不出该浮点和。
83
+ - **代码骨架(真机 js-reverse `evaluate_script` 跑一次拿金标准 Float32 buffer 落地 → 注入 `window.__AUDIO_F32__` → mock `startRendering` 直接吐真 buffer)**:
84
+ ```javascript
85
+ mkOAC.prototype.startRendering=function(){var self=this; return new Promise(function(resolve){
86
+ var d=window.__AUDIO_F32__||new Float32Array(self.length||5000);
87
+ var arr=d.length>=self.length?d.subarray(0,self.length):d;
88
+ self.state='complete'; setTimeout(function(){resolve({sampleRate:self.sampleRate,length:self.length,
89
+ numberOfChannels:self.numberOfChannels,getChannelData:function(){return arr;}});},0);
90
+ });};
91
+ ```
92
+ - **版本状态/边界**:**硬天花板类**,非版本坑——iv8 永不做像素/音频渲染(README 明确社区版无布局/渲染)。
93
+ 分辨:sum 校验型用注入 buffer;属性采集型(只读 destination/channelCount/sampleRate/state)mock 属性即可。金标准值随采集参数变,**运行时抓,勿写死**。
94
+
95
+ ---
96
+
97
+ ## E6. jsvmp null-safe 操作码 patch
98
+
99
+ - **现象**:jsvmp 渲染阶段大量 DOM 读写(`.style`/`.appendChild()`),操作码形如 `bx(i,Y)[bx(i,Z)]`。
100
+ 无 layout 时元素常返回 null,jsvmp 对 null 取属性/赋值 → TypeError → 采集中途崩。
101
+ - **代码骨架(正则把 property-GET/SET 操作码改 null-safe,命中 null 落到黑洞对象)**:
102
+ ```javascript
103
+ src=src.replace(/ux\(i,(\w+),bx\(i,(\w+)\)\[bx\(i,(\w+)\)\]\)/g,'ux(i,$1,bx(i,$2)==null?window.__nullSafe:bx(i,$2)[bx(i,$3)])');
104
+ src=src.replace(/([\w$.\[\]]+)\.appendChild\(/g,'($1||window.__nullSafe).appendChild(');
105
+ // window.__nullSafe = new Proxy(function(){},{get:(t,k)=>k==='length'?0:window.__nullSafe,set:()=>true,apply:()=>window.__nullSafe,construct:()=>window.__nullSafe});
106
+ ```
107
+ - **版本状态/边界**:与 iv8 版本无关,属源码级 patch。iv8 有 C++ DOM/layout 表面,很多此类 null 崩本不会出现;
108
+ 仅当目标 jsvmp 读到 iv8 未实现的 layout 量(如真实 `getBoundingClientRect` 宽)才需要。**先跑,撞到再 patch**,别预防性乱改。
109
+
110
+ ---
111
+
112
+ ## E7. 字体渲染指纹 — 采集物理量后注回 iv8
113
+
114
+ - **现象**:探测字体渲染精确像素宽(`getBoundingClientRect()` 测 Verdana 等文字宽)。iv8 无字体引擎,
115
+ 只能 `length*fontSize*0.55` 近似,精度不够 → 风控判负。
116
+ - **对策**:用真实浏览器只采集目标实际读取的字体宽度或渲染测量表,把这些值声明为原始环境输入并注回 iv8;最终 SDK、生成入口和 token 仍在 iv8 中执行。不要直接拿浏览器生成的 token 代替 iv8 结果。
117
+ - **版本状态/边界**:**永久物理量缺口**,任何 iv8 版本都不会内置完整字体渲染。若无法把所需测量值隔离为可声明输入,状态应记为 `ORACLE_BLOCKED`,不能静默切换参考运行时。
118
+
119
+ ---
120
+
121
+ ## 一致性红线(跨所有坑重申)
122
+
123
+ 改指纹只能改"值",改不掉"API 存不存在"。**安全区 = Chrome/Chromium 家族内换不同机器;别伪装 Safari/Firefox**(涉 API 存在性差异,iv8 改不动)。
124
+ 以下六条必须整套对齐,任一漏改就自相矛盾秒露馅(曾实测:改了 `navigator.userAgent` 为 Mac、漏改 `navigator.userAgentData.platform` 仍 Windows → UA 说 Mac、UA-CH 说 Windows):
125
+
126
+ > **UA ↔ userAgentData ↔ platform ↔ 时区 ↔ 语言 ↔ webgl(vendor/renderer)**
127
+
128
+ 对齐逻辑得自己保证(与逐字段对齐同功夫)。可配路径清单见 references/config-defaults-cheatsheet.md;三态与陷阱名单见 references/trap-blacklist.md。
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ consistency_lint.py — environment / config 静态拒错器(阶段 P2/P3 护栏)
5
+
6
+ 用途:
7
+ 在把 environment / config 交给 iv8.JSContext 之前做静态体检,
8
+ 在「补环境跑起来」之前就拦掉四类会导致翻车却难查的错误:
9
+ 1) config 里出现数组值 —— iv8 的 config 只接标量(引擎行为,如 timezone/
10
+ permissions.*),画像类字段(可数组、JS 可观测)必须放 environment。
11
+ 2) 画像字段(navigator/screen/webgl/canvas...)被误塞进 config。
12
+ 3) 一致性红线不自洽:UA 里的平台/OS ↔ navigator.platform ↔
13
+ userAgentData.platform ↔ 语言 ↔ 时区 互相矛盾(反爬最常查的交叉项)。
14
+ 4) 检测到 POW/时间敏感特征线索,却没把 time_mode 设成 'system'(给告警)。
15
+
16
+ 本脚本只做静态判定,不会创建 JSContext,因此不需要装 iv8 即可运行。
17
+
18
+ 用法(库调用,推荐):
19
+ from consistency_lint import lint
20
+ problems = lint(environment, config, time_mode="logical")
21
+ # problems 为字符串列表,空列表=通过
22
+
23
+ 用法(命令行):
24
+ python3 scripts/consistency_lint.py env.json config.json [time_mode]
25
+ 退出码 0 = 无问题;非 0 = 有问题(逐条打印到 stderr)。
26
+
27
+ 本文件属于 iv8-env-runtime skill。
28
+ 交叉引用:environment/config 的字段归属与默认值见
29
+ references/config-defaults-cheatsheet.md;
30
+ 一致性红线的完整清单见 references/iv8-api-contract.md。
31
+ """
32
+
33
+ import json
34
+ import sys
35
+
36
+ # environment(画像)专属的顶层键;出现在 config 里即为错放。
37
+ _PROFILE_KEYS = {
38
+ "navigator", "screen", "location", "webgl", "canvas",
39
+ "document", "window", "history", "performance", "plugins",
40
+ "userAgentData", "audio", "fonts",
41
+ }
42
+
43
+
44
+ def _is_array(v):
45
+ return isinstance(v, (list, tuple))
46
+
47
+
48
+ def _walk_config_for_arrays(node, path, problems):
49
+ """递归查 config 中的任何数组值。config 只接标量。"""
50
+ if isinstance(node, dict):
51
+ for k, v in node.items():
52
+ _walk_config_for_arrays(v, "{}.{}".format(path, k) if path else str(k), problems)
53
+ elif _is_array(node):
54
+ problems.append(
55
+ "[config-数组] config['{}'] 是数组,但 config 只接标量引擎行为值;"
56
+ "画像/可数组字段请移到 environment。".format(path))
57
+
58
+
59
+ def _check_config_shape(config, problems):
60
+ if not config:
61
+ return
62
+ if not isinstance(config, dict):
63
+ problems.append("[config-类型] config 必须是 dict(当前 {})。".format(type(config).__name__))
64
+ return
65
+ # 2) 画像字段误塞进 config
66
+ for k in config:
67
+ if k in _PROFILE_KEYS:
68
+ problems.append(
69
+ "[画像错放] config 里出现画像字段 '{}',它属于 environment(JS 可观测);"
70
+ "config 只放引擎行为(timezone/permissions.* 等标量)。".format(k))
71
+ # 1) config 数组值
72
+ _walk_config_for_arrays(config, "", problems)
73
+
74
+
75
+ def _ua_platform_hint(ua):
76
+ """从 UA 字符串粗判操作系统家族,返回 'windows'/'mac'/'linux'/'android'/None。"""
77
+ if not ua:
78
+ return None
79
+ low = ua.lower()
80
+ if "windows" in low or "win64" in low or "win32" in low:
81
+ return "windows"
82
+ if "android" in low: # android 要先于 linux 判(UA 里两者都含 linux)
83
+ return "android"
84
+ if "mac os x" in low or "macintosh" in low:
85
+ return "mac"
86
+ if "linux" in low or "x11" in low:
87
+ return "linux"
88
+ return None
89
+
90
+
91
+ def _navigator_platform_family(platform_str):
92
+ """navigator.platform -> OS 家族。"""
93
+ if not platform_str:
94
+ return None
95
+ low = platform_str.lower()
96
+ if "win" in low:
97
+ return "windows"
98
+ if "mac" in low:
99
+ return "mac"
100
+ if "linux" in low or "x11" in low:
101
+ return "linux"
102
+ if "android" in low or "arm" in low:
103
+ return "android"
104
+ return None
105
+
106
+
107
+ def _check_consistency_redline(env, problems):
108
+ """一致性红线:UA↔platform↔userAgentData.platform↔语言↔时区 自洽。"""
109
+ if not isinstance(env, dict):
110
+ problems.append("[env-类型] environment 必须是 dict(当前 {})。".format(type(env).__name__))
111
+ return
112
+
113
+ nav = env.get("navigator", {}) or {}
114
+ ua = nav.get("userAgent")
115
+ nav_platform = nav.get("platform")
116
+ uad = nav.get("userAgentData", {}) or {}
117
+ uad_platform = uad.get("platform") # 形如 "Windows" / "macOS" / "Linux"
118
+
119
+ ua_fam = _ua_platform_hint(ua)
120
+ nav_fam = _navigator_platform_family(nav_platform)
121
+
122
+ # UA 家族 vs navigator.platform 家族
123
+ if ua_fam and nav_fam and ua_fam != nav_fam:
124
+ problems.append(
125
+ "[红线-OS] navigator.userAgent 指向 {},但 navigator.platform='{}' 指向 {},不自洽。"
126
+ .format(ua_fam, nav_platform, nav_fam))
127
+
128
+ # UA 家族 vs userAgentData.platform
129
+ if ua_fam and uad_platform:
130
+ uad_low = uad_platform.lower()
131
+ uad_fam = ("windows" if "win" in uad_low else
132
+ "mac" if "mac" in uad_low else
133
+ "linux" if "linux" in uad_low else
134
+ "android" if "android" in uad_low else None)
135
+ if uad_fam and uad_fam != ua_fam:
136
+ problems.append(
137
+ "[红线-OS] userAgent 指向 {},但 userAgentData.platform='{}' 指向 {},不自洽。"
138
+ .format(ua_fam, uad_platform, uad_fam))
139
+
140
+ # 语言:navigator.language 应是 navigator.languages 的首项
141
+ language = nav.get("language")
142
+ languages = nav.get("languages")
143
+ if language and _is_array(languages) and languages:
144
+ if languages[0] != language:
145
+ problems.append(
146
+ "[红线-语言] navigator.language='{}' 与 navigator.languages[0]='{}' 不一致。"
147
+ .format(language, languages[0]))
148
+
149
+ # 时区:environment 侧无标准时区字段,时区归 config.timezone。
150
+ # 若 environment 里出现类时区键,提示挪到 config。
151
+ for k in env:
152
+ if "timezone" in str(k).lower():
153
+ problems.append(
154
+ "[红线-时区] environment 里出现 '{}';时区属于 config.timezone(引擎行为),"
155
+ "请挪到 config,并确保与语言/UA 地区自洽。".format(k))
156
+
157
+
158
+ def _detect_time_sensitive(env, config):
159
+ """粗测是否有 POW/时间差校验线索。返回命中的线索列表。"""
160
+ hints = []
161
+ blob = json.dumps({"env": env, "config": config}, ensure_ascii=False).lower()
162
+ for kw in ("pow", "proof_of_work", "proofofwork", "hashcash", "nonce", "difficulty"):
163
+ if kw in blob:
164
+ hints.append(kw)
165
+ return hints
166
+
167
+
168
+ def lint(environment, config=None, time_mode="logical"):
169
+ """静态体检,返回问题字符串列表(空=通过)。不创建 JSContext。"""
170
+ problems = []
171
+ _check_config_shape(config, problems)
172
+ _check_consistency_redline(environment or {}, problems)
173
+
174
+ # 4) 时间敏感却未用 system 时间 —— 告警(不算硬错,但要提示)。
175
+ hints = _detect_time_sensitive(environment or {}, config or {})
176
+ if hints and time_mode != "system":
177
+ problems.append(
178
+ "[告警-时间] 检测到疑似 POW/时间差线索 {},但 time_mode='{}'。"
179
+ "POW/时间差校验应改用 time_mode='system'(否则 Date.now 不反映真实耗时)。"
180
+ .format(hints, time_mode))
181
+
182
+ return problems
183
+
184
+
185
+ def _load_json(path):
186
+ if not path:
187
+ return {}
188
+ with open(path, "r", encoding="utf-8") as f:
189
+ return json.load(f)
190
+
191
+
192
+ def main(argv):
193
+ if len(argv) < 2:
194
+ print("用法:python3 consistency_lint.py env.json [config.json] [time_mode]", file=sys.stderr)
195
+ return 2
196
+ env = _load_json(argv[1])
197
+ config = _load_json(argv[2]) if len(argv) >= 3 else {}
198
+ time_mode = argv[3] if len(argv) >= 4 else "logical"
199
+
200
+ problems = lint(env, config, time_mode=time_mode)
201
+ if not problems:
202
+ print("[consistency_lint][通过] environment/config 静态体检无问题。")
203
+ return 0
204
+ print("[consistency_lint][不通过] 发现 {} 个问题:".format(len(problems)), file=sys.stderr)
205
+ for p in problems:
206
+ print(" - " + p, file=sys.stderr)
207
+ return 1
208
+
209
+
210
+ if __name__ == "__main__":
211
+ sys.exit(main(sys.argv))