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,14 @@
1
+ # dsh-js-reverse-plugin bundle layer: one insert that mounts the skills
2
+ # registrar and the js-reverse-mcp server. Applied after dsh-base, before the
3
+ # profile's own cordis.patch.yml.
4
+ - insert:
5
+ - id: js-reverse-plugin
6
+ name: 'dsh-js-reverse-plugin'
7
+ inject: [skills]
8
+ - id: mcp-js-reverse
9
+ name: '@deepseek-ai/dsh-mcp-client'
10
+ config:
11
+ transport: stdio
12
+ serverName: js-reverse
13
+ command: npx
14
+ args: ['-y', 'js-reverse-mcp@4.0.1']
package/index.js ADDED
@@ -0,0 +1,61 @@
1
+ // dsh-js-reverse-plugin — one DeepSeek Harness bundle that mounts the
2
+ // js-reverse skills (AST deobfuscation / iv8 / JSVMP) as runtime skills.
3
+ // The js-reverse-mcp server is mounted by the cordis.patch.yml layer, so a
4
+ // single `dsh plugin add` installs skills + MCP tools together.
5
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
6
+ import { join } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { parse } from 'yaml'
9
+
10
+ export const name = 'js-reverse-plugin'
11
+
12
+ /** This bundle contributes skills, so it needs the skill registry. */
13
+ export const inject = ['skills']
14
+
15
+ const SKILLS_DIR = fileURLToPath(new URL('./skills/', import.meta.url))
16
+
17
+ /**
18
+ * Split one SKILL.md into its YAML frontmatter and the markdown body.
19
+ * @param raw - the raw SKILL.md text.
20
+ * @returns parsed frontmatter (or {}) and the body after the frontmatter.
21
+ */
22
+ export function splitFrontmatter(raw) {
23
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw)
24
+ if (match === null) return { data: {}, content: raw }
25
+ return { data: parse(match[1]) ?? {}, content: raw.slice(match[0].length) }
26
+ }
27
+
28
+ /**
29
+ * Collect the runtime skill registrations from the bundled skills directory.
30
+ * @returns one registration per `<name>/SKILL.md` directory.
31
+ */
32
+ export function collectSkills() {
33
+ const registrations = []
34
+ for (const entry of readdirSync(SKILLS_DIR, { withFileTypes: true })) {
35
+ if (!entry.isDirectory()) continue
36
+ const skillPath = join(SKILLS_DIR, entry.name, 'SKILL.md')
37
+ if (!statSync(skillPath, { throwIfNoEntry: false })?.isFile()) continue
38
+ const { data, content } = splitFrontmatter(readFileSync(skillPath, 'utf8'))
39
+ if (typeof data.name !== 'string' || typeof data.description !== 'string') continue
40
+ registrations.push({
41
+ name: data.name,
42
+ description: data.description,
43
+ whenToUse: typeof data.whenToUse === 'string' ? data.whenToUse : undefined,
44
+ content,
45
+ path: skillPath,
46
+ source: 'custom',
47
+ resourceBase: { kind: 'directory', path: join(SKILLS_DIR, entry.name) },
48
+ metadata: Object.fromEntries(
49
+ Object.entries(data).filter(([key]) => !['name', 'description', 'whenToUse'].includes(key)),
50
+ ),
51
+ })
52
+ }
53
+ return registrations
54
+ }
55
+
56
+ /** Register every bundled skill; each registration is a disposable effect. */
57
+ export function apply(ctx) {
58
+ for (const skill of collectSkills()) {
59
+ ctx.skills.register(skill)
60
+ }
61
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "dsh-js-reverse-plugin",
3
+ "version": "0.1.0",
4
+ "description": "JavaScript reverse-engineering bundle for DeepSeek Harness: AST deobfuscation / iv8 / JSVMP skills plus the js-reverse-mcp server tools.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "files": [
8
+ "index.js",
9
+ "cordis.patch.yml",
10
+ "skills"
11
+ ],
12
+ "dependencies": {
13
+ "yaml": "^2.4.0"
14
+ },
15
+ "dsh": {
16
+ "bundle": {
17
+ "patch": "./cordis.patch.yml"
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: iv8-env-runtime
3
+ description: >-
4
+ 强制使用 iv8(Python 原生的 C++ V8 + 浏览器环境运行时)作为目标 JavaScript 的唯一受控执行底座,
5
+ 配置浏览器环境并在本地运行目标反爬 JS,拿到签名 / token,
6
+ 并用 js-reverse MCP 从真实出口倒推定位加密 / 签名"生成"入口。分两部分:Part1 找入口(js-reverse MCP),
7
+ Part2 补环境跑通(iv8)。当任务涉及以下时使用:iv8、补环境、脱浏览器跑 JS、js-reverse 找入口、
8
+ 本地产签名 / token、JSContext、page.load、add_resource、eventLoop.drain、指纹配置、stub 窟窿、
9
+ navigator/canvas/webgl 一致性。不得使用 Node.js vm 或外部补环境框架替代 iv8;Node.js 只可作为静态工程工具。
10
+ 本 skill 不涉及 Akamai 真实鼠标轨迹 / 行为采集式的边界或内容。
11
+ ---
12
+
13
+ > **版本基线:iv8 0.1.3 / 2026-07(社区版,已实测)。动手前先跑 `scripts/preflight.py`。**
14
+ > 所有 deploy 绑定值(bdms 版本 / aid / script id / u 数组 / golden sum)一律运行时解析,勿写死。
15
+
16
+ # iv8-env-runtime — 用 iv8 补环境脱浏览器跑反爬 JS
17
+
18
+ 用 iv8 在纯 Python 里跑目标站的反爬 / 签名 JS,本地产出签名 / token,全程无浏览器。
19
+ 两大部分:**Part1** 用 js-reverse MCP 找到真实"生成"入口;**Part2** 用 iv8 补环境把入口跑通。
20
+
21
+ ---
22
+
23
+ ## §0 能做 / 不能做红线(常驻护栏,8 条铁律)
24
+
25
+ > 违反任意一条几乎必翻车。每次动手前扫一遍。
26
+
27
+ 1. **断点用 `vdebugger;`,绝不用 `debugger;`**(原生 debugger 已被 iv8 禁用,目标常用无限 debugger 反调试)。
28
+ 2. **console 检测站:`enable_console=False` + `vconsole.log`**(`vconsole` 对目标 JS 完全不可见)。
29
+ 3. **`environment` = 画像**(JS 可观测的指纹,可含数组)/ **`config` = 引擎行为**(仅标量,**无数组**:timezone / permissions 等)。两者别混。
30
+ 4. **默认值三坑**:`location.href` 默认 `about:blank`(**几乎必配**);`canvas.fingerprint.toDataURL.png` 默认 `''`(**canvas 指纹站必配**);`navigator` 有 200+ 合理默认(**通常只配 `userAgent`**)。
31
+ 5. **社区版无真实网络栈**:真请求走 Python `requests` → `ctx.add_resource()` 注入响应 → `eventLoop.drain()` 推进,而不是指望 iv8 自己发包。
32
+ 6. **API 三态**:真实现(navigator/crypto/XHR)**别动**;stub 空壳(如 `MessageChannel` port=null)**替换**;陷阱 / 正确缺失(`process`/`_phantom`/`__webdriver_script_fn`/`StyleMedia`/`Float16Array`)**绝不补**(补了自曝)。
33
+ 7. **一致性红线**:UA ↔ userAgentData ↔ platform ↔ 时区 ↔ 语言 ↔ webgl **整套对齐**。说自己是 Mac 就别让 platform 还是 Win32。
34
+ 8. **`time_mode`**:默认 `logical`(sleep 瞬间完成);**POW / 时间差校验站用 `system`**(Date.now() 反映真实耗时)。
35
+ 9. **唯一运行底座**:目标原始 JS 只在 iv8 中执行。真实浏览器用于定位入口、采集环境事实和提供对账样本,不替代 iv8 参考运行;Node.js vm 不进入目标执行链。
36
+
37
+ ---
38
+
39
+ ## §1 前置门(P0,硬门)
40
+
41
+ **必跑 `scripts/preflight.py`。不过不得继续。**
42
+
43
+ 它做四件事:① 检测平台 / Python 版本 / 架构,装错平台或版本硬阻断并给替代路径;② 打印已安装 iv8 版本(本 skill 版本门控的**唯一真相源**,`references/version-gotchas.md` 消费它);③ 校验 `import iv8` 与 `get_defaults()` 可用;④ 打印跨平台安装结论。
44
+
45
+ 安装矩阵详见 `references/cross-platform-install.md`。速记:Windows x64 / Linux x64 → `pip install iv8`(PyPI,Py3.9–3.14);macOS 仅 Apple Silicon 实验版,**不上 PyPI**,去 GitHub Releases 下 `iv8-<ver>-cpXX-cpXX-macosx_14_0_arm64.whl` → `uv pip install ./…whl`(Py3.11–3.14 / macOS14+);Intel Mac / mac 上 Py<3.11 = 不支持。项目强制用 **uv** 管理。
46
+
47
+ > **CHECKPOINT P0**:preflight 全绿、版本已打印,才进 Part1 / Part2。**STOP** 若阻断,按它给的替代路径处理,别硬跑。
48
+
49
+ ---
50
+
51
+ ## §2 双轨全景
52
+
53
+ ```
54
+ Part1 找入口(js-reverse MCP,真浏览器) Part2 补环境跑通(iv8,纯 Python)
55
+ 真实出口 → 倒推 → 定位"生成"入口 → 分类入口类型 → 最小 environment 起跑 → 发现缺口 → 配/补 → 收割 token → 验收
56
+ ```
57
+
58
+ **运行阶段模型(P0–P4,带门):**
59
+
60
+ | 阶段 | 做什么 | 退出闸 |
61
+ |---|---|---|
62
+ | **P0** 环境就绪 | 跑 preflight | 全绿 + 版本打印 |
63
+ | **P1** 找入口 | 从真实出口倒推,定位 + 分类入口 | entry-report 填满 & 结论"已定位" |
64
+ | **P2** 起跑 + 发现缺口 | 最小 environment 零配跑,列缺口清单 | 缺口清单成形(monitor∩defaults + TypeError 栈) |
65
+ | **P3** 配 / 补 / 跑通 | 喂指纹、替 stub、网络桥接、drain、收割 | 本地算出 token |
66
+ | **P4** 验收 | parity 对账 + **服务器判决** | 服务器 200 / code=0 |
67
+
68
+ > **P1 ↔ P2 ↔ P3 可来回迭代**,不是单向瀑布。入口分类错了回 P1;缺口没配全回 P2;服务器拒了回 P3。
69
+
70
+ ---
71
+
72
+ ## §3 Part1 — 找入口(5 步骨架)
73
+
74
+ 方法论:**不是先读懂混淆代码,而是让真实代码自己跑,在出口附近观察参数从无到有的边界。**
75
+ 完整流程见 `references/part1-entry-location.md`,MCP 工具映射见 `references/part1-mcp-tool-map.md`。
76
+
77
+ 1. **明确目标字段**:字段名、所在请求 / 存储位置、真实样例(`list_network_requests` 找真实出口)。
78
+ 2. **在真实出口下断点**:XHR/fetch(`break_on_xhr`)、cookie/storage setter、或目标函数返回点(`set_breakpoint_on_text`)。
79
+ 3. **从断点向上看调用栈,只给每层定角色**:加载 / 初始化 / 触发 / **生成** / 读取 / 搬运 / 发送。别硬读混淆。
80
+ 4. **定位铁证 =「上一层没有,下一层有」**(`select_frame` + `evaluate_script(mainWorld=true)` 对比改写前输入 / 改写后输出)。**不是**"出现在调用栈里"就算。
81
+ 5. **判来源 + 最小复现**:本地生成 / 已有状态搬运 / 服务端下发 / 未确认;按入口类型(函数式 / 拦截式 / 事件式 / 状态机式 / 采集式 / 下发式)做最小复现。
82
+
83
+ **产出**:填满 `assets/entry-report-template.txt`。参考案例:拦截式见 `references/case-intercept-douyin.md`,函数式见 `references/case-function-iwencai.md`。
84
+
85
+ > **CHECKPOINT P1**:entry-report 每格有动态边界证据、结论写"已定位"。**STOP** 禁止用"可能是 / 和某 SDK 有关 / 调用栈里出现过"当结论——那些一律记"未定位"。
86
+
87
+ ---
88
+
89
+ ## §4 Part2 — 补环境 SOP(P2 → P3 → P4)
90
+
91
+ 核心认知:**补环境 = 造环境(iv8 全包)+ 配环境(你喂指纹值)+ 偶尔补漏(替 stub 窟窿)。** 手写 window/DOM 的体力活没了。
92
+
93
+ ### P2 起跑 + 发现缺口
94
+
95
+ 从 `assets/env_starter.py` 起手:最小 environment(只 `location` + `navigator.userAgent`)零配跑。
96
+
97
+ - **发现清单 = monitor ∩ defaults 交集**:`scripts/monitor_env.py`(debug 模式)列出"它读 / 调了哪些字段",`scripts/defaults_audit.py` 查这些字段的 iv8 默认是不是占位(about:blank / 空)→ **交集就是要配的**。
98
+ - **stub 窟窿 = TypeError 栈**(typeof 正常、抓不到缺失日志,一用就崩):`scripts/reproduce_stub_error.py` 复现异常栈定位。
99
+ - 判 API 三态见 `references/part2-discovery.md`;配值 / 三态原则见 `references/patching-principles.md`。
100
+
101
+ > **CHECKPOINT P2**:缺口清单成形(要配的指纹 + 要替的 stub)。**STOP** 别 grep 源码枚举字段一次补一堆——靠监控 + 报错发现,一次一处。
102
+
103
+ ### P3 配环境 + 补漏 + 跑通
104
+
105
+ 1. **喂指纹值**:`location` 全字段填真实 URL、看站配 `canvas`/`webgl`/`window.origin`、读页面 DOM 的站用 `innerHTML` 或 `page.load` 填真实 HTML。配值前先 `scripts/consistency_lint.py` 静态拒错(UA↔platform↔时区 不一致直接拦)。
106
+ 2. **替换 stub**:MessageChannel/Worker null port 等,配 `wrapNative` + `eventLoop.advance`+`drain`(core-js microtask 降级链,见 `references/version-gotchas.md` E2)。
107
+ 3. **网络桥接**:`scripts/network_bridge.py` 固化 "JS 发请求 → 暂停 → Python requests → `add_resource` 注入 → drain 推进" 握手。细节见 `references/network-and-eventloop.md`。
108
+ 4. **drain-before-read**:`scripts/run_target.py` 内联 drain 断言——**收割 token 前必须先 drain**(异步产物没排空就读会拿到 undefined/半成品)。
109
+
110
+ > **CHECKPOINT P3**:本地稳定算出 token。**STOP** 遇 iv8 专坑先版本门控查 `references/version-gotchas.md`(E1 btoa 在 0.1.3 已修**别照抄**旧版注入 / E2 MessageChannel / E4 WASM instanceof+OOM 炸弹 / E5 audio 金标准 buffer / E6 jsvmp null-safe patch / E7 字体渲染物理量→真机采集后注回 iv8)。
111
+
112
+ ### P4 验收(服务器判决是唯一真相)
113
+
114
+ - **本地 parity**:`scripts/parity_check.py` 把本地产物与真机样本**逐字符**对账(din 顺序等强校验)。
115
+ - **服务器判决**:拿本地产的 token 真发一次,**只认服务器 200 / code=0**。本地自洽不算成功。
116
+
117
+ > **CHECKPOINT P4**:服务器 200 / code=0 → 收工,别画蛇添足。
118
+ > **STOP 分流**:算出但被拒 → 回 P3 查哪个指纹没对上(parity 第一处分叉常指向自检点);403 等请求层拒 → **多半非环境**(cookie/IP/参数过期),先排除非环境因素别乱补。
119
+
120
+ ---
121
+
122
+ ## §5 references / scripts 索引表
123
+
124
+ | 文件 | 何时开 / 何时跑 |
125
+ |---|---|
126
+ | `references/iv8-api-contract.md` | 要用 iv8 任何 API 前——只准用契约里列的,别臆造 |
127
+ | `references/cross-platform-install.md` | P0,装 iv8 / 换机器 / 平台不支持时 |
128
+ | `references/version-gotchas.md` | P3 遇专坑(E1–E7);消费 preflight 打印的版本做门控 |
129
+ | `references/config-defaults-cheatsheet.md` | P2/P3 查某字段默认是否占位、environment vs config 归属 |
130
+ | `references/part1-entry-location.md` | P1 找入口的完整 8 步流程 + 来源判定 + 禁止结论 |
131
+ | `references/part1-mcp-tool-map.md` | P1 每步该调哪个 js-reverse MCP 工具 |
132
+ | `references/part2-discovery.md` | P2 判 API 三态、四手段发现要配 / 补什么 |
133
+ | `references/patching-principles.md` | P3 配值 / 替 stub 的一致性 / 最小 / 一次一处原则 |
134
+ | `references/network-and-eventloop.md` | P3 网络桥接 + eventLoop drain/advance 时序 |
135
+ | `references/trap-blacklist.md` | P2/P3 判断某缺失 API 是陷阱(别补)还是真漏补 |
136
+ | `references/case-intercept-douyin.md` | 参考拦截式入口 + 抖音 bdms 补环境实录 |
137
+ | `references/case-function-iwencai.md` | 参考函数式入口 + 爱问财补环境实录 |
138
+ | `scripts/preflight.py` | **P0 必跑**,硬门 |
139
+ | `scripts/monitor_env.py` | P2 debug 监控,列"它访问了啥"(要配的清单一半) |
140
+ | `scripts/defaults_audit.py` | P2 查字段默认占位(要配的清单另一半) |
141
+ | `scripts/consistency_lint.py` | P3 配值前静态拒错,一致性红线 |
142
+ | `scripts/run_target.py` | P2/P3 起跑目标 JS,内联 drain-before-read 断言 |
143
+ | `scripts/network_bridge.py` | P3 固化真请求 → add_resource → drain 握手 |
144
+ | `scripts/reproduce_stub_error.py` | P2 复现 stub 窟窿的 TypeError 栈 |
145
+ | `scripts/parity_check.py` | P4 本地产物 vs 真机逐字符对账 |
146
+ | `assets/env_starter.py` | P2 起跑模板(最小 environment) |
147
+ | `assets/entry-report-template.txt` | P1 产出,填满即退出 P1 |
148
+
149
+ ---
150
+
151
+ ## §6 iv8 唯一运行底座
152
+
153
+ 不要另造浏览器环境。iv8 已提供 `window`、DOM、BOM、XHR、Canvas、WebGL、Crypto、Timer 和事件循环;本 skill 只处理三类差异:
154
+
155
+ 1. **画像值不符**:通过 `JSContext(environment=..., config=...)` 配置,并用 `consistency_lint.py` 拦截自相矛盾。
156
+ 2. **已证实的 stub 空壳**:把补丁作为目标外部的 `stub_setup_js` 在原始 JS 之前注入;只实现目标实际调用的行为,函数或构造器用 `__iv8__.wrapNative` 保持原生表面,异步行为交给 `eventLoop.advance/drain`。
157
+ 3. **iv8 不具备的物理量**:在真实浏览器采集原始测量值,把它声明为环境输入后注入 iv8;最终目标 JS、生成入口和产物仍在 iv8 中运行。
158
+
159
+ 未知 API 默认保持 `undefined`。只有同时具备“真实 Chrome 确实存在、目标确实读取、iv8 当前实现不满足”三项证据时才补。不要导入通用 mock 框架,不要批量构造 window/DOM,不要把真实浏览器或其他 JavaScript 运行时当成 iv8 的替代执行链。具体判据与最小补丁结构见 `references/patching-principles.md` 和 `references/trap-blacklist.md`。
@@ -0,0 +1,23 @@
1
+ ========================================================================
2
+ P1 入口定位结论报告(模板)
3
+ 用法:逐字段填满。三条边界证据(上一层没有 / 下一层已有 / 中间结果==最终结果)
4
+ 必须同时齐备,末行才允许写「已定位」;任一字段只有猜测或调用栈痕迹 → 写「未定位」。
5
+ 说明见 references/part1-entry-location.md §7-8。
6
+ ========================================================================
7
+
8
+ 目标字段: (字段名 + 位置:URL query / header / body / cookie / storage + 真实样例值)
9
+ 真实出口: (URL / XHR.open / fetch / sendBeacon / cookie setter / storage setter / return / callback)
10
+ 直接相关 JS: (脚本 URL + 生成帧函数名;scriptId 会过期,记 URL)
11
+ 入口类型: (函数式 / 拦截式 / 事件式 / 状态机式 / 采集式 / 下发式)
12
+ 来源类型: (本地生成 / 已有状态搬运 / 服务端下发 / 未确认)
13
+ 前置状态: (触发前 JS 是否加载、init 是否完成、cookie/storage/config 是否存在)
14
+ 主动触发方式: (调函数 / 触发 XHR/fetch / 事件 / timer / 队列 / getter)
15
+ 输入参数: (this、arguments、URL、method、headers、body、config、事件对象)
16
+ 上一层没有的证据: (上一层输入里目标字段不存在的动态观察)
17
+ 下一层已有的证据: (下一层输出里目标字段已出现的动态观察)
18
+ 中间结果==最终结果证据:(哪个变量/返回值逐字符等于最终出口值)
19
+ 输出读取方式: (从 URL/header/body/cookie/storage/netLog/return/callback 的哪里读)
20
+ 对照实验: (不触发/触发/改输入/清状态/重放后的对照结果)
21
+
22
+ 结论: (已定位 / 未定位)
23
+ ========================================================================
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ env_starter.py — iv8 environment 起手模板(阶段 P2 零配起手)
5
+
6
+ 用途:
7
+ 给一个「最小可跑」的 environment 起手 dict,只填几乎必配的两项:
8
+ - location 全字段(iv8 默认 location.href 是 about:blank,几乎必须覆盖成真实页面 URL);
9
+ - navigator.userAgent(对外浏览器版本由它决定)。
10
+ canvas / webgl / DOM / screen 等一律「看站再加,别一次补全」——
11
+ P2 的做法是先零配跑、用 monitor∩defaults 交集找出真正被读到的字段,再按需补,
12
+ 过度预配反而制造不一致红线。
13
+
14
+ 用法:
15
+ from env_starter import make_environment
16
+ env = make_environment(
17
+ href="https://www.example.com/path?x=1",
18
+ user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...",
19
+ )
20
+ # 交给 scripts/consistency_lint.py 体检后再喂 iv8.JSContext(environment=env)
21
+
22
+ 本文件属于 iv8-env-runtime skill。
23
+ 交叉引用:
24
+ - 字段归属与默认值见 references/config-defaults-cheatsheet.md;
25
+ - 一致性红线(UA↔platform↔时区↔语言↔webgl)先跑 scripts/consistency_lint.py;
26
+ - canvas/webgl 等指纹值必须来自真实浏览器证据,并通过 iv8 environment/config 注入。
27
+ 说明:下方所有具体值均为「仅示例」占位,真实站点请按 P1/P2 观测到的真机值替换。
28
+ """
29
+
30
+ try: # 仅用于类型/URL 解析,缺失也不影响 make_environment 被直接改字段字典使用
31
+ from urllib.parse import urlsplit
32
+ except Exception: # noqa: BLE001
33
+ urlsplit = None
34
+
35
+
36
+ def _split_location(href):
37
+ """把一个页面 URL 拆成 iv8 location 各字段。href 是 location 里最关键的一项。"""
38
+ if urlsplit is None:
39
+ # 极简兜底:至少把 href 填上,其余留空由调用方补
40
+ return {"href": href, "origin": "", "protocol": "", "host": "",
41
+ "hostname": "", "port": "", "pathname": "", "search": "", "hash": ""}
42
+ u = urlsplit(href)
43
+ origin = "{}://{}".format(u.scheme, u.netloc) if u.scheme and u.netloc else ""
44
+ return {
45
+ "href": href,
46
+ "origin": origin,
47
+ "protocol": (u.scheme + ":") if u.scheme else "",
48
+ "host": u.netloc,
49
+ "hostname": u.hostname or "",
50
+ "port": str(u.port) if u.port else "",
51
+ "pathname": u.path or "",
52
+ "search": ("?" + u.query) if u.query else "",
53
+ "hash": ("#" + u.fragment) if u.fragment else "",
54
+ }
55
+
56
+
57
+ def make_environment(href, user_agent, language="en-US", languages=None):
58
+ """构造起手 environment。只配必需项;其余看站再加。
59
+
60
+ 参数:
61
+ href : 真实页面 URL(必配;iv8 默认 about:blank 几乎总要覆盖)。
62
+ user_agent : navigator.userAgent(必配;决定对外浏览器版本)。
63
+ language / languages : 语言,注意 language 应是 languages[0](红线,见 consistency_lint)。
64
+ """
65
+ if languages is None:
66
+ languages = [language, "en"]
67
+ return {
68
+ # ★ 必配:location 默认 about:blank,站点脚本读 location.* 时必须是真实页面
69
+ "location": _split_location(href),
70
+ # ★ 必配:navigator.userAgent 决定对外浏览器版本;其余 navigator 字段有 200+ 默认值,
71
+ # 通常只需覆盖 UA(以及和 UA 自洽的 language/languages)。
72
+ "navigator": {
73
+ "userAgent": user_agent,
74
+ "language": language,
75
+ "languages": languages,
76
+ },
77
+ # 以下为「看站再加」示例,默认注释掉——不要一次补全,过度预配会制造不一致红线:
78
+ # canvas 站点必配 canvas.fingerprint.toDataURL.png(默认 ''):
79
+ # "canvas": {"fingerprint": {"toDataURL": {"png": "<真机抓的 dataURL,仅示例,勿写死>"}}},
80
+ # webgl 指纹(须与 UA 显卡语义自洽):
81
+ # "webgl": {"vendor": "Google Inc. (NVIDIA)",
82
+ # "renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 ...)"},
83
+ # 屏幕:
84
+ # "screen": {"width": 1920, "height": 1080, "colorDepth": 24},
85
+ }
86
+
87
+
88
+ # 一个可直接参考/拷贝的示例实例(值均为占位,真实站点务必替换)。
89
+ STARTER_ENVIRONMENT_EXAMPLE = make_environment(
90
+ href="https://www.example.com/page", # 仅示例
91
+ user_agent=( # 仅示例,按真机替换
92
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
93
+ "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
94
+ ),
95
+ )
96
+
97
+
98
+ if __name__ == "__main__":
99
+ import json
100
+ print(json.dumps(STARTER_ENVIRONMENT_EXAMPLE, ensure_ascii=False, indent=2))
@@ -0,0 +1,96 @@
1
+ # 案例 · 函数式:爱问财 hexin-v(P1→P4 端到端)
2
+
3
+ > 函数式入口标本:目标 token 是一个**纯函数**的返回值,把输入喂对就逐字符复现,
4
+ > 与时间/鼠标键盘/cookie/刷新逻辑全无关。演示 P1 用断点证「纯函数」、P4 用「相同输入 ⟹
5
+ > 相同输出」当对账契约。deploy 绑定值(脚本 deploy 号 / `n(328)`/`n(598)` 方法名映射 /
6
+ > `TOKEN_SERVER_TIME` / token 样例)**仅示例,运行时从当前脚本解析,勿写死、勿跨 deploy 抄**。
7
+
8
+ ---
9
+
10
+ ## P1 定位:断点证 hexin-v 是 u 数组的纯函数
11
+
12
+ **目标字段**:请求 **header** 里的 `hexin-v`(位置在 header,不在 URL/body——先看清)。
13
+
14
+ **生成链路**(断点实证,调用栈自顶向下):
15
+ ```
16
+ XMLHttpRequest.open → A() → M.update() → i() → encode
17
+ ```
18
+ `i()` 是跑 switch 的内部函数:先 refresh(刷新 `u` 的动态槽位:时间 `u[2]`、事件
19
+ `u[7..12]`、计数器 `u[16]++`…),最后 `case"2": return h[n(328)](i)` 做 encode 返回 token。
20
+ encode 的输入 `i = u[n(598)]()`,即 `u` 自身的序列化。
21
+
22
+ **下断点 → 防重入 → 帧内快照**(对应 part1-mcp-tool-map §3-B/C):
23
+ 1. 在脚本 `u[16]++` 处 `set_breakpoint_on_text` 下断点。
24
+ 2. 触发一次 XHR `open()`(页面自身或手动 `new XMLHttpRequest().open('GET','/x')`)让执行停在
25
+ `i()` 帧。
26
+ 3. **`remove_breakpoint` 移除断点**——否则下一步嵌套调用会再次暂停死锁。
27
+ 4. 帧内 `evaluate_script`(用箭头函数取帧内局部)做快照与验证:
28
+ ```js
29
+ // 帧内可访问:u(索引可读写)、n(字符串解码器)、h(encode 模块)、M.update、z(="hexin-v")
30
+ const origNow = Date.now;
31
+ Date.now = () => 1782744000000; // 冻结时间 → u[2] 固定,排除时间干扰
32
+ u[16] = 4999; const T0 = M.update(); // ++ 后 = 5000
33
+ const S0 = []; for (let k=0;k<18;k++) S0[k] = u[k];
34
+ u[16] = 4999; const T1 = M.update();
35
+ u[16] = 4999; const T2 = M.update(); // 期望 T0===T1===T2
36
+ // 纯函数验证:设回 S0 直接 encode,绕过 refresh
37
+ const enc = s => { for (let k=0;k<18;k++) u[k]=s[k]; return h[n(328)](u[n(598)]()); };
38
+ const T_direct = enc(S0); // 期望 === T0
39
+ Date.now = origNow; // 还原
40
+ ```
41
+
42
+ **关键实测(P1 铁证)**:把 refresh 整段绕过、直接用一组固定 `u` 喂 encode,token 仍逐字符
43
+ 复现 ⇒ encode 不读 `u` 以外任何动态源(不读 Date/Math.random/事件/cookie)。这就是
44
+ 「中间结果 == 最终结果」的边界证据,且改任一 `u[k]`(如 `u[3]`、`u[16]`)token 就变,
45
+ 证明它确实依赖全部 `u` 且是确定性纯函数。
46
+
47
+ `u` 索引语义(**仅示例**):`u[0]`=cookie `v` 种子;`u[1]`=serverTime 常量;`u[2]`=秒级时间;
48
+ `u[3]`=UA 哈希;`u[4/5/6]`=屏幕指纹;`u[7..12]`=键鼠事件计数;`u[13]`=浏览器特征位;
49
+ `u[16]`=per-call 自增计数器;`u[17]`=常量 3。注意 `u` 不是原生数组
50
+ (`Array.isArray(u)===false`、无 `.length`),只能 `u[k]` 索引逐个读写。
51
+
52
+ P1 结论落 `assets/entry-report-template.txt`:入口类型=函数式,来源类型=本地生成,
53
+ 结论=已定位。
54
+
55
+ ---
56
+
57
+ ## P2/P3:iv8 复现 = 直接调函数(对账解耦成两层)
58
+
59
+ 函数式复现骨架比拦截式简单——不装 hook、不发 XHR,**直接调 encode**。补环境职责因此
60
+ 退化为一件事:**让 iv8 里的 `u` 数组等于浏览器里的 `u`**。对账被解耦成两层:
61
+
62
+ | 层 | 内容 | 是否需要补环境 |
63
+ |---|---|---|
64
+ | A. encode 纯函数 | 喂相同 `u` → 相同 token | 否,同一份脚本在受控 iv8 中必须与浏览器样本一致 |
65
+ | B. 环境 → u | `u[0]`=cookie、`u[3]`=UA、`u[4/5/6/13]`=指纹… | 是,这才是补环境真正要做的 |
66
+
67
+ **P2**:iv8 加载**同一 deploy** 的 chameleon 脚本;A 层零补环境就该过(过不了说明脚本没
68
+ 正确加载/运行)。**P3**:补 `location`/`navigator`(UA)/指纹,让 B 层各槽位 == 浏览器;
69
+ 逐槽对账,差哪槽补哪槽(iv8 原生配置与补丁契约见 `references/patching-principles.md`)。
70
+
71
+ ---
72
+
73
+ ## P4 验收:相同输入 ⟹ 逐字符相同 token(+ 服务器判决)
74
+
75
+ **A 层对账契约**(铁判据):iv8 里把 `u[0..17]` 设成采样快照的 18 个整数,调
76
+ `h[n(328)]( u[n(598)]() )`,**必须逐字符等于**采到的 token 样例(**仅示例**):
77
+ ```
78
+ u = [100,1782743000,1782744000,200,7,10,5,0,0,0,0,0,0,3748,0,0,5000,3]
79
+ ⟹ A8jIp4oFNqnBHVotsalEM-aFnz3fcSx7DtUA_4J5FMM2XWZjKoH8C17lwwfR
80
+ ```
81
+ 对到 18 个整数,token 自动对——不必去赌最终那串 token。A 层通过后再让 B 层把 `u` 逐槽
82
+ 对齐浏览器。
83
+
84
+ **最终真相仍是服务器**:把带对齐 `u` 产出的 `hexin-v` 放进 header 发真包,服务器响应正常
85
+ (可接受标准)或与浏览器完全一致(最强标准)才算成功;本地自洽不算。算出被拒 → 回 P3
86
+ 定位差异槽位(`u[0]`/`u[13]` 定位法)。`403` 非环境优先排查。
87
+
88
+ ---
89
+
90
+ ## 陷阱
91
+
92
+ - **绑定 deploy**:encode 算法、`n(328)/n(598)` 方法名映射、`TOKEN_SERVER_TIME` 都随 deploy
93
+ 变。iv8 与浏览器必须用**同一份脚本**对账;换 deploy 重新采标准答案。方法名要在当前脚本
94
+ 运行时里取,不能跨 deploy 硬抄。
95
+ - 冻结 `Date.now` 只为 P1 排除时间干扰、证纯函数;证完即还原,避免影响页面。
96
+ - `u` 非原生数组:别用 `Array.from(u)`(得空),逐 `u[k]` 读写。
@@ -0,0 +1,133 @@
1
+ # 案例 · 拦截式:抖音 a_bogus(P1→P4 端到端)
2
+
3
+ > 拦截式入口标本:目标签名不是「调函数返回」,而是 init 装 XHR hook 后、发请求那一刻
4
+ > 被注入 URL。演示 P1 用 MCP 三招递进找到 bdms,P2/P3 用 iv8 装 hook + 触发 + 捞 netLog。
5
+ > 全部 deploy 绑定值(bdms 版本 / aid / pageId / paths / a_bogus 样例)**仅示例,运行时
6
+ > 从真机解析,勿写死**。
7
+
8
+ ---
9
+
10
+ ## P1 定位:三招递进找到 bdms(拦截式)
11
+
12
+ **目标字段**:URL query 里的 `a_bogus`。删掉重放 → 被拦/空数据 ⇒ 确认是门票。
13
+ 位置在 **URL query**(不是 header/body),这决定了往「网络出口」方向找。
14
+
15
+ **招① 搜参数名**(`search_in_sources("a_bogus")`)→ `No matches found`。
16
+ 反爬藏了名字,是常态,不卡住,换招(见 part1-mcp-tool-map §3-A)。
17
+
18
+ **招② 堵网络出口**(`evaluate_script(mainWorld=true)`):
19
+ ```js
20
+ XMLHttpRequest.prototype.open.toString() // 不是 [native code],被改写成安全代理
21
+ window.fetch.toString()
22
+ ```
23
+ `open` 被改写成 `function(){ this.secureOpenArgs=arguments; this.ucProxyParam={...};
24
+ t.nativeXMLHttpRequestOpen.apply(this,arguments) }` ⇒ 网络出口被安全代理接管,`a_bogus`
25
+ 在这条代理链里注入。
26
+
27
+ **招③ 搜 hook 明文胎记**:hook 自用属性名 `secureOpenArgs`、`nativeXMLHttpRequestOpen`
28
+ 没被混淆 → `search_in_sources("secureOpenArgs")` 命中字节 `@byted/uc-secure-dtrait-core`,
29
+ 顺藤摸到真正算签名的模块 **bdms**。
30
+
31
+ **等价快招**:`break_on_xhr`(带 a_bogus 的请求) → `get_paused_info` 看调用栈 → 栈里直接
32
+ 显示安全 SDK wrapper → 一步到 SDK。
33
+
34
+ **判类型**:`evaluate_script` 求 `Object.keys(window.bdms)` → 只有 `["getReferer","init"]`,
35
+ 没有 `sign(url)`。没有签名函数 + 出口被 hook ⇒ **拦截式**。
36
+ 对照实验(P1 铁证):同一 XHR,不 init → URL 无 a_bogus;init 后 → URL 自动带 a_bogus。
37
+ 这就是「上一层没有、下一层有」的动态边界。
38
+
39
+ **抓 init 的 config**(拦截式必须拿对 config,否则 hook 装不对):
40
+ init 是 webpack 转发壳 `function(){return X(e,this,arguments,r)}`,所有 export 共用 →
41
+ 下**条件断点**只在真 init 命中:
42
+ ```
43
+ set_breakpoint_on_text("return X(e,this,arguments,r)",
44
+ condition="arguments[0]&&arguments[0].paths&&arguments[0].aid")
45
+ navigate(reload) → 命中
46
+ evaluate(frameIndex=0, "() => arguments[0]") // 箭头函数取暂停帧实参 = config
47
+ ```
48
+ 抓到(**仅示例**):`{"aid":6383,"pageId":6241,"paths":["^/aweme/v1/",...],"boe":false,
49
+ "ddrt":8.5,"ic":8.5}`。唯一要上心的是 `paths`——确认 `^/aweme/v1/` 命中你的接口
50
+ `/aweme/v1/web/aweme/detail/`;其余字段照搬真机值。
51
+
52
+ P1 结论落 `assets/entry-report-template.txt`:入口类型=拦截式,来源类型=本地生成
53
+ (hook 现场算),结论=已定位。
54
+
55
+ ---
56
+
57
+ ## P2 起手跑 + 发现缺口
58
+
59
+ 最小 environment 零配跑 bdms(location 默认 about:blank 必配;navigator 配 UA)。
60
+ 加载 bdms 直接崩:`TypeError: Cannot set properties of null (setting 'onmessage')`。
61
+ 这是**报错驱动**发现的 stub 窟窿——iv8 的 `MessageChannel` 看起来存在,但
62
+ `new MessageChannel().port1/port2` 是空壳。定位见 `references/trap-blacklist.md` 的
63
+ API 三态(stub 空壳→替换)。
64
+
65
+ ---
66
+
67
+ ## P3 配环境 + 补漏 + 跑通
68
+
69
+ ```python
70
+ import iv8
71
+ from urllib.parse import urlencode
72
+
73
+ environment = {
74
+ "location": { # 必配,默认 about:blank 会让签名上下文错
75
+ "href": "https://www.douyin.com/video/<id>", "origin": "https://www.douyin.com",
76
+ "protocol": "https:", "host": "www.douyin.com", "hostname": "www.douyin.com",
77
+ "port": "", "pathname": "/video/<id>", "search": "", "hash": "",
78
+ },
79
+ "navigator": {"userAgent": "Mozilla/5.0 ... Chrome/124.0.0.0 Safari/537.36",
80
+ "language": "en-US", "languages": ["en-US", "en"]},
81
+ }
82
+
83
+ with iv8.JSContext(environment=environment) as ctx:
84
+ # ① 替换 MessageChannel stub 空壳(wrapNative 伪装成原生,降检测差异)
85
+ ctx.eval("""
86
+ window.MessageChannel = __iv8__.wrapNative(function(){
87
+ const p1={onmessage:null}, p2={onmessage:null};
88
+ p1.postMessage=d=>{ if(p2.onmessage) setTimeout(()=>p2.onmessage({data:d}),0); };
89
+ p2.postMessage=d=>{ if(p1.onmessage) setTimeout(()=>p1.onmessage({data:d}),0); };
90
+ return {port1:p1, port2:p2};
91
+ },'MessageChannel');
92
+ """)
93
+ ctx.eval(open("examples/js/bdms_1.0.1.19.js", encoding="utf-8").read()) # ② 加载 SDK
94
+ ctx.eval('window.bdms.init({...抓到的 config 原样照搬...});') # ③ 装 hook
95
+ entries = ctx.eval(f''' # ④ 触发 + 捞
96
+ var xhr=new XMLHttpRequest();
97
+ xhr.open("GET","{url}?{urlencode(params)}",true);
98
+ xhr.setRequestHeader("Content-Type","application/json, text/plain, */*");
99
+ xhr.send(null);
100
+ window.__iv8__.netLog.entries;
101
+ ''', to_py=True)
102
+
103
+ signed_url = entries[0]["url"] # ← 带 a_bogus 的最终 URL(hook 副作用产出,非 bdms 返回值)
104
+ ```
105
+
106
+ MessageChannel 走 core-js microtask 降级链时若不产出,需配 `wrapNative` +
107
+ `eventLoop.advance`/`drain` 推进,见 `references/network-and-eventloop.md`。
108
+
109
+ ---
110
+
111
+ ## P4 验收:服务器判决唯一真相
112
+
113
+ ```python
114
+ resp = requests.get(signed_url, headers=headers, cookies={"ttwid": "..."})
115
+ print(resp.status_code) # 200 + 拿到真实视频详情 JSON ⇒ 服务器认账
116
+ ```
117
+
118
+ - **200 = 服务器端判决**,证明本地 a_bogus 通过抖音真实风控,不是本地自欺。
119
+ - 本地 netLog 有 a_bogus 但**不算成功**——必须服务器认账。算出被拒 → 回 P3 对齐指纹;
120
+ `403` 多半非环境(cookie/ttwid/IP/参数过期),先排除非环境因素再谈指纹对齐。
121
+
122
+ ---
123
+
124
+ ## 排错速查
125
+
126
+ | 现象 | 说明 | 下一步 |
127
+ |---|---|---|
128
+ | 搜参数名 No matches | 反爬藏名(常态) | 换堵网络出口招 |
129
+ | open 是 [native code] | 没被 hook 或 hook 在别处 | 查 send/fetch/setRequestHeader 或下 XHR 断点看栈 |
130
+ | 断点命中无数次 | webpack 转发壳共用 | 加 condition 只在 paths+aid 时停 |
131
+ | 跑崩 TypeError null onmessage | 撞 MessageChannel stub | 替换该 stub |
132
+ | netLog 有记录无 a_bogus | 未 init / paths 不命中 / 没真触发 | 逐条核对 init、paths、xhr、读 netLog |
133
+ | URL 有 a_bogus 但请求 403 | 多半非环境 | 先查 cookie/ttwid/IP/参数过期 |