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,773 @@
1
+ # iv8 最小动态取证
2
+
3
+ 本文只解决一个问题:
4
+
5
+ > 当一次 AST 解混淆缺少某个运行事实时,怎样用 iv8 在不修改原始 JavaScript 文件的前提下取得最小充分证据,并把证据带回静态分析主线。
6
+
7
+ 本文不是 iv8 安装手册、完整补环境教程、网络爬虫模板或 token 运行框架。需要系统补环境、真实网络桥接或完整运行验收时,推荐交接专门的 iv8 运行环境 Skill。
8
+
9
+ ## 目录
10
+
11
+ 1. [读取时机](#1-读取时机)
12
+ 2. [证据任务先于工具](#2-证据任务先于工具)
13
+ 3. [原始文件与运行产物](#3-原始文件与运行产物)
14
+ 4. [版本门控与最小冒烟](#4-版本门控与最小冒烟)
15
+ 5. [L0~L4 观测阶梯](#5-l0l4-观测阶梯)
16
+ 6. [L0:引擎层旁路监控](#6-l0引擎层旁路监控)
17
+ 7. [捕获 fd1 与 fd2](#7-捕获-fd1-与-fd2)
18
+ 8. [netLog 的准确边界](#8-netlog-的准确边界)
19
+ 9. [L1:DevTools 与 API watch](#9-l1devtools-与-api-watch)
20
+ 10. [L2:独立 harness 与 trigger](#10-l2独立-harness-与-trigger)
21
+ 11. [L3:独立 probe](#11-l3独立-probe)
22
+ 12. [L4:派生副本插桩](#12-l4派生副本插桩)
23
+ 13. [runtime patched 与 A/B](#13-runtime-patched-与-ab)
24
+ 14. [事件循环与异步结果](#14-事件循环与异步结果)
25
+ 15. [动态证据映射回 AST](#15-动态证据映射回-ast)
26
+ 16. [证据记录格式](#16-证据记录格式)
27
+ 17. [分流与返回主线](#17-分流与返回主线)
28
+ 18. [禁止事项](#18-禁止事项)
29
+
30
+ ## 1. 读取时机
31
+
32
+ 只在以下条件同时满足时读取并使用本文:
33
+
34
+ - 当前候选已被分类为 `dynamic` 或 `hybrid`。
35
+ - 静态分析已经定位结构,但缺少明确的运行事实。
36
+ - 缺失事实依赖浏览器 API、初始化顺序、闭包状态、实际分支、事件循环或运行期属性。
37
+
38
+ 可以在向用户提交决策卡之前读取本文,用它设计最小实验、说明风险和执行范围。读取 reference 不需要批准;真正创建或执行 iv8 harness、trigger、probe 或插桩副本前,必须取得用户对本次动态取证范围的批准。
39
+
40
+ 以下情况不应使用 iv8:
41
+
42
+ - AST、作用域、binding、引用关系或确定性语义已经足够证明结论。
43
+ - decoder 可以作为纯函数在更小的受控环境中执行。
44
+ - 只是为了“看看运行起来会发生什么”,尚未定义证据问题。
45
+ - 已经进入 VM Opcode、PC、值栈、字节码或虚拟控制流。
46
+ - 必须依赖真实字体、布局、音频渲染、浏览器网络栈或现有页面会话。
47
+
48
+ iv8 是证据提供者,不是解混淆主线。
49
+
50
+ ## 2. 证据任务先于工具
51
+
52
+ 开始前先写清:
53
+
54
+ ```text
55
+ 当前问题:
56
+ 静态已经证明:
57
+ 唯一或有限的事实缺口:
58
+ 为什么必须运行:
59
+ 最小执行入口:
60
+ 需要观察的值、顺序、异常或调用:
61
+ 成功条件:
62
+ 显式不证明:
63
+ ```
64
+
65
+ 不接受以下模糊目标:
66
+
67
+ ```text
68
+ 跑一下看看。
69
+ 把日志全打出来。
70
+ 动态分析整个文件。
71
+ 看看 iv8 能不能解。
72
+ ```
73
+
74
+ 动态取证应回答有限问题,例如:
75
+
76
+ ```text
77
+ 固定输入 X 下,decoder D(17) 的返回值是什么?
78
+ public init 调用后,哪个宿主 API 被替换?
79
+ 请求触发时,最终 URL 新增了哪个参数?
80
+ 闭包变量 K 在调用 decoder 前的实际值是什么?
81
+ ```
82
+
83
+ 一次运行只能证明声明输入、环境和覆盖路径下的事实,不能自动推广到全部输入或全部分支。
84
+
85
+ ## 3. 原始文件与运行产物
86
+
87
+ 至少区分以下角色:
88
+
89
+ ```text
90
+ target.original.js
91
+ 固定 hash 的原始运行目标;只读,不格式化,不插日志。
92
+
93
+ target.formatted.js
94
+ Prettier 生成的阅读副本;只用于阅读和建立源码坐标。
95
+
96
+ analysis.current.js
97
+ 当前 AST 分析视图;可以不可执行,不作为原始行为证据。
98
+
99
+ harness.py
100
+ 创建 Context、加载目标、捕获日志和保存证据。
101
+
102
+ trigger.js
103
+ 独立触发文件;只调用本次批准的入口。
104
+
105
+ probe.js
106
+ 独立运行时探针;只有 L3 使用。
107
+
108
+ target.instrumented.js
109
+ 独立插桩副本;只有 L4 使用。
110
+
111
+ runtime.raw.log
112
+ 原始引擎或调试日志。
113
+
114
+ runtime-evidence.json
115
+ 结构化证据、环境、覆盖范围和源码映射。
116
+ ```
117
+
118
+ 每次运行前重新计算并核对 `target.original.js` 的 hash。
119
+
120
+ 动态执行默认加载原始文件:
121
+
122
+ ```python
123
+ ctx.eval(original_source, name="target.original.js")
124
+ ```
125
+
126
+ 不要为了方便断点而执行格式化副本;格式变化可能触发 `toString()`、校验和或文件 hash 检测。
127
+
128
+ ## 4. 版本门控与最小冒烟
129
+
130
+ iv8 是编译扩展,API 和已知 stub 可能随版本变化。不要凭记忆应用补丁。
131
+
132
+ 每次新工作区至少记录:
133
+
134
+ ```text
135
+ 操作系统与架构
136
+ Python 版本
137
+ iv8 实际版本
138
+ import 是否成功
139
+ JSContext 冒烟结果
140
+ 本次使用的 API
141
+ ```
142
+
143
+ 最小冒烟:
144
+
145
+ ```python
146
+ import platform
147
+ import sys
148
+
149
+ import iv8
150
+
151
+ version = getattr(iv8, "__version__", "<unknown>")
152
+ print("platform:", platform.platform())
153
+ print("python:", sys.version)
154
+ print("iv8:", version)
155
+
156
+ with iv8.JSContext() as ctx:
157
+ assert ctx.eval("1 + 1") == 2
158
+ assert ctx.eval("navigator.webdriver") is False
159
+ ```
160
+
161
+ 规则:
162
+
163
+ - 版本号以实际 import 后读取的值为准。
164
+ - 冒烟失败时停止,不猜测 API。
165
+ - 先复现当前版本的实际行为,再决定是否需要 workaround。
166
+ - 已知问题在新版修复后,不得继续注入旧补丁。
167
+ - 本文使用的接口曾在 iv8 Community Edition 0.1.3 契约上核验,但不能据此假定所有版本完全相同。
168
+ - import、平台 wheel 或 API 契约不成立时,标记当前候选 `BLOCKED_CURRENT`,或交接专门的 iv8 运行环境 Skill。
169
+
170
+ 本文只依赖以下已核验接口:
171
+
172
+ ```text
173
+ iv8.JSContext(...)
174
+ ctx.eval(...)
175
+ ctx.add_resource(...)
176
+ ctx.with_devtools(...)
177
+ ctx.expose(...)
178
+ iv8.JSContext.get_defaults()
179
+ window.__iv8__.eventLoop.*
180
+ window.__iv8__.page.load(...)
181
+ window.__iv8__.netLog.entries
182
+ window.__iv8__.wrapNative(...)
183
+ ```
184
+
185
+ 不要臆造其他方法。
186
+
187
+ ## 5. L0~L4 观测阶梯
188
+
189
+ 优先选择能回答当前问题的最低级别。
190
+
191
+ | 等级 | 方法 | 原始文件字节 | 运行时对象 | 默认 `runtime patched` | 主要用途 |
192
+ | --- | --- | --- | --- | --- | --- |
193
+ | L0 | iv8 引擎层 debug 监控、netLog | 不变 | 不主动包装目标对象 | `false` | API 读写、调用、反射、缺失探测、请求结果 |
194
+ | L1 | DevTools、行断点、API watch、scope | 不变 | 不主动包装目标对象 | `false` | 局部变量、调用栈、实际分支 |
195
+ | L2 | 独立 harness 与 trigger | 不变 | 只按公开接口触发 | `false` | 初始化顺序、公开入口、参数和最终产物 |
196
+ | L3 | 独立 probe、wrapper、getter/setter、`expose` | 不变 | 已改变 | `true` | 公开边界参数、返回、异常和写入 |
197
+ | L4 | 独立运行插桩副本 | 原始文件不变;运行副本已改 | 已改变 | `true` | 前四级无法取得的内部值 |
198
+
199
+ 注意:
200
+
201
+ - “原始文件未修改”不等于“运行时无影响”。
202
+ - `mode="debug"`、断点和日志仍有时间与暂停开销。
203
+ - `wrapNative` 只能降低 wrapper 的可观察差异,不能证明等价。
204
+ - iv8 文档将 `window.__iv8__` 设计为目标侧不可检测工具对象;证据报告仍不应把任何调试平面宣称为绝对无影响。
205
+
206
+ ## 6. L0:引擎层旁路监控
207
+
208
+ `mode="debug"` 可记录:
209
+
210
+ - 浏览器 API 属性读取 `getter`。
211
+ - 属性写入 `setter`。
212
+ - 方法或构造调用 `call`。
213
+ - `typeof`、`in` 等存在性查询 `query`。
214
+ - `Object.keys`、descriptor、`Reflect.*`、`Function.prototype.toString`、JSON 等反射路径。
215
+ - 疑似缺失 API。
216
+
217
+ 高频 Math、JSON、Array、TypedArray 等默认可能被静音。只有当前问题确实需要时才调整 `ignore_apis`,不要无边界放大日志。
218
+
219
+ 最小骨架:
220
+
221
+ ```python
222
+ import hashlib
223
+ from pathlib import Path
224
+
225
+ import iv8
226
+
227
+ target_path = Path("target.original.js")
228
+ target_bytes = target_path.read_bytes()
229
+ target_hash = hashlib.sha256(target_bytes).hexdigest()
230
+ target_source = target_bytes.decode("utf-8")
231
+ trigger_source = Path("trigger.js").read_text(encoding="utf-8")
232
+
233
+ with iv8.JSContext(mode="debug", environment={}) as ctx:
234
+ ctx.eval(target_source, name="target.original.js")
235
+ ctx.eval(trigger_source, name="trigger.js")
236
+ net_entries = ctx.eval(
237
+ "window.__iv8__.netLog.entries",
238
+ to_py=True,
239
+ )
240
+
241
+ print("target sha256:", target_hash)
242
+ ```
243
+
244
+ `environment={}` 只是示例。只有目标实际依赖且本次实验已经声明的画像字段才应配置。
245
+
246
+ 对“疑似缺失 API”不要直接补:
247
+
248
+ 1. 先判断它是否只是反自动化陷阱。
249
+ 2. 在真实 Chromium 中本来就应为 `undefined` 的对象必须保持缺失。
250
+ 3. 只有确认真实浏览器存在、并且当前事实缺口确实依赖它时,才进入单独的环境修补任务。
251
+
252
+ ## 7. 捕获 fd1 与 fd2
253
+
254
+ iv8 的 C++ debug 日志直接写进程的 stdout/stderr 文件描述符。Python 的 `contextlib.redirect_stdout()` 不能保证捕获这些日志。
255
+
256
+ fd1/fd2 是进程级资源。优先让每次 iv8 harness 在独立子进程中运行,并为每次运行创建唯一 `run_id` 目录;这样不会吞并主 Agent 或其他并发任务的输出。
257
+
258
+ 只有确认当前进程单线程、没有其他 stdout/stderr 写入者时,才使用下面的进程内重定向。它使用进程内锁拒绝嵌套捕获,用 `O_EXCL` 拒绝覆盖既有证据,并始终在 `finally` 中恢复:
259
+
260
+ ```python
261
+ import os
262
+ import sys
263
+ import threading
264
+ from contextlib import contextmanager
265
+ from pathlib import Path
266
+
267
+
268
+ _NATIVE_LOG_LOCK = threading.Lock()
269
+
270
+
271
+ @contextmanager
272
+ def capture_native_log(log_path):
273
+ log_path = Path(log_path)
274
+ log_path.parent.mkdir(parents=True, exist_ok=True)
275
+
276
+ if not _NATIVE_LOG_LOCK.acquire(blocking=False):
277
+ raise RuntimeError("native fd capture is already active in this process")
278
+
279
+ saved_out = -1
280
+ saved_err = -1
281
+ log_fd = -1
282
+ try:
283
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
284
+ flags |= getattr(os, "O_CLOEXEC", 0)
285
+ log_fd = os.open(str(log_path), flags, 0o644)
286
+
287
+ sys.stdout.flush()
288
+ sys.stderr.flush()
289
+ saved_out = os.dup(1)
290
+ saved_err = os.dup(2)
291
+ os.dup2(log_fd, 1)
292
+ os.dup2(log_fd, 2)
293
+ os.close(log_fd)
294
+ log_fd = -1
295
+ yield
296
+ finally:
297
+ sys.stdout.flush()
298
+ sys.stderr.flush()
299
+ if saved_out >= 0:
300
+ os.dup2(saved_out, 1)
301
+ os.close(saved_out)
302
+ if saved_err >= 0:
303
+ os.dup2(saved_err, 2)
304
+ os.close(saved_err)
305
+ if log_fd >= 0:
306
+ os.close(log_fd)
307
+ _NATIVE_LOG_LOCK.release()
308
+ ```
309
+
310
+ 使用:
311
+
312
+ ```python
313
+ from datetime import datetime, timezone
314
+ from pathlib import Path
315
+ import secrets
316
+
317
+
318
+ run_id = (
319
+ datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ")
320
+ + "-"
321
+ + secrets.token_hex(4)
322
+ )
323
+ run_dir = Path("evidence") / run_id
324
+ run_dir.mkdir(parents=True, exist_ok=False)
325
+
326
+ with capture_native_log(run_dir / "runtime.raw.log"):
327
+ with iv8.JSContext(mode="debug", environment=environment) as ctx:
328
+ ctx.eval(original_source, name="target.original.js")
329
+ ctx.eval(trigger_source, name="trigger.js")
330
+ ```
331
+
332
+ 若目标日志已经存在,让本次运行失败,不得改回 `O_TRUNC`。保留原始日志,再生成分类摘要;不要只保留正则解析后的统计。
333
+
334
+ 进程内锁只能协调调用同一 helper 的代码,不能阻止其他线程直接写 fd1/fd2。因此有并发、长生命周期服务或不受控库线程时,独立子进程是硬边界,不使用进程内重定向。
335
+
336
+ 常用分类可以包括:
337
+
338
+ ```text
339
+ getter
340
+ setter
341
+ call
342
+ query
343
+ reflection
344
+ suspected missing API
345
+ ```
346
+
347
+ 日志格式属于版本相关实现细节。解析器匹配失败时保留原始日志并停止分类推断,不要把“没有解析到”写成“目标没有访问”。
348
+
349
+ ## 8. netLog 的准确边界
350
+
351
+ 读取:
352
+
353
+ ```python
354
+ entries = ctx.eval(
355
+ "window.__iv8__.netLog.entries",
356
+ to_py=True,
357
+ )
358
+ ```
359
+
360
+ `netLog` 可以回答:
361
+
362
+ - 目标是否发起 XHR、fetch 或导航。
363
+ - 请求方法和最终 URL。
364
+ - 拦截式 SDK 是否在 URL 上追加签名参数。
365
+ - 某个 trigger 是否命中了网络 Hook。
366
+
367
+ 它不能证明:
368
+
369
+ - 请求已经通过真实 HTTP/HTTPS 发送。
370
+ - 服务端已经接受该请求。
371
+ - TLS、代理、Cookie 池或真实浏览器网络栈与线上一致。
372
+
373
+ iv8 Community Edition 的 XHR、fetch 和外部资源默认匹配离线 resource bundle,不直接发送真实网络请求。
374
+
375
+ 因此:
376
+
377
+ - 只观察签名后的 URL 或请求描述时,读取 `netLog` 即可。
378
+ - 需要响应时,必须单独声明 Python 真请求、`ctx.add_resource()` 和事件循环推进。
379
+ - 真请求需要额外用户授权,不属于默认 AST 动态取证。
380
+ - 完整网络桥接应交接专门运行环境流程,不在本文展开。
381
+
382
+ ## 9. L1:DevTools 与 API watch
383
+
384
+ 需要局部变量、闭包状态、调用栈或实际分支时,使用外部 DevTools:
385
+
386
+ ```python
387
+ with (
388
+ iv8.JSContext(mode="debug", environment=environment)
389
+ .with_devtools(
390
+ port=9229,
391
+ watch_apis=[
392
+ "navigator.userAgent",
393
+ "document.cookie",
394
+ ],
395
+ enable_console=False,
396
+ )
397
+ ) as ctx:
398
+ ctx.eval(original_source, name="target.original.js")
399
+ ctx.eval(trigger_source, name="trigger.js")
400
+ ```
401
+
402
+ 规则:
403
+
404
+ - 优先在命名的原始脚本上设置外部行断点。
405
+ - `watch_apis` 只放与当前证据问题相关的 API。
406
+ - 记录实际命中的断点、调用栈、scope、输入和路径。
407
+ - 修改变量会改变运行事实;一旦修改,标记 `runtime patched: true`。
408
+ - 原生 `debugger;` 在 iv8 中被禁用,不会触发断点。
409
+ - iv8 提供 `vdebugger;`,但为了保持原始文件不变,只能把它放在独立 trigger/probe 中。
410
+ - 关闭标准 console 上报后,可在独立 probe 中使用 `vconsole.log()`;它只进入 DevTools 通道。
411
+
412
+ 外部断点会暂停执行,因此 `runtime patched: false` 只表示未改运行时对象,不表示没有调试影响。
413
+
414
+ ## 10. L2:独立 harness 与 trigger
415
+
416
+ 很多目标不是“调用一个 sign 函数”,而是:
417
+
418
+ ```text
419
+ 加载目标
420
+ → 调 public init
421
+ → 触发 XHR/fetch/事件
422
+ → 从宿主边界取得结果
423
+ ```
424
+
425
+ 把触发行为写进独立 `trigger.js`,不要拼进原始文件。
426
+
427
+ 加载顺序必须记录:
428
+
429
+ ```text
430
+ 1. 创建全新 JSContext。
431
+ 2. 配置本次声明的 environment/config。
432
+ 3. 加载固定 hash 的原始目标。
433
+ 4. 加载独立 trigger。
434
+ 5. 按需要推进事件循环。
435
+ 6. 从公开结果、netLog 或声明的观察点读取证据。
436
+ ```
437
+
438
+ `trigger.js` 只包含本次问题需要的调用。例如:
439
+
440
+ ```javascript
441
+ // 使用真实样本中已经确认的公开入口替换占位符。
442
+ window.publicApi.init(window.__CASE_INIT__);
443
+ window.publicApi.run(window.__CASE_INPUT__);
444
+ ```
445
+
446
+ 不要把案例专用入口固化成通用模板。入口名、初始化参数和 trigger 都是案例证据的一部分。
447
+
448
+ 以下情况仍可标记 `runtime patched: false`:
449
+
450
+ - trigger 只调用目标已经公开的 API。
451
+ - 不替换目标函数、宿主属性或原型。
452
+ - 输入和环境配置均已单独记录。
453
+
454
+ ## 11. L3:独立 probe
455
+
456
+ L0~L2 无法取得公开边界的参数、返回值、异常或写入时,使用独立 probe。
457
+
458
+ 典型方法:
459
+
460
+ - 包装公开函数。
461
+ - 包装 prototype 方法。
462
+ - 使用 `Object.defineProperty` 观察属性写入。
463
+ - 使用 `ctx.expose()` 把小量证据送到 Python。
464
+
465
+ `ctx.expose()` 的对象位于 `__iv8__.data`,不污染普通 `window` 全局;JS 回调 Python 时会重新获取 GIL并阻塞当前 V8 执行,所以回调必须轻量。
466
+
467
+ Python 侧:
468
+
469
+ ```python
470
+ events = []
471
+
472
+
473
+ def emit_evidence(event):
474
+ events.append(event)
475
+
476
+
477
+ with iv8.JSContext(environment=environment) as ctx:
478
+ ctx.expose(emit_evidence, "emitEvidence")
479
+ ctx.eval(original_source, name="target.original.js")
480
+ ctx.eval(probe_source, name="probe.js")
481
+ ctx.eval(trigger_source, name="trigger.js")
482
+ ```
483
+
484
+ probe 包装公开函数的骨架:
485
+
486
+ ```javascript
487
+ (function () {
488
+ var holder = window.publicApi;
489
+ var key = "run";
490
+ var desc = Object.getOwnPropertyDescriptor(holder, key);
491
+ if (!desc || typeof desc.value !== "function") {
492
+ throw new Error("probe precondition failed: publicApi.run");
493
+ }
494
+
495
+ var original = desc.value;
496
+ var wrapped = function () {
497
+ __iv8__.data.emitEvidence({
498
+ phase: "call",
499
+ argc: arguments.length,
500
+ thisType: typeof this
501
+ });
502
+
503
+ try {
504
+ var result = Reflect.apply(original, this, arguments);
505
+ __iv8__.data.emitEvidence({
506
+ phase: "return",
507
+ resultType: typeof result
508
+ });
509
+ return result;
510
+ } catch (error) {
511
+ __iv8__.data.emitEvidence({
512
+ phase: "throw",
513
+ name: error && error.name,
514
+ message: String(error && error.message)
515
+ });
516
+ throw error;
517
+ }
518
+ };
519
+
520
+ Object.defineProperty(holder, key, {
521
+ value: __iv8__.wrapNative(wrapped, key),
522
+ writable: desc.writable,
523
+ enumerable: desc.enumerable,
524
+ configurable: desc.configurable
525
+ });
526
+ })();
527
+ ```
528
+
529
+ 规则:
530
+
531
+ - 原样传递 `this` 和全部参数。
532
+ - 返回原始返回值,不替换 Promise。
533
+ - 异常只能记录后原样重抛。
534
+ - 尽可能保留 descriptor 和加载顺序。
535
+ - 不为日志进行可能触发 getter、Proxy 或序列化副作用的深拷贝。
536
+ - `wrapNative` 只降低 `toString()` 差异,不能证明 wrapper 不可检测。
537
+ - 使用 probe 的运行一律标记 `runtime patched: true`。
538
+
539
+ ## 12. L4:派生副本插桩
540
+
541
+ 只有在以下条件全部满足时才使用:
542
+
543
+ - L0~L3 不能取得完成当前 AST 步骤所需的内部事实。
544
+ - 插桩位置可以静态、稳定地定位。
545
+ - 用户明确批准运行派生副本。
546
+ - 能建立原始位置到插桩位置的映射。
547
+
548
+ 文件角色:
549
+
550
+ ```text
551
+ target.original.js
552
+ 不变,保留原始 hash。
553
+
554
+ target.instrumented.js
555
+ 单独生成,记录 transform、hash 和插桩点。
556
+ ```
557
+
558
+ 要求:
559
+
560
+ - `runtime patched: true`。
561
+ - 证据等级低于原始目标旁路观测。
562
+ - 固定输入下与原始目标做 A/B。
563
+ - 报告明确哪些结论来自插桩副本。
564
+ - 不把插桩副本的成功运行当成原始目标行为等价证明。
565
+ - 不在 AST Skill 中插桩 VM 分派器、PC、值栈、帧栈或虚拟函数。
566
+
567
+ ## 13. runtime patched 与 A/B
568
+
569
+ 每次动态实验必须显式写:
570
+
571
+ ```text
572
+ runtime patched: true | false
573
+ ```
574
+
575
+ 以下情况为 `true`:
576
+
577
+ - 替换函数或 prototype。
578
+ - 安装 getter/setter。
579
+ - 修改局部变量或运行时对象。
580
+ - 注入缺失 API 或覆盖 stub。
581
+ - 执行插桩副本。
582
+
583
+ 以下情况通常为 `false`:
584
+
585
+ - 仅启用 L0 debug 日志。
586
+ - 仅设置外部断点或 API watch,且未改值。
587
+ - trigger 只调用公开入口。
588
+
589
+ 即使为 `false`,仍记录 debug、暂停和时间开销。
590
+
591
+ L3/L4 必须做 A/B:
592
+
593
+ ```text
594
+ A:全新 Context + 原始目标 + trigger
595
+ B:全新 Context + 原始目标 + probe + 同一 trigger
596
+ ```
597
+
598
+ 对照至少包括:
599
+
600
+ - 是否完成相同阶段。
601
+ - 返回类型、异常类型和关键结构。
602
+ - 公开副作用。
603
+ - `netLog` 请求方法、URL 结构和顺序。
604
+ - 关键调用顺序。
605
+ - 是否出现新的环境探测或错误。
606
+
607
+ 每次都创建全新 Context,避免上一次初始化、计数器或 Hook 污染。
608
+
609
+ 如果输出含时间、随机数或计数器,不要求原始字符串完全相等;先冻结可冻结的输入,再比较结构性不变量。不能控制的熵源必须写入未覆盖范围。
610
+
611
+ A/B 发生实质差异时:
612
+
613
+ - 不使用该 probe 的结果支持 transform。
614
+ - 缩小 probe 或退回更低级别。
615
+ - 当前候选无法继续时标记 `BLOCKED_CURRENT`。
616
+
617
+ ## 14. 事件循环与异步结果
618
+
619
+ iv8 不会替分析者自动推进所有逻辑时间。读取异步结果前必须选择正确的推进方式。
620
+
621
+ | 方法 | 准确语义 | 典型用途 |
622
+ | --- | --- | --- |
623
+ | `drain()` | 排空所有已到期任务,不推进时间 | 同步触发后的收尾、注入响应后的回调 |
624
+ | `drainMicrotasks()` | 只排空微任务 | Promise、`queueMicrotask`、MutationObserver |
625
+ | `drainTimers()` | 只处理已到期 timer | 已到期定时器 |
626
+ | `tick(ms?)` | 推进并执行一轮 | 逐轮观察 |
627
+ | `advance(total, step?)` | 分帧推进虚拟时间 | rAF、分帧逻辑、多个 timer |
628
+ | `sleep(ms?, max?)` | 推进指定逻辑时间并按顺序排队 | 明确延时 |
629
+
630
+ 示例:
631
+
632
+ ```python
633
+ ctx.eval(trigger_source, name="trigger.js")
634
+ ctx.eval("window.__iv8__.eventLoop.drainMicrotasks()")
635
+ ctx.eval("window.__iv8__.eventLoop.advance(100)")
636
+ result = ctx.eval("window.__CASE_RESULT__", to_py=True)
637
+ ```
638
+
639
+ 不要机械执行所有推进方法。根据目标实际注册的任务选择最小组合,并记录:
640
+
641
+ ```text
642
+ time_mode
643
+ 推进方法
644
+ 推进时长
645
+ 最大任务数
646
+ 读取结果的时点
647
+ ```
648
+
649
+ 时间差、PoW 或真实耗时检查可能需要 `time_mode="system"`;普通可控实验优先逻辑时间。改变 time mode 会改变运行条件,必须进入证据记录。
650
+
651
+ 拦截式 SDK 只在 XHR URL 上加签时,`netLog` 可能已经包含目标事实,不必为了等待不存在的响应而推进时间。
652
+
653
+ ## 15. 动态证据映射回 AST
654
+
655
+ 动态事实必须回到静态主线,不能停留在日志中。
656
+
657
+ 推荐映射键:
658
+
659
+ - `ctx.eval(..., name="target.original.js")` 提供的脚本名。
660
+ - 原始行列或 byte offset。
661
+ - 调用栈中的函数位置。
662
+ - 唯一源码片段。
663
+ - AST 节点类型、父节点路径和邻近字面量。
664
+ - 格式化视图中的对应行列。
665
+ - transform 前后的稳定节点 ID。
666
+
667
+ 映射表:
668
+
669
+ | evidence_id | 运行事实 | 输入与路径 | 原始位置 | 格式化位置 | AST 节点 | 置信度 |
670
+ | --- | --- | --- | --- | --- | --- | --- |
671
+ | `E-RUN-001` | | | | | | |
672
+
673
+ 注意:
674
+
675
+ - Prettier 格式化会改变行列,不能把格式化行号直接当成原始断点位置。
676
+ - 找不到唯一映射时,只能把证据记为候选关联,不能据此批量替换。
677
+ - 动态值写回分析视图前,必须说明它是固定输入事实、路径事实还是可泛化常量。
678
+ - 路径事实不能被提升为未覆盖分支的静态常量。
679
+
680
+ 完成映射后回到 AST 流程:
681
+
682
+ ```text
683
+ 动态取得事实
684
+ → 绑定到原始 AST 节点
685
+ → 检查适用范围
686
+ → 生成或修正单步 transform
687
+ → 验证派生分析视图
688
+ → 重新阅读和画像
689
+ → 向用户提交下一候选
690
+ ```
691
+
692
+ ## 16. 证据记录格式
693
+
694
+ 每次实验至少保存:
695
+
696
+ ```markdown
697
+ # Runtime Evidence <ID>
698
+
699
+ ## Question
700
+
701
+ ## Static Boundary
702
+
703
+ ## Approval
704
+
705
+ ## Runtime
706
+
707
+ - iv8 version:
708
+ - Python/platform:
709
+ - target path:
710
+ - target sha256:
711
+ - environment/config:
712
+ - time mode:
713
+ - trigger path and sha256:
714
+ - probe path and sha256:
715
+ - instrumented path and sha256:
716
+ - observation level: L0 | L1 | L2 | L3 | L4
717
+ - runtime patched: true | false
718
+
719
+ ## Execution Order
720
+
721
+ ## Observation
722
+
723
+ ## Raw Evidence
724
+
725
+ - native log:
726
+ - netLog:
727
+ - DevTools notes:
728
+ - structured events:
729
+
730
+ ## A/B
731
+
732
+ ## Source Mapping
733
+
734
+ ## Coverage
735
+
736
+ ## Explicit Non-Conclusions
737
+
738
+ ## Decision
739
+
740
+ Return to AST | retry lower intrusion | blocked | handoff
741
+ ```
742
+
743
+ ## 17. 分流与返回主线
744
+
745
+ | 结果 | 下一步 |
746
+ | --- | --- |
747
+ | 已取得有限、可映射事实 | 回到 AST 单步 transform |
748
+ | 只需再缩小一个观察点 | 重新设计更小实验 |
749
+ | probe 改变行为 | 退回 L0~L2,或缩小 probe |
750
+ | 需要系统补环境、离线资源和完整事件循环 | 推荐专门的 iv8 运行环境 Skill |
751
+ | 需要真实网络请求、账号、Cookie 或代理 | 单独申请授权并交接运行环境流程 |
752
+ | 需要真实布局、字体、音频、渲染或页面会话 | 转真实浏览器、CDP 或 js-reverse 能力 |
753
+ | 需要 VM Opcode、PC、值栈或 Trace | 交接 VM Skill |
754
+ | 当前事实仍无法取得 | `BLOCKED_CURRENT`,继续其他静态候选 |
755
+
756
+ 动态取证结束后,iv8 不继续控制后续步骤;下一步重新由最新分析视图、收益和用户选择决定。
757
+
758
+ ## 18. 禁止事项
759
+
760
+ - 不执行尚未定义证据问题的完整未知目标。
761
+ - 不在用户只批准静态步骤时静默升级动态。
762
+ - 不原地修改或格式化原始运行文件。
763
+ - 不把分析视图当成原始运行目标。
764
+ - 不把 `node:vm` 称为不可信代码安全沙箱。
765
+ - 不把 iv8 Community Edition 的 `netLog` 描述为真实网络请求。
766
+ - 不把 `drain()` 描述为推进时间。
767
+ - 不用 Python 普通 stdout 重定向冒充已捕获 C++ 日志。
768
+ - 不把 `wrapNative` 描述为绝对不可检测。
769
+ - 不把 runtime wrapper 描述为“零侵入”。
770
+ - 不把一次运行结果推广到全部输入和路径。
771
+ - 不因“疑似缺失 API”日志而批量补全浏览器对象。
772
+ - 不在本 reference 中复制完整补环境流程。
773
+ - 不在 AST Skill 中借动态调试进入 VM 内部。