本帖最后由 13955925361 于 2026-10-29 17:49 编辑
ob混淆反解skill · 对外分发版
obfuscator.io(ob)及其变种混淆 JS 的 AST 反解技能包:一套站点无关的可复用 Babel pass 库 +
"观察 → 归因 → 写 pass → 验证" 的工作流 + 解密器接线 playbook + 语义无损验证脚本。
本版不含任何样例代码、原始混淆文件或反解产物,也没有一键复跑脚本(它需要样例输入)。
所有命令都以你自己的 encode.js 为输入。skill/cases/ 里保留的是推导思路与判据,
不是可分发的代码;文档中不出现可定位的站点地址。
脚本注释里提到的 run_all.js 是开发包的一键复跑器(需要样例输入),本版未含。
一、目录结构
ob混淆反解skill/
├── skill/ ★技能本体(文档 + 脚本)
│ ├── SKILL.md 入口:特征速查、管线、验收标准(先读这份)
│ ├── WORKFLOW.md Stage 0~F 操作手册
│ ├── DECRYPTOR.md 解密器接线心法 + 原型库 A/B/C/D
│ ├── PITFALLS.md 28 个坑 + 十二条共性错误思考 + 排查顺序
│ ├── cases/ 三个案例的完整推导链(思路示范)
│ └── scripts/
│ ├── probe.js Stage 0 指纹探测
│ ├── deob.js ★默认入口(自动带 --auto-oracle --split-alias)
│ ├── deobfuscate.js 主管线(A 归一化 → B 解密 → C 还原 → D/E 收敛)
│ ├── passes.js ★可复用 pass 库(含 --auto-oracle 多组解密器发现)
│ ├── test_gates.js 第 0 阶段:闸门自检
│ ├── check_decode_safety.js ⑥ 该不该解(S1~S4)
│ ├── verify.js F1~F4 验收
│ ├── check_badvalues.js 错值 / 漏解检测
│ ├── runtime_smoke.js 产物真加载冒烟
│ └── browser_truth_probe.js 生成浏览器真值探针页(可选·事后)
├── node_modules/ 56 个依赖包(按闭包复制,约 25 MB)
├── package.json
└── RUN.md 本文件
二、环境要求
- 自带
node_modules:从 @babel/parser @babel/traverse @babel/types @babel/generator jsdom
五个入口按各包 dependencies 递归求闭包后复制,放在任何目录都能跑,不需要 npm install。
- 栈大小:大 bundle 必须
node --stack-size=8000,否则会 Maximum call stack size exceeded。
- 全程离线:解密不需要浏览器、不需要 MCP。可选的浏览器对拍是产物出来之后的事后核对。
三、快速开始:解密你自己的 encode.js
按顺序执行(S = 本包根目录,encode.js 换成你的文件):
# 0) ★前置:先验"闸门本身"可不可信,再采信后面 safety 的结论
node --stack-size=8000 skill/scripts/test_gates.js encode.js --log out.gates.log
# 1) ⑥ 该不该解:数组封闭性 / 调用点纯读 / 取值确定性 / 值合法性
node --stack-size=8000 skill/scripts/check_decode_safety.js encode.js --json out.safety.json
# 2) 主管线:默认入口 deob.js 已替你带上 --auto-oracle 与 --split-alias
# 为什么设成默认:漏掉 --split-alias 不报错,只会让 `(wt=kt)(364)` 这类赋值式别名调用
# 【整组 0 处被解】,产物看着正常,只有 4) 的 provider 残留会报警——靠人记不如靠默认值。
node skill/scripts/deob.js encode.js decode.js
# 外部解密器(如 decrypt.js):node skill/scripts/deob.js encode.js decode.js --oracle-file decrypt.js
# 要显式关掉:--no-split-alias / --no-auto-oracle(本层开关,不传下游)
# 3) 验收:node verify.js <原始encode.js> <产物> [参考答案]
# 第三参数可省——没有参考答案时 D1/D2 自动跳过,F1~F4 仍是硬判据
node skill/scripts/verify.js encode.js decode.js
# 4) 错值 + 漏解检测(★必带 --base,否则覆盖率/方向性指标出不来)
node skill/scripts/check_badvalues.js decode.js --base encode.js
# 5) 产物真加载冒烟(jsdom + DOM/浏览器 API 桩)
node skill/scripts/runtime_smoke.js decode.js
--help 可看各脚本全部参数;外部解密器(不在 encode.js 里)用
deobfuscate.js --decrypt-module <你的解密器.js> 接线。
四、结果怎么判读
| 环节 |
合格 |
红线 |
| ⓪ preflight |
A 组该拒的都拒、该解的都解,C 组检查器不崩不空转 |
preflight 不过 ⇒ 后面 safety 的"危险 0 组"不可信 |
| ⑥ safety |
危险 0 组(且报告里无 unchecked) |
危险 N 组 ⇒ 那几组不能常量化;未覆盖/未判定 ≠ 通过 |
| deob |
日志有"解密 N 处"与"C7 回收",无 Babel 报错 |
解密 0 处 ⇒ 没认到形态(换 --group/--decrypt-module,别改主干) |
| verify |
F1~F4 全过;provider 残留 = 0 |
F3 悬空引用、F4 整组残留 |
| bad |
badValues 为空;provider 形态数组残留 = 0;随机串占比低于原始 |
随机串占比反升 ⇒ 数组错位(静默错值) |
| smoke |
SMOKE OK(或报告与原始文件行为一致) |
加载即卡死 / selfDefending 残留 |
三档口径:危险(查到且不符,exit=1)/ 未覆盖(该验却没验,exit=1)/ 未判定
(连重放都失败、拿不到 oracle)。第三档不产生替换风险,但绝不是"查过且通过"。
五、已知边界
- 解密器在外部文件:单文件发现层看不到它,
--auto-oracle 报 0 组属正常——改用 --decrypt-module 接线。
- 该不该解的闸门是保守的:判不了就报"危险/未覆盖"并跳过该组,不会为了多解而放宽。
- 两个真值源都是"解完之后"的可选核对(不是解密流程的一部分):
① browser_truth_probe.js 生成静态探针页,在真实浏览器跑原解码器切片,与沙箱值逐项对拍;
② 目标站同源双 iframe(A=原始 / B=产物)比可观测行为。没人开浏览器就先跑到第 4) 步全绿,产物即可用。
六、推荐阅读顺序
skill/SKILL.md —— 特征速查表 + 验收硬指标,判断"这个站属于哪类"。
skill/WORKFLOW.md —— 每个阶段干什么、先做什么(成员归并必须早于归一化)。
skill/DECRYPTOR.md —— 解密器怎么接线;原型不匹配时怎么自己推。
skill/PITFALLS.md —— 卡住时按末尾的"排查顺序"走,不要一上来就改 pass。
skill/cases/ —— 需要"怎么想"的示范时再读。
ob-ast-deobfuscation —— obfuscator.io AST 反解流水线
这套技能解决什么
把 obfuscator.io(ob)及其变种混淆的 JS 还原成可读、可分析、可纯算的代码。核心资产是一套站点无关的
可复用 Babel pass 库 + "观察→归因→写 pass→验证"的工作流 + 解密器接线 playbook。
设计哲学(务必先读):最终目标只有一个——反混淆成功。特征一样能直接复用最好;特征不一样,
也要能根据特征现场推理、写出插件完成还原。所以本 skill 沉淀的是"怎么想"的能力(学习原型框架 +
记忆踩坑归因 + 思考新形态推导),主干是通用 pass 库 + 推导方法,案例库是"如何从 AST 结构特征推导 pass"
的思考示范,不是"新网站匹配到样例N就套用样例N代码"的撞库查表。面对新站:
- 结构 pass 直接复用——它们针对 AST 形态 + 作用域语义编写,形态在就触发、不在就安全空转,绝不因换站产出错代码;
这条成立的前提是形态先被压统一:所以成员归并(Stage 0.5)必须排在归一化之前。不做它,各站的对象
就是增量构建/别名组装/嵌套各式各样,后面每个 pass 都得加站点特有的附加筛选条件——可复用的要素就没了。
- 默认不需要重写解密器接线:
--auto-oracle 已把原型 A/D 都覆盖成一条按【结构特征】的通用前置
(全作用域发现 N 组“数组 provider + 轮转 + selector” → 逐组抠源码沙箱重放 → 汇总成多组 oracle 表)。
只有当解密器脱离真实环境跑不起来(要浏览器对象/自定义算法)时,才把接线写进 --oracle-module 的模块里,
主干依然不动。★绝对不要为某个站去主干 pass 加“本站筛选条件”——那是下一个站又要改一次的债。详见 DECRYPTOR.md;
- 原型都不匹配时,靠 cases 教的推导链(观察→归因→写 pass→验证)现场推——特例的价值是教会思考,
特征还原只是副产品。
一句话:授人以渔。结构框架要背熟、可复用;具体筛选条件(偏移量/函数真名/参数形态/编码方式)每站现写;
特征对不上时,用 cases 里的思考方式自己推导。
目录
ob-ast-deobfuscation/
├── SKILL.md 本篇·总览与快速上手
├── WORKFLOW.md 七阶段流水线的步骤与原理(Stage 0→A→B→C→D→E→F)
├── DECRYPTOR.md ★解密器接线 playbook:原型库 + 五步框架 + 推导法(每站唯一必重写处)
├── PITFALLS.md 踩坑记录(每条:现象→根因→修法→可复用教训)
├── cases/
│ ├── case-1-inline-trio.md 内联三件套+轮转(原型A)·无CFF/无包装器
│ ├── case-2-external-decryptor.md 外部双参解密器(原型B)·stringArrayWrappers+CFF+toString反调试
│ └── case-3-cff-wrappers.md 单文件内联四件套+索引表(原型D)·CFF外壳是 for(;!![];)·错值陷阱
└── scripts/
├── passes.js 可复用 pass 库(3 个 A-前置 + 14 个结构 pass + 静态求值下层 + 解密器接线层 + runPipeline)
├── probe.js Stage0 特征探测(只读,输出指纹→决定开哪些pass/怎么接解密器)
├── deobfuscate.js 管线运行器(结构pass全复用,只在此接线本站解密器)
├── verify.js 静态语义无损验证(F1解析/F2编译/F3零悬空 + D1字符串/D2属性名 对照组差分)
├── check_badvalues.js 错值检测 + 【基线自证】(--base <原始文件>:非内建名下标 / provider 残留 /
│ fn(数字) 调用点消解率 / 随机串占比方向——后三条抓“根本没解”,不靠参考答案)
├── test_gates.js 【前置自检】用真实文件把闸门自己打一遍(写法绕过 / 崩/空转 / 把“没查”报成“安全”),
│ A 组 7 项 + C 组 4 项;发现不出组则 SKIP(盲区,不算通过)
├── check_decode_safety.js 【该不该解】四道闸门(S1 数组封闭性 / S2 调用点纯读 /
│ S3 取值确定性 / S4 值合法性),逐组出结论,任一不过 exit=1
├── browser_truth_probe.js 【真值对拍】把原始文件的解码器原样切片、在真 Chrome 里跑,
│ 与 Node 沙箱重放逐组逐调用点比(验“值对不对、有没有整体错位”)
└── runtime_smoke.js 运行时冒烟(整包加载 + 正则回溯定点探针,抓静态测不出的"一加载就卡死")
快速上手(新站点标准动作)
# 依赖:@babel/parser @babel/traverse @babel/types @babel/generator(脚本按位置向上找 node_modules)
# 0) 【前置自检】先验工具再验站点:拿一个有解密器组的原始文件把闸门自己打一遍
# 任何一项不符 ⇒ 后面 safety 报的“危险 0 组”全部不成立(发现不出组时 SKIP,那是盲区)
node scripts/test_gates.js encode.js [--group 组键] [--log gates.log]
# 1) Stage0 探测:看清用了哪些混淆手段 + 定位解密器
node scripts/probe.js /path/to/encode.js
# 2) 接线解密器并跑管线(据 probe 结果选选项)
# · 解密器在外部文件(原型B):
node scripts/deobfuscate.js encode.js out.js --oracle-file decrypt.js [--oracle-name _0x293d]
# · 解密器在源文件内(原型 A/D,无论几组、嵌在哪个作用域):一条命令自解析,无需手工抠源码
node scripts/deobfuscate.js encode.js out.js --auto-oracle [--oracle-name I]
# · 调用点用别名(`wt = kt` / `(t = Mt, t(452))`):加 --split-alias(别名链由接线层自动反查)
# · 解密器要真实环境才能跑:把接线写进自己的模块 → --oracle-module wire.js(主干不动)
# · 仅结构还原/冒烟(不接解密器):不给 --oracle-file/--auto-oracle 即可
# 2.5) 【该不该解】逐组过四道闸门,再决定要不要常量化(★不解也要说明理由)
node scripts/check_decode_safety.js encode.js [--group 组键] [--json safety.json]
# S1 数组封闭性(轮转之外有没有被写/逃逸)|S2 调用点纯读(不是赋值目标、不喂 eval)
# S3 取值确定性(重读/交叉读一致 → 不依赖调用历史)|S4 值合法性(下标在范围内、可打印)
# 四道全过 ⇒ 可解;任一不过 ⇒ 打印证据 + exit=1,不许默默常量化
# 结果分三档:危险(查到且不符)/ 未覆盖(该验却没验,exit=1)/ 未判定(连重放都失败,汇总里单列)
# 3) 验证语义无损(【默认不需要参考答案】;有对照组才传第三个参数做 D1/D2 差分)
node scripts/verify.js encode.js out.js [reference.js]
# 静态全绿也可能【静默错值】或【漏解】—— 必跑错值 + 基线自证(基准=原始文件):
node scripts/check_badvalues.js out.js --base encode.js
# 3.5) 【终审】独立真值源(★不靠自己的沙箱自证)——【解完之后】做,不参与解密
# ★解密全程离线:没有浏览器/MCP 时,跑到 4) 全绿就能交付;这一步是可选加强证据
node scripts/browser_truth_probe.js encode.js probe/
# 生成 index.html + node.json:原始解码器原样切片、各包一个 IIFE(防同名遮蔽),
# 有人开浏览器时读回 {groups,totalSites,okSites,mismCount}(mismCount 必须 0);没人开就先放着。
# 再进一步(同样只在产物完成后):目标站同源双 iframe 分别注入原始/产物,比导出面与字段结构。
# 4) 运行时冒烟(★必做:静态层在原理上测不出"语法合法但一执行就卡死")
node scripts/runtime_smoke.js out.js [--ref encode.js]
可复用 pass 库(scripts/passes.js)
站点无关、直接复用的结构 pass(形态在则触发,不在则安全空转):
| Pass |
作用 |
触发形态 |
关键安全闸门 |
A-前置<br>normalizeLiterals<br>splitSequenceStatements<br>restoreParamInit |
先把"声明形态"立起来:模板串→字符串;语句级逗号序列拆成多句;函数头的形参初始化提成 var 声明 |
`a!b!c`.split('!') / a=…,b=…; / function f(a,b){ a={…}, b=fn } / function f(a,Y){ Y={}; … } / if (IH={}, IH["k"]=fn, …) |
只提"常量/静态可证"的形参初始化;给形参占位改名以断开旧绑定,否则 binding 永挂在形参上;只认 if 的 test(其 test 只求值一次;while/for-test 每轮求值、do-while 在体后求值,提出去都不等价) |
expressionSplit (A0) |
拆赋值式别名调用 |
(wt=kt)(364) |
仅纯改名赋值 |
constantFold (A1) |
折常量表达式 |
a+b/x-y/!![]/-1 |
isNodesEquivalent 防死循环、exit 序防爆栈 |
identifierInline (A2) |
别名内联收敛 |
var x=y;以及形参单次赋值 fx = I(含嵌在 if(test) 大逗号序列里的) |
无重赋值 + 遮蔽检查;init 是形参绝不内联(值由调用方决定) |
memberConsolidate (C0早期+C1解密后) |
增量对象归并字面量 |
var o={};o[k]=v |
value hoist-safe + 写入门(条件门内不提) + 读晚于写;整体读取(Id=o/f(o))允许,只有把它当赋值/update/delete 目标才拒 |
removeDeadAliasAssignments (C1-D) |
删死别名赋值 |
Id = IH 而 Id 被赋值后零次读取 |
左值单次赋值 + 零引用 + 右值无副作用 + 值被丢弃 |
collapseObjectAlias (C1-X) |
对象别名折叠 |
IH = Y(同一对象换名后用 IH["k"]) |
Y 必须常量绑定 + X 单次赋值 + 赋值无条件 + 读取晚于赋值且能看到 Y + 全部替换成功才删赋值 |
inlineWrapperStrings (C2-S) |
包装器字符串属性内联 |
_0x56c137['k'] |
resolveWrapperObject 三闸门 + 收敛迭代 |
inlineWrapperCalls (C2-J) |
花指令调用内联 |
_0x56c137['cmp'](a,b) |
自由名 binding 身份一致 + 沿用原形参名 |
removeUnusedWrappers (C2-R) |
删空引用包装器 |
引用归零的对象 |
零引用零重赋值 |
removeSelfDefending (C6) |
剥 selfDefending 自保护 / console 禁言壳 |
var X=W(this,function(){...search("(((.+)+)+)+$")...}); X(); |
回调含嵌套量词正则 或 读 console 成员且串集含 log+warn;删 X() 与 W["k"](X) 两种触发器 |
unflattenCFF (C3) |
控制流平坦化还原 |
while(!![]){switch(seq[i++])} / for(;!![];){...} |
序列任一值缺 case 放弃;case 落穿(fallthrough)放弃;外壳带 init 时把 init 整段提到循环前(等价,init 只求值一次),非 for 的 init/let/const 则放弃;摊平后同处删 seq/idx 声明(替换前判定引用是否全在被替换子树内) |
removeUnusedVars (C4) |
死变量回收 |
零引用 var |
init 无副作用才删、先 reparse |
removeDeadBranch (C5) |
删恒定条件分支 |
if(true/false) |
var 提升保全 + 含函数声明放弃 + let/const 不摊平 |
removeDecryptInfrastructure (C7) |
按引用计数连锁拆除解密三件套(轮转→selector→provider) |
轮转 IIFE + 零引用函数声明 |
顺序硬依赖(反过来中间步计数永不为零);自记忆化 f=function(){} 是体内赋值必须放行;序列项只换占位值不整句删;binding 查不到视为在用;有 eval/Function 时只对全局零引用声明补“名字不得是字符串字面量”闸门(闭包内名字反射不可达,否则会白扣体积,见 PITFALLS #25) |
接线层(decryptStrings(ast, oracle, oracleName) + 三个入口):
buildOracleFromExternalFile(原型 B:外部文件 module.exports)、buildOraclesByGroupReplay /
extractInlineStringDecryptor(原型 A/D:全作用域多组发现 + 逐组沙箱重放,含 provider→selector→轮转按序重放)、
--oracle-module(自己写接线模块)。selector 的两种 ob 写法都认:形态 M(自记忆化两条语句,偏移量在内层下标里)
与形态 S(单 return + 逗号序列)。★只认其中一种会让整个原型 A 站认不出解密器(case① 实测踩过,见 PITFALLS #23)。
已通用化的静态求值下层(站点无关,形态在则生效):singleAssignedValue(单次无条件赋值:覆盖
var a=v 与 if (..., a=v, ...) 两类载体)、staticObjectOf / staticStringOf(常量对象字面量取值)、
memberAssignedLiteral(o["k"]=<字面量> 成员赋值取值)、staticNumberPath(解密器参数的静态数值)。
详见 DECRYPTOR.md。
runPipeline(ast, opts) 按标准顺序串起来(顺序即依赖,见 WORKFLOW.md),opts 控制解密器接线、
是否开 expressionSplit、死分支轮数、只跑指定 pass。
特征速查表(识别入口层)
这是索引,不是答案:拿 probe.js 输出的指纹名对照,快速认出"这站像哪个原型 / 要开哪些 pass",
命中后必须回到 DECRYPTOR.md 或 cases 看推导链——表只告诉你"像谁",为什么这么写、
怎么纠错、本站要现写哪些筛选条件,全在推导示范里。指纹为 0 时对应 pass 安全空转,开着也无害。
| probe.js 指纹 |
高值意味着 |
动作 |
去哪看推导 |
Top8 高频调用名 + numericArgCalls |
定位到解密器 / 大数组函数。★两者都是下限不是事实:真名可能被形参别名遮掉而不进 Top8(case3 实测真名 I 不在榜),numericArgCalls 也不含"参数是索引表成员"的调用(case3:127 vs 实际 3902) |
命不中就转看"形态类指纹";认原型 → 接线 Stage B |
DECRYPTOR.md §2 · case3 §1.1 |
rotatorIIFE(push+shift 循环)+ rotatorSizes |
数组在运行期被原地校正;字节数悬殊时大的是主程序外壳不是轮转器 |
沙箱按 provider → 解密器 → 轮转器 重放,漏一步就静默错值 |
DECRYPTOR §1 步骤2 · case1 §4 · case3 §3 |
解密器 module.exports 在外部文件 |
原型 B |
buildOracleFromExternalFile |
case2 §1-2 |
解密器自记忆化(体内给自身赋值)+ 赋值式别名调用 (w=k)(n) |
原型 A(常见多组、嵌在函数体内) |
--auto-oracle --split-alias:形态 M + 全作用域多组发现 |
case1 §2-4 |
解密器在源文件内、单return+逗号序列+z=z-N |
原型 D(单组、顶层) |
同一条 --auto-oracle(形态 S) |
DECRYPTOR §1 · case3 §2 |
assignToParam 高 + var 声明极少 |
ob 把索引表/解密器别名塞进函数开头大逗号序列(可能在 if(test) 里),不是 declarator |
A2 认“形参单次赋值”;静态求值用 singleAssignedValue |
PITFALLS #15 · case3 §4 |
memberAssignLiteral 高 |
索引表用 o["k"]=<字面量> 构建,不在对象字面量里 |
staticStringOf 回落 memberAssignedLiteral |
case3 §5 |
wrapperLikeObjects 高 |
stringArrayWrappers 包装器 |
C2-S/C2-J/C2-R,作用域绑定三闸门 |
case2 §3 |
switchOnUpdate 高(与 whileTrue 无关) |
控制流平坦化 CFF;外壳可能写成 for(;!![];),此时 whileTrue=0 |
C3;缺 case / case 落穿放弃;序列载体要认全(switch(IZ[i++]) / switch(obj["k"][i++]) / switch("0\|1\|2"[i++]),只认第一种会让“C1 一归并、C3 还原数就掉”) |
case2 §4 · case3 §6 |
ifConstTest 高 |
恒真恒假死分支 |
C5 迭代删(var 提升保全) |
WORKFLOW Stage D/E |
emptyObjectIncr 高 |
增量对象 var o={};o[k]=v |
C0早期+C1(hoist-safe 才归并,可能正确空转) |
case2 §6 |
emptyObjectIncr 高 但对象是从形参接来的(binding.kind==='param') |
ob 的“对象池”花指令:X={}; X["k"]=fn; Y=X; Y["k"](…),整段可能塞在 if(test) 或 for(init) 里;残留表现是大量 X["随机串"] 与 X["随机串"]=function 成对 |
按顺序:A-前置提成 var → C1 归并 → C1-D 删死别名 → C1-X 折别名 → C2 内联/删除。少任何一步都会停在“只剩成员读写但不删” |
case3 §7 |
toStringMarkers / debuggerStmt |
selfDefending 自保护 |
必须剥(C6):嵌套量词正则在源码变长后会指数回溯,产物一加载就卡死;console 禁言壳同为脚手架,一并剥 |
case2 §5 · PITFALLS #10 |
四条不可违背的语义安全底线
- A. 能证明等价才替换:证明不了就原样保留(宁可留混淆,不可改行为)。
- B. 作用域敏感:同名不同作用域绝不混为一谈;一律用引用处 scope 向上 getBinding,禁止按名全局查表
(obfuscator 会跨函数重用同一个名字做不同对象,按名查表会拿错值且错得毫无痕迹)。
- C. 刷新作用域看需要,不机械 reparse:一句话判据——接下来要跑的代码读不读 scope/binding?
读:pass 之间用 reparse(重:整树重解析,runPipeline 的 step() 已统一做)、pass 内部改完又接着读就补 scope.crawl()(轻:只重扫当前作用域);不读:跳过。遍历引用一律 referencePaths.slice() 快照。
- D. 变换必须配删除:任何"摊平/内联/折叠"的 pass,都要在同一处把随之失去用途的声明一并删掉。
典型:C3 摊平 while(!![]){switch(seq[idx++])} 后,var seq=..., idx=0 就是纯脚手架。
★更难的是判据:replaceWithMultiple 之后 binding 已 stale(旧 referencePaths 仍指向脱树的旧节点,node 非 null),
引用计数恒非 0、事后怎么查都"还有引用"。所以要在替换前判定"这些引用是否全落在即将被替换的子树内"。
验收标准(硬指标)
反混淆的唯一硬标准是语义无损,不是"代码变短",也不是"跟课程答案像不像"。
★默认没有参考答案:F1-F4 + 错值形态 + 基线自证(以原始文件算工作量) + 真跑冒烟 就已经能定案;
D1/D2 只是“凑巧有对照组”时多加的一层差分——而且对照组可能和产物有同源缺陷,“跟答案一样”≠“对”(坑26)。
每轮改完 pass 都用同一套客观检查卡一遍:
- F1 产物可解析 / F2 可编译(vm.Script)/ F3 零新增悬空引用(产物 free 变量集合 ⊆ 原文件);
- F4 特征残留(CFF 外壳/恒真 if 应为 0;剩下的
switch 应是业务原生);
- D2 成员属性名“只对照组有” = 0(没误删任何 API 调用)。三类已内置解释,都不得当成误删:
脚手架属性(console/constructor/bind,C6 剥壳正确移除)、包装器属性名内联后消失(值已就地出现在调用点)、
换了载体(o.getterFor(i) 被 C2-J 内联成方法体、t.architecture = n 被归并进 {architecture: n})——
最后一类判据只认“名字在产物里以键位/定义位/字面量仍在”,不许放宽到任意同名标识符(否则真误删也会被解释掉);
★反向的那个数(“只有我有”)不是分数,是待办清单:它多就说明有花指令包装器没删。case3 顺着它
从 1079 降到 180,每一档下降都对应一个可复用的新闸门(C1-D / C1-X / if(test) 载体,见 case3 §7.1)。
- D1 字符串差分:少的若住在对照组保留的解密器字符串数组里,已由“本站 C7 已拆除三件套”自动解释(死项),
剩下的须人工归因(多为不透明谓词哨兵串/CFF序列串/被折叠合并的碎片,确认非误删);
- F5 错值检测 + F6 基线自证(
check_badvalues.js out.js --base encode.js):
· 【抓解错了】产物里不得出现 "xxx"["随机串"] 这种非内建方法名下标;可打印率不得 <90%。
★这是唯一能抓“oracle 接错/数组错位”的静态手段:F1-F3 全绿、字符串看着都正常,但语义已经错了。
· 【抓没解完】provider 形态数组残留必须 = 0、fn(数字) 调用点必须大量消解、
随机串占比必须低于原始文件。三条全部以【原始 encode.js】为基准,不需要参考答案;
退出码直接受它们影响(坑26:只靠“错值形态”永远查不出“根本没解”)。
静态层通过 ≠ 产物能用。必补 runtime_smoke.js:
- 产物整包加载不超时(超时 =
JSON.stringify//search 之类灾难性回溯,语法完全合法);
- 正则回溯定点探针 0 命中:这一层不依赖站点环境,即使产物因缺全局早早抛错也能查出卡死结构;
- 注意"执行抛错"不等于通过——代码没跑到底,卡死点可能根本没走到,必须看探针结论。
细节见 WORKFLOW.md、PITFALLS.md、cases/。
|