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.
- package/cordis.patch.yml +14 -0
- package/index.js +61 -0
- package/package.json +20 -0
- package/skills/iv8-env-runtime/SKILL.md +159 -0
- package/skills/iv8-env-runtime/assets/entry-report-template.txt +23 -0
- package/skills/iv8-env-runtime/assets/env_starter.py +100 -0
- package/skills/iv8-env-runtime/references/case-function-iwencai.md +96 -0
- package/skills/iv8-env-runtime/references/case-intercept-douyin.md +133 -0
- package/skills/iv8-env-runtime/references/config-defaults-cheatsheet.md +75 -0
- package/skills/iv8-env-runtime/references/cross-platform-install.md +108 -0
- package/skills/iv8-env-runtime/references/iv8-api-contract.md +97 -0
- package/skills/iv8-env-runtime/references/network-and-eventloop.md +113 -0
- package/skills/iv8-env-runtime/references/part1-entry-location.md +138 -0
- package/skills/iv8-env-runtime/references/part1-mcp-tool-map.md +77 -0
- package/skills/iv8-env-runtime/references/part2-discovery.md +80 -0
- package/skills/iv8-env-runtime/references/patching-principles.md +91 -0
- package/skills/iv8-env-runtime/references/trap-blacklist.md +60 -0
- package/skills/iv8-env-runtime/references/version-gotchas.md +128 -0
- package/skills/iv8-env-runtime/scripts/consistency_lint.py +211 -0
- package/skills/iv8-env-runtime/scripts/defaults_audit.py +143 -0
- package/skills/iv8-env-runtime/scripts/monitor_env.py +185 -0
- package/skills/iv8-env-runtime/scripts/network_bridge.py +147 -0
- package/skills/iv8-env-runtime/scripts/parity_check.py +175 -0
- package/skills/iv8-env-runtime/scripts/preflight.py +144 -0
- package/skills/iv8-env-runtime/scripts/reproduce_stub_error.py +165 -0
- package/skills/iv8-env-runtime/scripts/run_target.py +150 -0
- package/skills/iv8-jsvmp-recovery/SKILL.md +129 -0
- package/skills/iv8-jsvmp-recovery/agents/openai.yaml +4 -0
- package/skills/iv8-jsvmp-recovery/references/case-study-douyin-a-bogus.md +320 -0
- package/skills/iv8-jsvmp-recovery/references/controlled-oracle-and-equivalence.md +247 -0
- package/skills/iv8-jsvmp-recovery/references/static-trace-evidence.md +289 -0
- package/skills/iv8-jsvmp-recovery/references/vmp-domain-model.md +212 -0
- package/skills/iv8-jsvmp-recovery/scripts/verify-equivalence.py +281 -0
- package/skills/js-ast-deobfuscation/SKILL.md +140 -0
- package/skills/js-ast-deobfuscation/agents/openai.yaml +4 -0
- package/skills/js-ast-deobfuscation/references/anti-patterns.md +325 -0
- package/skills/js-ast-deobfuscation/references/first-pass.md +255 -0
- package/skills/js-ast-deobfuscation/references/interaction-and-stopping.md +758 -0
- package/skills/js-ast-deobfuscation/references/iv8-runtime-observation.md +773 -0
- package/skills/js-ast-deobfuscation/references/jsvmp-identification.md +328 -0
- package/skills/js-ast-deobfuscation/references/methods-and-evidence.md +460 -0
- package/skills/js-ast-deobfuscation/references/patterns-and-priority.md +516 -0
- package/skills/js-ast-deobfuscation/references/validation-contract.md +604 -0
- package/skills/js-ast-deobfuscation/scripts/prepare-source.mjs +921 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# JSVMP 识别与反误判
|
|
2
|
+
|
|
3
|
+
本文只回答两个问题:
|
|
4
|
+
|
|
5
|
+
1. 什么源码证据足以确认存在 JSVMP;
|
|
6
|
+
2. AST 解混淆应该清理到哪里并交接。
|
|
7
|
+
|
|
8
|
+
它不解释 Opcode,不跟踪 PC、值栈或帧栈,也不反编译字节码。
|
|
9
|
+
|
|
10
|
+
## 目录
|
|
11
|
+
|
|
12
|
+
1. [判定结论](#1-判定结论)
|
|
13
|
+
2. [四道硬门](#2-四道硬门)
|
|
14
|
+
3. [结论等级](#3-结论等级)
|
|
15
|
+
4. [本地真实样本](#4-本地真实样本)
|
|
16
|
+
5. [常见误判](#5-常见误判)
|
|
17
|
+
6. [静态与动态边界](#6-静态与动态边界)
|
|
18
|
+
7. [记录与交接](#7-记录与交接)
|
|
19
|
+
|
|
20
|
+
## 1. 判定结论
|
|
21
|
+
|
|
22
|
+
JSVMP 不是“大数字数组”“`while + switch`”或“变量名像 `pc`”。
|
|
23
|
+
|
|
24
|
+
只有同一条数据流形成以下闭环,才能确认代码中存在自定义 JavaScript 指令解释器:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
自定义程序数据
|
|
28
|
+
→ 按指令位置取出 opcode 和 operands
|
|
29
|
+
→ dispatcher 选择可复用的指令语义
|
|
30
|
+
→ handler 修改持续存在的虚拟执行状态
|
|
31
|
+
→ 跳转、调用、返回或异常处理决定下一条指令
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
再证明目标公开 API、业务返回值或业务副作用依赖这条闭环,才确认“目标业务被 JSVMP 虚拟化”。
|
|
35
|
+
|
|
36
|
+
## 2. 四道硬门
|
|
37
|
+
|
|
38
|
+
以下 A~D 必须同时成立,而且必须属于同一个执行闭环。
|
|
39
|
+
|
|
40
|
+
### A. 程序被表示为数据
|
|
41
|
+
|
|
42
|
+
找到数组、字符串、Buffer 或结构化记录,并证明它不是普通配置或资源,而是提供:
|
|
43
|
+
|
|
44
|
+
- opcode;
|
|
45
|
+
- operands;
|
|
46
|
+
- 跳转目标;
|
|
47
|
+
- 常量或名称索引。
|
|
48
|
+
|
|
49
|
+
只看到高熵数组或二进制数据,不算通过。
|
|
50
|
+
|
|
51
|
+
### B. 存在重复取指和分派
|
|
52
|
+
|
|
53
|
+
证明 dispatcher 的选择值直接或间接来自程序数据,并在一次执行中反复取得“下一条指令”。
|
|
54
|
+
|
|
55
|
+
分派形式可以是比较树、`switch`、handler table、函数线程或递归 continuation;不要求固定语法。
|
|
56
|
+
|
|
57
|
+
### C. 存在持续的虚拟执行状态
|
|
58
|
+
|
|
59
|
+
除指令位置外,至少还要看到一种跨指令保存的状态:
|
|
60
|
+
|
|
61
|
+
- operand/value stack;
|
|
62
|
+
- register 或 accumulator;
|
|
63
|
+
- virtual locals、scope 或 environment;
|
|
64
|
+
- virtual call frames;
|
|
65
|
+
- completion、return 或 exception state。
|
|
66
|
+
|
|
67
|
+
普通模块缓存、parser 输入游标或业务状态字段不算 VM 执行状态。
|
|
68
|
+
|
|
69
|
+
### D. handler 实现抽象的 JS 语义和虚拟控制流
|
|
70
|
+
|
|
71
|
+
至少证明:
|
|
72
|
+
|
|
73
|
+
- 一类数据或计算语义,例如 load、property access、call、arithmetic;
|
|
74
|
+
- 一类虚拟控制语义,例如 jump、branch、call、return、throw。
|
|
75
|
+
|
|
76
|
+
handler 应是可被不同指令位置复用的抽象操作。若每个分支直接写着某个原函数的业务基本块,更像控制流平坦化。
|
|
77
|
+
|
|
78
|
+
## 3. 结论等级
|
|
79
|
+
|
|
80
|
+
使用结构门,不使用“出现几个特征”的打分法。
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
CONFIRMED_VM_STRUCTURE
|
|
84
|
+
A ∧ B ∧ C ∧ D
|
|
85
|
+
|
|
86
|
+
CONFIRMED_BUSINESS_JSVMP
|
|
87
|
+
A ∧ B ∧ C ∧ D
|
|
88
|
+
+ 公开 API、目标业务路径或可观察结果确实经过该 VM
|
|
89
|
+
|
|
90
|
+
PROBABLE_JSVMP
|
|
91
|
+
已形成大部分闭环,但关键来源或消费者仍不可见;
|
|
92
|
+
同时没有打包器、parser、普通状态机等更直接解释
|
|
93
|
+
|
|
94
|
+
INSUFFICIENT_VM_EVIDENCE
|
|
95
|
+
只有数组、dispatcher、状态变量或其他孤立表象
|
|
96
|
+
|
|
97
|
+
NOT_JSVMP
|
|
98
|
+
已证明结构属于模块加载、数据解析、packer、Wasm 或其他机制
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
不知道时保留 `Unknown`。不要为了给出确定答案而把 `PROBABLE` 升级成 `CONFIRMED`。
|
|
102
|
+
|
|
103
|
+
## 4. 本地真实样本
|
|
104
|
+
|
|
105
|
+
以下短片段来自一个真实本地样本的派生分析视图。只省略无关分支,并给局部变量补充说明性中性名;判定依赖数据流,不依赖名称。
|
|
106
|
+
|
|
107
|
+
### 4.1 程序记录进入解释器
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
vmRecords = [
|
|
111
|
+
[[63, 1, 54, 0, 8, /* ... */, 76], 0, true, []],
|
|
112
|
+
[[73, 0, 69, 1, /* ... */, 76], 1, true, []]
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
function executeVmEntry(entryId, thisArg, args, hostSlots) {
|
|
116
|
+
return executeVmRecord(vmRecords[entryId], thisArg, args, hostSlots);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`vmRecords` 单独出现时只是候选。`entryId → vmRecords[entryId] → executeVmRecord` 证明记录被当作程序执行,A 通过。
|
|
121
|
+
|
|
122
|
+
### 4.2 初始化独立执行状态
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
var instructionData,
|
|
126
|
+
strictMode,
|
|
127
|
+
exceptionTable,
|
|
128
|
+
scopeChain,
|
|
129
|
+
instructionPointer,
|
|
130
|
+
completionType,
|
|
131
|
+
completionValue,
|
|
132
|
+
stackPointer = -1,
|
|
133
|
+
valueStack = [],
|
|
134
|
+
callFrames = [];
|
|
135
|
+
|
|
136
|
+
instructionData = vmRecord[0];
|
|
137
|
+
exceptionTable = vmRecord[3];
|
|
138
|
+
scopeChain = [hostSlots, argumentsObject];
|
|
139
|
+
instructionPointer = 0;
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
这里同时存在指令位置、值栈、调用帧、作用域和完成状态,C 通过。
|
|
143
|
+
|
|
144
|
+
### 4.3 从程序数据取指并持续分派
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
function dispatchInstructions() {
|
|
148
|
+
for (;;) {
|
|
149
|
+
var opcode = instructionData[instructionPointer++];
|
|
150
|
+
if (opcode < 38) {
|
|
151
|
+
// ...
|
|
152
|
+
} else {
|
|
153
|
+
// ...
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
嵌套比较树与 `switch` 等价:选择值来自 `instructionData[instructionPointer++]`,B 通过。
|
|
160
|
+
|
|
161
|
+
### 4.4 handler 操作虚拟状态
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
var argc = instructionData[instructionPointer++];
|
|
165
|
+
stackPointer -= argc;
|
|
166
|
+
var args = valueStack.slice(stackPointer + 1, stackPointer + argc + 1);
|
|
167
|
+
var callee = valueStack[stackPointer--];
|
|
168
|
+
var receiver = valueStack[stackPointer--];
|
|
169
|
+
|
|
170
|
+
if (vmFunctionMetadataByFunction.has(callee)) {
|
|
171
|
+
callFrames.push([
|
|
172
|
+
instructionData,
|
|
173
|
+
scopeChain,
|
|
174
|
+
instructionPointer,
|
|
175
|
+
completionType,
|
|
176
|
+
completionValue
|
|
177
|
+
]);
|
|
178
|
+
initializeVmFrame(/* virtual callee */);
|
|
179
|
+
} else {
|
|
180
|
+
valueStack[++stackPointer] = callee.apply(receiver, args);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
同一个 dispatcher 还会修改 `instructionPointer` 做条件跳转,并用 `completionType`、`completionValue` 和 `callFrames.pop()` 处理返回与异常。它实现的是可复用的调用、跳转、返回和异常语义,不是单个业务基本块,D 通过。
|
|
185
|
+
|
|
186
|
+
### 4.5 公开 API 与 VM 相连
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
e.d(n, {
|
|
190
|
+
init: function () {
|
|
191
|
+
return publicInit;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
executeVmEntry(0, void 0, arguments, {
|
|
196
|
+
get 19() {
|
|
197
|
+
return publicInit;
|
|
198
|
+
},
|
|
199
|
+
set 19(value) {
|
|
200
|
+
publicInit = value;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
window.bdms = n;
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
这段代码先证明 `entry 0 → host slot 19 → publicInit → window.bdms.init` 的宿主桥。要证明 VM 确实向桥中写入函数,还要继续读取记录 0 和对应 handler。
|
|
208
|
+
|
|
209
|
+
记录 0 含有连续数据:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
63, 128, 54, 1, 19
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
解释器中,与这段数据相连的两个 handler 效果是:
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
valueStack[++stackPointer] = createVmFunction(
|
|
219
|
+
instructionData[instructionPointer++],
|
|
220
|
+
scopeChain
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
var depth = instructionData[instructionPointer++];
|
|
224
|
+
var slot = instructionData[instructionPointer++];
|
|
225
|
+
var target = scopeChain;
|
|
226
|
+
while (depth > 0) {
|
|
227
|
+
target = target[0];
|
|
228
|
+
--depth;
|
|
229
|
+
}
|
|
230
|
+
target[slot] = valueStack[stackPointer--];
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
由于虚拟帧初始化为 `scopeChain = [hostSlots, argumentsObject]`,这段数据会创建 `vmRecords[128]` 对应的 VM 函数,再写入 `hostSlots[19]`,触发 setter 把它赋给 `publicInit`。
|
|
234
|
+
|
|
235
|
+
因此这个样本不只是“含有一个解释器”,而是可以静态确认公开的 `window.bdms.init` 由 VM 函数支撑。这里只识别了证明宿主边界所需的 handler 效果,没有恢复完整 Opcode 表或记录 128 的业务算法。
|
|
236
|
+
|
|
237
|
+
完整证明链是:
|
|
238
|
+
|
|
239
|
+
```text
|
|
240
|
+
vmRecords[entryId]
|
|
241
|
+
→ executeVmRecord
|
|
242
|
+
→ instructionData / instructionPointer / valueStack / callFrames
|
|
243
|
+
→ instructionData[instructionPointer++]
|
|
244
|
+
→ dispatcher handlers
|
|
245
|
+
→ return / exception / host slots
|
|
246
|
+
→ public init
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
确认这条边界不需要解释任意一个 Opcode 的业务含义。
|
|
250
|
+
|
|
251
|
+
## 5. 常见误判
|
|
252
|
+
|
|
253
|
+
### 数字模块 ID
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
6696: function (module, exports, require) {
|
|
257
|
+
var isCallable = require(5437);
|
|
258
|
+
module.exports = function (value) {
|
|
259
|
+
if (isCallable(value)) return value;
|
|
260
|
+
throw TypeError("not a function");
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
这里 `6696` 是模块地址,函数体仍由 JavaScript 引擎直接执行。没有程序流、取指指针、虚拟状态和指令闭环,所以不是 JSVMP。
|
|
266
|
+
|
|
267
|
+
同一个文件可以同时包含打包器和 JSVMP。本地样本就是外层模块加载器包着内层 VM;不要把两类结论互斥化。
|
|
268
|
+
|
|
269
|
+
### 其他弱证据
|
|
270
|
+
|
|
271
|
+
| 表象 | 默认分类 | 决定性缺口 |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| 大数字数组、Base64、TypedArray | 数据候选 | 未证明数据被当作程序 |
|
|
274
|
+
| `while + switch(state)` | 状态机或控制流平坦化候选 | selector 未必来自 `program[pc]` |
|
|
275
|
+
| 大量数字 `case` | dispatcher 候选 | 未证明 operands、VM state 和重复取指 |
|
|
276
|
+
| Webpack 模块表 | 打包器 | 模块 ID 不是 opcode |
|
|
277
|
+
| parser/regex/template VM | 领域解释器 | 指令语义未承载目标 JS 业务 |
|
|
278
|
+
| 解码后交给 `eval`/`Function` | packer 或动态载荷 | 最终由原生 JS 引擎执行 |
|
|
279
|
+
| `WebAssembly.instantiate` | Wasm | JS 中没有自定义取指解释闭环 |
|
|
280
|
+
| 名为 `pc`、`stack`、`opcode` 的变量 | 命名线索 | 名称不能替代数据流证据 |
|
|
281
|
+
|
|
282
|
+
Parser 确实可能使用 VM。此时应写“确认存在 parser VM”,不能自动写成“业务使用 JSVMP”。继续比较 handler 是 `shift/reduce/token`,还是 `load/property/call/branch/return`。
|
|
283
|
+
|
|
284
|
+
## 6. 静态与动态边界
|
|
285
|
+
|
|
286
|
+
本地样本的 A~D 和公开入口关联都在源码中形成闭环,因此静态即可确认,不需要先补环境或运行程序。
|
|
287
|
+
|
|
288
|
+
只有下列关键事实在源码中不可见时,才考虑最小动态边界取证:
|
|
289
|
+
|
|
290
|
+
- 指令数据在运行时才解密或下载;
|
|
291
|
+
- entry ID 或公开入口只在特定生命周期确定;
|
|
292
|
+
- dispatcher 或 handler 在运行时生成;
|
|
293
|
+
- 只能通过一次公开调用确认目标路径是否进入候选 VM。
|
|
294
|
+
|
|
295
|
+
动态取证只观察公开调用、VM 入口、entry/record 身份和外部结果。不要在 AST Skill 中记录 Opcode trace、PC trace、值栈或帧栈;若确认结论必须依赖这些内部证据,保留 `PROBABLE_JSVMP` 并交接 VM 分析。
|
|
296
|
+
|
|
297
|
+
## 7. 记录与交接
|
|
298
|
+
|
|
299
|
+
在 `ANALYSIS.md` 中记录:
|
|
300
|
+
|
|
301
|
+
```text
|
|
302
|
+
verdict:
|
|
303
|
+
CONFIRMED_BUSINESS_JSVMP |
|
|
304
|
+
CONFIRMED_VM_STRUCTURE |
|
|
305
|
+
PROBABLE_JSVMP |
|
|
306
|
+
INSUFFICIENT_VM_EVIDENCE |
|
|
307
|
+
NOT_JSVMP
|
|
308
|
+
|
|
309
|
+
A_program_as_data: pass | fail | unknown
|
|
310
|
+
B_fetch_dispatch: pass | fail | unknown
|
|
311
|
+
C_virtual_state: pass | fail | unknown
|
|
312
|
+
D_instruction_semantics: pass | fail | unknown
|
|
313
|
+
business_or_public_path: pass | fail | unknown
|
|
314
|
+
connected_dataflow:
|
|
315
|
+
source_anchors:
|
|
316
|
+
alternative_explanations:
|
|
317
|
+
counterevidence:
|
|
318
|
+
unknowns:
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
确认后,AST 阶段可以:
|
|
322
|
+
|
|
323
|
+
- 恢复阻碍边界识别的外层字符串;
|
|
324
|
+
- 用 `vmRecords`、`executeVmEntry`、`entry_0`、`slot_19` 等中性名称建立坐标;
|
|
325
|
+
- 建立 entry、record、host binding、slot、public API 和原始位置映射;
|
|
326
|
+
- 清理解释器外围、打包器和宿主桥中确有收益的隐藏。
|
|
327
|
+
|
|
328
|
+
一旦下一问题变成“某个 Opcode 做什么”“某段字节码是什么算法”或“如何采集 PC/栈轨迹”,停止 AST 解混淆并交接。
|