v3.3.0: 核心层回归扩容 + 案例层强化
SKILL.md: - Phase 1-5 详细动作从 references/ 拉回核心层 - 10 个场景速查回迁 - 经验法则扩到 24 条(含环境伪装编码纪律) - 工具名全部对齐 MCP v1.0.0 API - 版本记录移至 README.md cases/jsvmp-xhr-interceptor-env-emulation.md: - 追加禁动清单(5 个禁动对象) - 追加 UA 分支矩阵(Firefox/Chrome 必补/禁补) - 追加 6 条踩坑记录 + env-patch 最小化原则 cases/_template.md: - 追加禁动清单 + UA 分支矩阵可选段骨架 README.md: - MCP 工具表对齐 v1.0.0 API - 项目结构补充 cases/ 和完整 references/ - 追加版本记录 - 删除 skill不保存什么 段 references/: - phase-details.md / mcp-cookbook.md / experience-rules-full.md 顶部加以SKILL.md为准提示
This commit is contained in:
@@ -101,6 +101,22 @@
|
||||
|
||||
---
|
||||
|
||||
## 🚫 禁动清单(实战踩过的"不要碰",可选段)
|
||||
|
||||
> 如果本案例有"AI 演绎偏差导致踩坑"的经验,在此列出。每条格式:
|
||||
>
|
||||
> ### ❌ 禁动对象 N:___具体对象___
|
||||
> - **理由**:___为什么禁动(要有实测证据或技术原理)___
|
||||
> - **正确做法**:___如果真需要某种效果,替代方案是什么___
|
||||
|
||||
## 🌐 UA 分支矩阵(如涉及环境伪装,可选段)
|
||||
|
||||
> 如果本案例涉及 jsdom 环境伪装,列出 UA 分支的必补/禁补清单。
|
||||
>
|
||||
> ### UA 分支 1:___(如 Firefox UA)___
|
||||
> **必补**:...
|
||||
> **禁补**:...
|
||||
|
||||
## 踩坑记录
|
||||
|
||||
| # | 坑 | 现象 | 解决方法 |
|
||||
|
||||
@@ -76,6 +76,99 @@
|
||||
|
||||
jsdom 环境伪装,喂入-截出策略截获 a_bogus。环境值必须与 UA 自洽。
|
||||
|
||||
## 🚫 禁动清单(实战踩过的"不要碰")
|
||||
|
||||
> 以下 API / 全局对象 / 原型位置,**未经 `hook_function(mode='trace')` 或 `instrumentation` 证明 JSVMP 真的读了**,严禁在 env-patch 里改动。每一条都源自本案例的真实踩坑——改动后签名正常生成但服务端静默拒绝(HTTP 200 + 空 body)。
|
||||
|
||||
### ❌ 禁动对象 1:`win.Error` 构造函数
|
||||
|
||||
- **理由**:替换 `win.Error` 会让所有 `instanceof Error` 判断走向不确定,JSVMP 内部多处用 Error 做类型分发,替换会连锁打乱内部状态。**没有任何证据证明 JSVMP 读 Error.stack**(本案例实测)
|
||||
- **正确做法**:完全不动。如果担心 Error.stack 里的 Node.js 路径泄露,先用 `hook_function(path='Error', mode='trace')` 确认 JSVMP 真的调用了再说
|
||||
|
||||
### ❌ 禁动对象 2:任何 `constructor.name` 修改
|
||||
|
||||
- **理由**:改 `document.constructor.name = 'HTMLDocument'` 不会让 `document.constructor === window.HTMLDocument` 成立,反而让 `Object.prototype.toString.call(document.constructor)` 返回非标准字符串,得不偿失
|
||||
- **正确做法**:要让 `Object.prototype.toString.call(document) === '[object HTMLDocument]'`,**只走 `Symbol.toStringTag` 路线**(设到 `HTMLDocument.prototype` 和 `Document.prototype` 上)
|
||||
|
||||
### ❌ 禁动对象 3:`Object.prototype` 层的原型链修改
|
||||
|
||||
- **理由**:`Object.getPrototypeOf(Object.getPrototypeOf(doc))` 可能越过预期到达 `Object.prototype`,改那里会破坏全局 `Object.prototype.toString`,JSVMP 任何对象的 toString 调用都会跑进你的代码
|
||||
- **正确做法**:`scanPrototypeChain` 必须有 `if (proto === Object.prototype) break;` 边界
|
||||
|
||||
### ❌ 禁动对象 4:`Symbol.toStringTag` 设在实例上
|
||||
|
||||
- **理由**:`Object.prototype.toString.call(document)` 的算法是**原型链查找** `Symbol.toStringTag`。设在 `doc` 实例上对 `instanceof` / `constructor.name` 等相关检测不生效
|
||||
- **正确做法**:统一设到 prototype 上(`HTMLDocument.prototype` / `Document.prototype` / `Screen.prototype` / `Navigator.prototype` / `Performance.prototype`)
|
||||
|
||||
### ❌ 禁动对象 5:对 `Function.prototype.toString` 做第二次覆盖
|
||||
|
||||
- **理由**:整个 env-patch 只能有**一次** `win.Function.prototype.toString = function() {...}` 赋值。"三层防御"指**单次赋值的函数体内部**有三段检测逻辑(WeakSet + jsdom 源码正则 + 源码 fallback),**不是覆盖三次**。第二次覆盖会让命名函数暴露自身源码给 JSVMP
|
||||
- **正确做法**:写完第一次赋值后**不要**再写第二次"加固"。`grep -c "win.Function.prototype.toString *=" env-patch.js` 应该 = 1
|
||||
|
||||
### 行数自检
|
||||
|
||||
| 文件 | 合理范围 | 警报阈值 |
|
||||
|---|---|---|
|
||||
| env-patch-firefox.js | ~600-650 行 | > 800 行 |
|
||||
| env-patch-chrome.js | ~650-700 行 | > 850 行 |
|
||||
|
||||
超过警报阈值 → 跑上述 5 条禁动清单的 grep 自检,大概率有一条踩了:
|
||||
|
||||
```bash
|
||||
grep -c "win.Function.prototype.toString *=" env-patch.js # 必须 = 1
|
||||
grep -c "win.Error *=" env-patch.js # 必须 = 0
|
||||
grep -c "\.constructor.*name" env-patch.js # 必须 = 0
|
||||
```
|
||||
|
||||
## 🌐 UA 分支矩阵(必须按目标站点 UA 选分支,不跨分支混补)
|
||||
|
||||
> 环境值必须与 `navigator.userAgent` 自洽。Firefox UA 下补 Chrome 独有 API = 立即暴露。
|
||||
|
||||
### Firefox UA 分支(UA 含 `Firefox/XXX`)
|
||||
|
||||
**必补**(jsdom 缺失但真实 Firefox 有):
|
||||
|
||||
- ✅ `Notification`(构造函数 + `permission` + 静态 `requestPermission`)
|
||||
- ✅ `Worker` / `SharedWorker` / `RTCPeerConnection`
|
||||
- ✅ `AudioContext` / `OfflineAudioContext`(至少 stub `createOscillator` / `createDynamicsCompressor` / `startRendering`)
|
||||
- ✅ `matchMedia`(返回 `{matches:false, media:query}`)
|
||||
- ✅ `requestIdleCallback` / `cancelIdleCallback`(`setTimeout` 包装)
|
||||
- ✅ `indexedDB` / `caches` / `speechSynthesis` / `customElements` / `visualViewport`
|
||||
- ✅ `isSecureContext = true` / `clientInformation = navigator`
|
||||
- ✅ `WebSocket`(带静态常量 `CONNECTING / OPEN / CLOSING / CLOSED`)
|
||||
- ✅ `document.fonts` / `document.timeline`
|
||||
- ✅ `navigator.permissions.query`(stub) / `navigator.clipboard.readText/writeText`(stub)
|
||||
- ✅ `IntersectionObserver` / `ResizeObserver` / `PerformanceObserver` / `MessageChannel`(Firefox 有,jsdom 没有)
|
||||
- ✅ `structuredClone`(Firefox 有,jsdom 没有)
|
||||
|
||||
**禁补**(Chrome 独有,Firefox UA 下必须 `undefined`):
|
||||
|
||||
- ❌ `navigator.userAgentData`(Chrome 独有 Client Hints)
|
||||
- ❌ `navigator.connection`(Chrome 独有 Network Information API)
|
||||
- ❌ `navigator.getBattery`(Firefox 已从 Web 移除)
|
||||
- ❌ `window.chrome`(名字都带 chrome)
|
||||
- ❌ `performance.memory`(Chrome 独有)
|
||||
|
||||
### Chrome UA 分支(UA 含 `Chrome/XXX`)
|
||||
|
||||
**必补**(与 Firefox 大部分重合,额外加上):
|
||||
|
||||
- ✅ `navigator.userAgentData`(完整 NavigatorUAData,含 `getHighEntropyValues()`)
|
||||
- ✅ `navigator.connection`(`NetworkInformation` stub)
|
||||
- ✅ `window.chrome.{app, runtime, csi, loadTimes}`(完整 Chrome 对象)
|
||||
|
||||
**禁补**(Firefox 独有):
|
||||
|
||||
- ❌ `navigator.buildID`(Firefox 独有)
|
||||
- ❌ `InstallTrigger`(Firefox 独有标识符)
|
||||
|
||||
### 自检命令(Firefox UA 下)
|
||||
|
||||
```bash
|
||||
grep -c "userAgentData\|navigator\.connection\|getBattery\|window\.chrome\|performance\.memory" env-patch.js
|
||||
# 必须 = 0(Firefox UA 下)
|
||||
```
|
||||
|
||||
## 各 Phase 加速指引
|
||||
|
||||
- **Phase 1**: `search_code(keyword="_SdkGlueInit")` 定位配置;`scripts(action='save')` 保存三件套(webmssdk.es5.js / bdms.js / sdk-glue.js)到 `config/`
|
||||
@@ -298,6 +391,17 @@ function generateABogus(fullUrl, cookieStr) {
|
||||
| 11 | `resources: 'usable'` 是 JSVMP 拦截器激活的必要条件 | 不加 `resources: 'usable'` 时 SDK 加载成功、`byted_acrawler.frontierSign()` 能返回 X-Bogus,但 JSVMP 的 XHR 拦截器不触发(a_bogus 不生成) | jsdom 的 `resources: 'usable'` 选项会启用完整的 XHR 实现(含网络层),JSVMP 拦截器依赖这个完整实现才能正确 Hook `XMLHttpRequest.prototype.open`。不加此选项时 XHR 是简化版,拦截器无法正常工作。副作用是会尝试加载外部脚本导致 `SyntaxError: Unexpected token '<'`,但不影响 SDK 功能 |
|
||||
| 12 | `navigator.permissions` 和 `navigator.clipboard` 缺失 | 环境指纹哈希不匹配 | Firefox 浏览器有这两个 API,jsdom 没有。需要补充存根:`nav.permissions = { query: markNative(function query() { return Promise.resolve({ state: 'prompt' }); }) }` 和 `nav.clipboard = { readText/writeText 存根 }` |
|
||||
|
||||
### 踩坑记录(2026-04-19 抖音 a_bogus 二次复用补充)
|
||||
|
||||
| # | 坑 | 现象 | 解决方法 |
|
||||
|---|---|---|---|
|
||||
| 13 | `Function.prototype.toString` 被覆盖两次("三层防御"被误解为覆盖三段代码)| 签名 192 字符但服务端空 body | 整个 patch 有且仅有一次 `win.Function.prototype.toString = function() {...}` 赋值。三层防御指函数体内部的三段检测逻辑(WeakSet / jsdom 源码正则 / 源码 fallback),不是覆盖三次 |
|
||||
| 14 | `Symbol.toStringTag` 设在 `doc` 实例上而非 `HTMLDocument.prototype` | 签名 192 字符但服务端空 body | 设到 `win.HTMLDocument.prototype` 和 `win.Document.prototype`。不设实例、不 walk 到 `Object.prototype` |
|
||||
| 15 | 替换 `win.Error` 构造函数(为"清洗 stack 中 Node.js 路径")| 签名 192 字符但服务端空 body | 完全不动 Error。没有证据证明 JSVMP 读 stack,先证再动手 |
|
||||
| 16 | 修改 `document.constructor.name = 'HTMLDocument'` | 签名 192 字符但服务端空 body | 不动任何 `constructor.name`。用 `Symbol.toStringTag` 路线 |
|
||||
| 17 | Firefox UA 下补 Chrome 独有 API(`userAgentData / connection / getBattery / window.chrome / performance.memory`)| 签名 192 字符但服务端空 body | UA 与 API 矩阵必须自洽,见"UA 分支矩阵"段 |
|
||||
| 18 | env-patch 代码量爆炸(612 行 → 1030 行)| 签名生成正常但服务端静默拒绝 | 严守行数阈值,每加代码走"先证明需要 → 加 → 验证 → 保留;否则回退"循环,禁止"先加上保险" |
|
||||
|
||||
---
|
||||
|
||||
## 变体说明
|
||||
@@ -556,3 +660,17 @@ JSVMP 用自定义指令集执行,反编译成本极高且版本迭代频繁
|
||||
- **document 实例方法**:`createElement`、`createTextNode`、`createDocumentFragment`、`getElementById`、`getElementsByClassName`、`getElementsByTagName`、`querySelector`、`querySelectorAll`
|
||||
|
||||
遗漏任何一个都可能导致 JSVMP 检测到非原生环境,生成的 a_bogus 被服务端静默拒绝(HTTP 200 + 空 body)。
|
||||
|
||||
### 6. env-patch 最小化原则(2026-04-19 实测补充)
|
||||
|
||||
jsdom 环境伪装的 env-patch **行数越多越不稳**。每一行"保险代码"都可能引入新的泄露点。
|
||||
|
||||
**加代码的唯一合法循环**:
|
||||
|
||||
1. 用 `hook_function(mode='trace')` 或 `instrumentation(action='install')` 证明 JSVMP 确实读了某个 API / 属性
|
||||
2. 用 `compare_env` 确认 jsdom 中该 API 的值与真实浏览器不同
|
||||
3. 加最小化 stub(只 stub 真正被读的属性/方法)
|
||||
4. 跑真实请求验证成功
|
||||
5. 保留;否则立即回退
|
||||
|
||||
**禁止**:"先加上保险"式编码。没有证据的补丁 = 未知泄露点。
|
||||
|
||||
Reference in New Issue
Block a user