reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# 控制流平坦化与 Opcode 模式
|
||||
|
||||
这份文档用于处理控制流平坦化、opcode `if` 链、虚拟机解释器主分发器,以及测试位噪声分支。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- `for (...; ![];) { switch (...) { ... } }`
|
||||
- `while (true) { switch (...) { ... } }`
|
||||
- `"3|1|2".split('|')` 一类数组或顺序表驱动的平坦化
|
||||
- 大量 `if (0x31 === opcode) ... else if ...`
|
||||
- 虚拟机解释器里围绕同一个 opcode 变量做长链分发
|
||||
|
||||
## 控制流展平原则
|
||||
|
||||
- 优先写定向 visitor,不要一开始追求通用大而全。
|
||||
- 先验证 `init`、`test`、`update`、`body` 结构是否稳定。
|
||||
- 只要结构不匹配,就直接跳过,不要强行展平。
|
||||
- 拼接 case 时同步清理仅用于跳转的赋值、无意义 `break` 和明显噪声语句。
|
||||
|
||||
## `if` 链改写为 `switch`
|
||||
|
||||
仅在以下条件同时满足时改写:
|
||||
|
||||
- 整条链都在判断同一个判别表达式。
|
||||
- 每个分支都拿这个表达式与字面量比较。
|
||||
- 分支数量足够长,改写后可读性明显提升。
|
||||
|
||||
## 测试位噪声与哨兵比较
|
||||
|
||||
某些样本会在测试条件里塞入噪声,例如:
|
||||
|
||||
```js
|
||||
cond && fn(...) !== {}
|
||||
cond && fn(...) === {}
|
||||
```
|
||||
|
||||
只在以下前提下清理:
|
||||
|
||||
- 它位于 `if`、`for.test` 或 `while.test` 位置。
|
||||
- 右侧确实只是混淆器注入的空对象比较噪声。
|
||||
- 删除后不会影响左侧真实条件的副作用顺序。
|
||||
|
||||
## 虚拟机解释器建议
|
||||
|
||||
- 第一轮只求看清主分发器、栈变量、opcode 读取器。
|
||||
- 清掉虚假分支后,再把 `if` 链改成 `switch`。
|
||||
- 第二轮再考虑简化 case 内的局部结构。
|
||||
|
||||
## 停止条件
|
||||
|
||||
- 主 opcode 分发器已可读。
|
||||
- 关键 case 已经可以逐个分析。
|
||||
- 继续静态展开开始明显增加误改写风险。
|
||||
|
||||
到这里就可以转入人工分析或结合运行时证据继续逆向。
|
||||
@@ -0,0 +1,19 @@
|
||||
# 模式分层
|
||||
|
||||
将这个 skill 保持为三层结构:
|
||||
|
||||
1. 通用变换层
|
||||
只处理稳定、低风险、能跨多个样本复用的改写。
|
||||
2. 检测与流水线选择层
|
||||
先命中混淆家族或站点,再决定步骤顺序。
|
||||
3. 家族或站点适配层
|
||||
当某种残留可以稳定复现,但又不适合泛化时,新增窄范围脚本和配套规则文档。
|
||||
|
||||
新增一个适配器时,要一起更新这些位置:
|
||||
|
||||
- `references/patterns/<pattern>.md`
|
||||
- `scripts/patterns/<pattern>.js`
|
||||
- `scripts/pipeline-config.js`
|
||||
- `scripts/detect-patterns.js` 对应的配置命中项
|
||||
|
||||
除非某条规则已经在多个无关样本中重复出现,而且安全边界依然清晰,否则不要把站点规则回灌进通用脚本。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 变种OB混淆
|
||||
|
||||
当样本是OB变种混淆时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 仍然能看到 `_0x` 风格的标识符。
|
||||
- 经过通用处理后,OB 家族的包装层依旧存在。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 将 OB 家族逻辑保留在专用适配器里,只给 OB 类样本复用。
|
||||
- 适配器运行后,直接把 `loop/switch` 残留与参考 `decode` 结果对比。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 仍然残留的直接 `loop/switch` 平坦化结构。
|
||||
- 任何高于参考基线的 OB 风格命名或包装层痕迹。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 顶象
|
||||
|
||||
当样本属于顶象或非常接近的混淆家族时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- IIFE 的参数会喂给后续平坦化阶段使用的数组或查找对象。
|
||||
- 清理 dispatcher 会有帮助,但更关键的是恢复由数组承载的执行顺序。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 先解析由参数驱动的数组,再判断 `loop/switch` 平坦化是否仍然存在。
|
||||
- 平坦化完成后保持流程保守,默认不要再追加第二轮高开销的字面量内联。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 相对 `decode.js` 仍然存在的结构噪音。
|
||||
- 在直接 `loop/switch` 块已经清零后,依然残留的 dispatcher 包装层差距。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 极验4
|
||||
|
||||
当样本属于极验4,或属于相近的 guarded-switch VM 家族时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- `loop/switch` 平坦化外层还包着 guarded `if` 结构。
|
||||
- 即使通用平坦化器已经能处理较简单的 handler,输出与参考结果仍然差很多。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 将 guarded-switch 适配器作为家族专用步骤运行,不要塞进通用平坦化器内部。
|
||||
- 优先压低直接 `loop/switch` 残留,再考虑命名或可读性层面的清理。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 仍然残留的直接 `loop/switch` 热点。
|
||||
- 仍然比 `decode.js` 更嘈杂的 guarded opcode dispatcher 结构。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 中华人民共和国公安部
|
||||
|
||||
当样本属于中华人民共和国公安部,并且符合 OB 家族特征时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 存在 `_0x` 风格标识符以及 OB 家族的包装结构。
|
||||
- 虽然与招投标样本属于同一家族,但 `loop/switch` 残留画像并不相同。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 复用共享的 OB 适配器,但为这种残留画像保留独立规则文档。
|
||||
- 以直接 `loop/switch` 块是否降到参考基线作为成功判断标准。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 经过 OB 适配器后依然存在的 `loop/switch` 平坦化残留。
|
||||
- 持续存在的 `_0x` 命名或包装层差距。
|
||||
@@ -0,0 +1,18 @@
|
||||
# reese84
|
||||
|
||||
当文件路径、hint 或样本标签明确指向 `reese84` 时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 样本体量大,呈现明显的 VM 风格,并且 dispatcher 残留较重。
|
||||
- 完成结构性处理后,`inline-literals` 的开销会显著升高。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 优先采用 `normalize -> prune -> inline-dispatchers -> flatten -> if-to-switch` 这条顺序。
|
||||
- 除非在更小的裁剪样本上已经验证安全,否则跳过后置的 `inline-literals`。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 仍然存在的 dispatcher 包装层。
|
||||
- 相对 `decode.js` 的最终行数差距。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 同花顺
|
||||
|
||||
当样本属于同花顺,或属于相关的 split-order wrapper 家族时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 残留的 `split('|')` 顺序字符串会驱动 `loop/switch` 平坦化。
|
||||
- 在抵达真正的顺序字符串之前,包装对象还会层层指向其他包装对象。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 在 `flatten-array-control-flow.js` 之前插入专门的顺序源适配器。
|
||||
- 不要默认把 `inline-literals` 当成这里的瓶颈解法。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 尚未解开的 `split('|')` 顺序数组。
|
||||
- 由多层包装对象驱动的剩余 `loop/switch` 代码块。
|
||||
@@ -0,0 +1,17 @@
|
||||
# 小红书
|
||||
|
||||
当样本属于小红书时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- wrapper 清理相对较轻,剩余平坦化也不算多。
|
||||
- 可读性提升主要依赖结构规范化,而不是更深的求值或执行。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 让适配器保持轻量。
|
||||
- 优先做规范化和包装层清理,而不是激进的常量传播。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 仍然与 `decode.js` 存在差异的小型 dispatcher 或包装层残留。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 网易易盾
|
||||
|
||||
当样本属于网易易盾时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 通用 dispatcher 处理之后,wrapper 仍然比较密集。
|
||||
- 做一轮字面量传播通常仍然有帮助,而且不至于把文件推向病态体积。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 让 dispatcher 适配器尽量贴近 `inline-dispatchers` 这一步。
|
||||
- 允许执行一轮 `inline-literals`,但不要盲目重复。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 相对 `decode.js` 的 dispatcher 包装层数量差距。
|
||||
- 主平坦化已经消除后,仍然存在的少量结构差距。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 安全改写规则
|
||||
|
||||
这份文档只回答一个问题:当前节点是否“确定可还原”。
|
||||
|
||||
只有在答案明确为“是”时,才做 AST 替换。
|
||||
|
||||
## 可以直接替换的典型场景
|
||||
|
||||
- 数组字面量下标访问,且下标是 `NumericLiteral`,数组内容本身也稳定。
|
||||
- 已建立最小解密环境后,调用参数全是数字字面量或纯字面量。
|
||||
- `['a', 'b', 'c'].join('')`、`String.fromCharCode(...)`、`split('').reverse().join('')` 这类纯计算表达式。
|
||||
- 明确的字符串或数字别名链,例如 `var a = 'x'; var b = a;`。
|
||||
- 可以静态确认结果的恒真或恒假比较,例如 `if ('a' === 'a')`。
|
||||
|
||||
## 不要直接替换的高风险场景
|
||||
|
||||
- 依赖 `this`、`window`、闭包外部变量或浏览器环境的调用。
|
||||
- 含 getter、setter、代理对象、动态属性访问、副作用函数调用的表达式。
|
||||
- 不能确认 binding 是否稳定的别名链。
|
||||
- 顺序很关键的逻辑表达式、逗号表达式、条件测试表达式。
|
||||
- 需要跨作用域推断执行结果的节点。
|
||||
|
||||
## 默认判断顺序
|
||||
|
||||
1. 先看节点结果是否只依赖字面量和局部稳定绑定。
|
||||
2. 再看求值过程中是否存在副作用。
|
||||
3. 再看是否依赖执行顺序、宿主环境或外部状态。
|
||||
4. 任意一步无法确认,就不要替换。
|
||||
|
||||
## 常见误区
|
||||
|
||||
- 不要把所有 `CallExpression` 都拿去局部求值。
|
||||
- 不要把 `LogicalExpression` 默认折叠成左值或右值,尤其是含函数调用时。
|
||||
- 不要把 `StringLiteral`、`NumericLiteral`、`BooleanLiteral` 再次纳入“循环 evaluate 并回写”的流程。
|
||||
- 不要为了追求完全还原而牺牲稳定性。
|
||||
|
||||
## 推荐做法
|
||||
|
||||
- 先得到“结构可读、关键字符串已恢复、主控制流已展开”的结果。
|
||||
- 把不能静态确认的区域保留下来,等后续结合运行时证据再处理。
|
||||
- 每完成一批替换后重新 parse 一次,确保输出仍然稳定。
|
||||
@@ -0,0 +1,61 @@
|
||||
# 逗号表达式与结构标准化
|
||||
|
||||
这份文档用于处理 `SequenceExpression`、表达式语句、`if`/`for` 中的逗号表达式,以及部分可安全收敛的结构标准化。
|
||||
|
||||
## 总原则
|
||||
|
||||
- 不要把所有逗号表达式都统一拆成多条语句。
|
||||
- 必须先看父节点,再决定改写方式。
|
||||
- 必须保证执行顺序和副作用顺序不变。
|
||||
|
||||
## 按父节点判断
|
||||
|
||||
### `ExpressionStatement`
|
||||
|
||||
- 通常可以直接拆成多条表达式语句。
|
||||
- 但如果末尾结果还被后续逻辑隐式依赖,先确认再拆。
|
||||
|
||||
### `IfStatement.test`
|
||||
|
||||
- `if (a(), b(), test)` 可以改写为:
|
||||
1. 先把 `a()`、`b()` 提到 `if` 之前。
|
||||
2. 保留 `if (test)`。
|
||||
|
||||
### `ForStatement.init`
|
||||
|
||||
- `for (a(), b(), c(); test; update)` 中的 `init` 逗号表达式最适合先拆,因为它只执行一次。
|
||||
|
||||
### `ForStatement.test`
|
||||
|
||||
- `for (init; a(), b(), test; update)` 可以改写为:
|
||||
1. 保留循环头里的稳定部分。
|
||||
2. 把 `a()`、`b()` 放到循环体开头。
|
||||
3. 在顶部加基于 `test` 的 `break` 守卫。
|
||||
|
||||
### `ForStatement.update`
|
||||
|
||||
- 只在循环体中不存在 `continue` 风险时再拆。
|
||||
- 如果存在 `continue`,把更新表达式搬到循环体尾部可能会改变行为,应保守跳过。
|
||||
|
||||
### `ReturnStatement` 与 `VariableDeclarator`
|
||||
|
||||
- 这两类需要分别处理,不能简单套用表达式语句的拆法。
|
||||
- 如果暂时没有足够稳定的模板,宁可不动。
|
||||
|
||||
## 可顺手清理的结构
|
||||
|
||||
- 零参数、立即执行、纯包裹的 IIFE。
|
||||
- 表达式语句里的简单三元噪声。
|
||||
- 可静态确认且不改变副作用顺序的简单逻辑表达式语句。
|
||||
|
||||
## 不要做的事
|
||||
|
||||
- 不要把复杂 `LogicalExpression` 默认改写成 `if`。
|
||||
- 不要在不知道父节点语义的情况下使用批量 `replaceWithMultiple`。
|
||||
- 不要只因为“文本更好看”就重排表达式执行顺序。
|
||||
|
||||
## 验证建议
|
||||
|
||||
- 结构标准化后立即重新 parse。
|
||||
- 对 `for.test` 和 `for.update` 的改写,优先用代表性样本回归一次。
|
||||
- 如果改写后只提升了少量可读性,却增加了行为不确定性,就回退。
|
||||
@@ -0,0 +1,53 @@
|
||||
# 字符串表与最小求值
|
||||
|
||||
这份文档用于处理字符串表、解码包装、局部别名链和最小引导 `eval`。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 文件开头几条语句就在初始化解码器。
|
||||
- 顶层 IIFE 里传入数组、对象或字符串表。
|
||||
- 源码里大量出现 `fn(12)`、`arr[123]`、`String.fromCharCode(...)`。
|
||||
- 第一层去壳之后,函数内部还存在二次字符串恢复。
|
||||
|
||||
## 推荐顺序
|
||||
|
||||
1. 先定位数组字面量、解码函数和别名链。
|
||||
2. 尽量优先做静态恢复,例如数组下标、纯字面量拼接、纯数字参数调用。
|
||||
3. 只有静态无法推进时,才执行最小引导代码。
|
||||
4. 删除已经消费掉的引导声明或包装层。
|
||||
5. 重新 parse 一次,再进入控制流还原阶段。
|
||||
|
||||
## 最小引导 `eval` 原则
|
||||
|
||||
- 只执行前几条足以构造解码器的代码。
|
||||
- 不要直接执行整份源文件。
|
||||
- 不要在未隔离的环境里执行明显依赖 DOM、`window`、定时器或网络的逻辑。
|
||||
- 求值目标应尽量窄,例如“只恢复数字参数到字符串的函数调用”。
|
||||
|
||||
## 常见模式
|
||||
|
||||
### 顶层 IIFE 带数组参数
|
||||
|
||||
- 从顶层调用提取 `formalParam -> actualArg` 映射。
|
||||
- 只替换明确命中的数组下标访问。
|
||||
- 处理包装函数里直接可展开的语句块。
|
||||
|
||||
### 前几条语句就是解密引导
|
||||
|
||||
- 只截取前 2 到 5 条关键语句。
|
||||
- 建立最小运行时后,恢复只带字面量参数的调用点。
|
||||
- 删除已消费的引导语句。
|
||||
|
||||
### 去掉第一层后,局部函数还有二次恢复
|
||||
|
||||
- 先展开顶层 IIFE。
|
||||
- 重新 parse。
|
||||
- 再识别函数内部那组“构造器 + 成员表达式 + 数字下标”的模式。
|
||||
|
||||
## 停止条件
|
||||
|
||||
- 主要字符串表已经恢复。
|
||||
- 核心解码器入口已经清楚。
|
||||
- 剩余部分不再适合纯静态恢复。
|
||||
|
||||
满足以上条件后,应切换到控制流或结构标准化阶段。
|
||||
Reference in New Issue
Block a user