该项目并未实现不可区分性混淆。您不应使用此产品来保护秘密。它的存在是为了阻止逆向工程,而非防止逆向工程。
一个 JavaScript 混淆器,旨在使代码对人类和大型语言模型(LLM)都极难阅读和分析。使用 TypeScript 编写。实现了来自同行评审的密码学混淆研究中的技术。
npm install
npm run build
# 基本使用
node dist/index.js input.js > output.js
# 指定目标 token 预算(默认:2,000,000)
node dist/index.js --target-tokens 500000 input.js > output.js
# 最小混淆(小输出)
node dist/index.js --target-tokens 10000 input.js > output.js
# 最大膨胀(10M tokens)
node dist/index.js --target-tokens 10000000 input.js > output.js
# 使用环境变量
INPUT_FILE=input.js node dist/index.js > output.js
# 帮助
node dist/index.js --help
| 选项 | 默认 | 描述 |
|---|---|---|
--target-tokens <n> | 2000000 | 目标输出大小(以 token 计)。小输入会膨胀至该上限。大输入会减少膨胀以保持预算。 |
--help, -h | 显示帮助信息 |
npm run build # 编译 TypeScript 到 dist/
npm run start # 运行混淆器(读取 input.js)
npm run test # 运行测试套件
npm run obfuscate-package # 对 npm 包运行兼容性测试
const { obfuscate } = require('./dist/obfuscator');
const code = 'function add(a, b) { return a + b; }';
const obfuscated = obfuscate(code);
// 带选项
const obfuscated = obfuscate(code, { targetTokens: 500000 });
混淆器在四个阶段中应用 20 个变换。每个阶段建立在前一个阶段之上。
| 顺序 | 变换 | 文件 | 描述 |
|---|---|---|---|
| 1 | 反调试陷阱 | transforms/antiDebug.ts | 注入 eval("debugger") 语句和 10-20 个带有素数间隔(5 秒 - 600 秒)的 setInterval 循环,反复触发调试器断点。每个实例使用唯一的编码字符串。 |
| 2 | 穿刺程序触发器 | transforms/tripwires.ts | 嵌入隐藏检查,将参数哈希与秘密值进行比较。5 种哈希模式(位指纹、模运算、charCodeAt、数字哈希、typeof+length)。对秘密输入触发静默状态损坏、忙循环或抛出异常。[论文 4] |
| 3 | LPN 噪声注入 | transforms/noiseInjection.ts | 通过算术计算中的拆分路径添加和取消随机噪声。6 种模式:加/减、XOR、乘/除、拆分双变量、计算哈希链、位旋转。中间值在没有追踪完整取消过程的情况下是无意义的。[论文 7] |
| 顺序 | 变换 | 文件 | 描述 |
|---|---|---|---|
| 4 | 控制流平坦化 | transforms/controlFlowFlattening.ts | 将函数体转换为 while(true) { switch((_s * P) % M) { ... } } 状态机,使用 模算术调度——case 值通过 (stateId * multiplier) % modulus 编码,使用随机素数参数。[论文 3] |
| 5 | 不透明谓词 | transforms/opaquePredicates.ts | 注入总是评估为 true 或 false 但在数学上难以证明的 if 条件(例如 (x*x+x)%2===0)。涵盖模算术、位运算和类型检查类别的 15 个谓词公式。 |
| 6 | 代理函数 | transforms/proxyFunctions.ts | 将所有函数调用路由至两个分发器:_fc(fn, ...args) 用于简单调用,_mc(obj, prop, ...args) 用于方法调用。使用捕获在局部变量中的 Function.prototype.apply 以增强弹性。 |
| 7 | 上下文窗口耗尽 | transforms/contextExhaustion.ts | 将表达式包裹在深度嵌套的三目运算符中,包含不透明条件和 void 表达式链以及条件 void 填充。迫使 LLM 将上下文窗口 token 浪费在噪声上。 |
| 8 | 逗号表达式合并 | transforms/commaExpressions.ts | 将连续的表达式语句合并为单个逗号表达式:a(); b(); return c() 变为 return a(), b(), c()。 |
| 顺序 | 传递 | 文件 | 描述 |
|---|---|---|---|
| 9 | 第一遍:编目 | passes/firstPass.ts | 遍历 AST 并对每个标识符进行编目,构建一个全局映射,为每个标识符分配一个随机的 6-16 字符 Unicode 名称,这些名称从 16 个脚本范围(中日韩、谚文、希腊、西里尔、天城文、泰文、阿拉伯、片假名等)中选择。 |
| 10 | 第二遍:替换 | passes/secondPass.ts | 将所有标识符名称替换为其混淆的 Unicode 等效名称。将 require() 参数编码为 String.fromCharCode(...)。将静态 import/export 源编码为 Unicode 转义字符串字面量。替换类 superClass 引用、模板字面量表达式和解构模式。 |
| 11 | 第三遍:虚拟参数 | passes/thirdPass.ts | 向每个函数声明和表达式注入 0-15 个随机未使用的参数。跳过带有 rest 参数的函数。移除所有注释。 |
| 顺序 | 变换 | 文件 | 描述 |
|---|---|---|---|
| 12 | 全局变量编码 | transforms/globalVariableEncoding.ts | 将全局变量引用(从 globalThis + window 包动态发现)替换为 eval("Name<suffix>".replace(new RegExp("<suffix>$"), ""))。两个字符串均通过字符串数组流转。 |
| 13 | 属性键编码 | transforms/propertyKeyEncoding.ts | 将点访问转换为计算访问,使用每作用域注册表。跨作用域访问有效,因为所有后缀在运行时通过 .replace() 解析为相同的属性名。 |
| 14 | 数字编码 | transforms/numberEncoding.ts | 11 种编码策略:移位+加、XOR 恒等、补码、除法、嵌套移位、双重 NOT、模运算等。每个实例唯一生成。跳过属性键和 switch case 值。 |
| 15 | 自完整性验证 | transforms/selfIntegrity.ts | 注入 2-4 个运行时检查:eval 原生代码验证、Function.prototype.toString 完整性、时间异常检测、代码结构验证。反篡改响应:忙等待、抛出异常、静默损坏。[论文 10] |
| 16 | 字符串数组提取 | transforms/stringArrayExtraction.ts | 将所有字符串收集到单个数组中,使用 链式 XOR 解密(条目 N 的密钥依赖于条目 N-1 的解码内容)和 稀疏位置相关错误模式(每个字符使用不同的 XOR 密钥,在选定位置带有 LPN 启发的稀疏错误)。[论文 2, 9] |
| 17 | Console 存根 | obfuscator.ts | 动态发现所有 console 方法并将其设置为无操作函数。 |
| 18 | Terser 压缩 | obfuscator.ts | 通过 terser 去除空白/格式(mangle: false,compress: false)。如果 terser 无法解析输出,则回退到基于正则表达式的去除。 |
死代码在多个点注入,使用两种生成策略:
| 策略 | 来源 | 描述 |
|---|---|---|
| 基于模板 | transforms/deadCodeInjection.ts | 9 种模板类型:循环累加、数组构建、对象操作、字符串拼接、嵌套条件、try/catch、while 倒计时、switch 计算、位运算链。模板引用真实作用域变量。 |
| 基于变异 | transforms/deadCodeInjection.ts | 克隆 真实 语句并进行变异:在等价组内交换运算符、扰动常量、重命名标识符。生成在 AST 结构上与真实代码无法区分的死代码。[论文 3] |
死代码注入点:
多个变换受同行评审的密码学混淆研究启发:
| 论文 | 作者 | 实现的技术 |
|---|---|---|
| [论文 1] 论程序混淆的(不)可能性 | Barak, Goldreich, Impagliazzo, Rudich, Sahai, Vadhan, Yang | 不可混淆函数测试用例——验证工具,测试秘密是否在混淆后存活 |
| [论文 2] 所有电路的候选 iO 和功能性加密 | Garg, Gentry, Halevi, Raykova, Sahai, Waters | 链式字符串解密——Kilian 风格随机化,每个条目的密钥依赖于前一个解码字符串 |
| [论文 3] 基于多线性子群消除假设的 iO | Gentry, Lewko, Sahai, Waters | 基于变异的死代码(结构上与真实代码相同);CFF 中的模算术状态转换 |
| [论文 4] 如何使用 iO:可否认加密及其他 | Sahai, Waters | 穿刺程序触发器——对秘密输入触发的隐藏检查 |
| [论文 7] 基于良好基础假设的 iO | Jain, Lin, Sahai | 数字计算中的 LPN 启发噪声注入 |
| [论文 9] 基于双线性映射和 LPN 变体的 iO | Ragavan, Vafa, Vaikuntanathan | 具有位置相关错误模式的稀疏 XOR 编码 |
| [论文 10] 零量子电路的 iO 及其应用 | Bartusek, Malavolta | 用于死代码质量验证的空电路测试;自完整性验证(双模式) |
两个验证工具测量混淆质量,位于 src/verification/:
verification/nullCircuitTest.ts)混淆一个真实函数和一个“空”函数(相同形状,执行无操作),然后比较 14 个结构指标以评分它们的可区分程度。相似度越高 = 混淆越好。
import { runNullCircuitTest } from './verification/nullCircuitTest';
const result = runNullCircuitTest(realCode, paramCount, stmtCount, threshold, targetTokens);
console.log('相似度:', result.similarity); // 0.0-1.0
verification/unobfuscatableTests.ts)来自论文 1 不可能证明的 7 个测试用例,试图从混淆代码中提取秘密:
import { runAllTests, printSummary } from './verification/unobfuscatableTests';
console.log(printSummary(runAllTests(10000)));
测试:点函数(密码)、魔法数字、金丝雀字符串、嵌入密钥、URL、正则表达式模式、控制流签名。
--target-tokens 选项通过膨胀预算控制输出大小,该预算缩放死代码注入(主要体积杠杆)。预算门控变换:
| 预算比率 | 启用的变换 |
|---|---|
| > 3 | 反调试、触发器、CFF、不透明谓词、逗号合并 |
| > 5 | 代理函数、属性键编码、噪声注入、自完整性 |
| > 8 | 上下文窗口耗尽 |
| > 10 | 全局变量编码 |
死代码乘数从 1x(比率 30)到 150x(比率 1500+),控制死 switch case 和不透明谓词分支的数量和大小。