文档
/
实用方案
/

eval 与 new Function 下的 VM 混淆

eval 与 new Function 下的 VM 混淆

VM 混淆如何处理动态代码构造(直接 eval 与 Function 构造器)、哪些会被字节码化、哪些会被跳过、混淆器会发出哪些警告,以及如何诊断运行时的 ReferenceError。

为什么这很重要

VM 混淆会把函数体编译成通过运行时内解释器分派的字节码。外围作用域中的 标识符也会被重命名。这两种转换都会与那种在运行时由字符串构建出来的代码发生糟糕的相互作用: eval(s)new Function(...s)Function(...s)。如果运行时构建的代码引用了一个被 混淆器重命名过的标识符,那么在所生成的函数首次运行时,您就会得到 Uncaught ReferenceError: <renamed-name> is not defined

混淆器对每种模式的处理各不相同。下面的矩阵是简短版;本页其余部分会逐一 解释每个单元格。

混淆器的处理方式一览

您源码中的模式会发生什么
eval('literal string')(body 是字符串字面量)正常运行。包含此调用的函数 会失去 VM 字节码化(直接 eval 会读取周围的局部变量,而一旦函数被编译为字节码,VM 就不再保留这些变量)。
eval(dynamicExpression)可能在运行时以 ReferenceError 崩溃。 包含此调用的函数 —— 以及在其内部定义的每个函数 —— 也会失去 VM 字节码化。
(0, eval)(s) / window.eval(s)(间接)正常运行。包含此调用的函数会照常被 VM 字节码化。间接 eval 无法看到周围的局部变量,因此重命名不会破坏它。
new Function('a', 'b', 'return a + b')(所有参数都是字符串字面量)正常运行。包含此调用的函数会照常被 VM 字节码化。
new Function(dynamicBody) / Function(dynamicBody)可能在运行时以 ReferenceError 崩溃。 包含此调用的函数 —— 以及在其内部定义的每个函数 —— 也会失去 VM 字节码化。

所有这些模式也会作为非致命警告出现在混淆结果上 —— 关于警告形态与一段 CI 片段,请参阅下面的 在运行前发现问题

为什么静态与动态被区别对待

eval(s) 能够 读写 它被调用所在函数的局部变量。当 s 是字符串字面量时, 混淆器可以在混淆时解析其 body,并与周围代码一致地重命名标识符。当 s 是动态表达式时,解析只在运行时发生 —— 而那时标识符早已被 重命名,因此运行时构建出的源码引用的是已不存在的旧名称。

new Function(s) 的机制不同:其 body 始终像是定义在您文件顶部那样运行,只能访问 全局变量,永远访问不到调用点周围的局部变量。这本身是安全的 —— 但如果您通过 把一个被重命名的标识符拼接进 body 来构建它(例如通过某个内部已被混淆器 改写过的函数的 func.toString()),那么运行时编译出的函数仍会撞上同一类 ReferenceError

静态的 new Function('return 42') 从不带有这种风险:其 body 是一个重命名器从不检查的普通字符串,且 在运行时它只需要看到全局变量。混淆器会原样保留该调用,其外层函数仍然 有资格进行 VM 字节码化。

您在运行时看到的错误

常见的症状是在动态构建的函数首次运行时出现 ReferenceError

browser console

Error

这里的 TU 是混淆器在 bundle 的 IIFE 作用域内部引入的一个被重命名的标识符。动态 eval / Function 构造器调用求值了一个引用它的 body,但该 body 运行在一个 TU 未定义的作用域里。

在运行前发现问题

混淆器会通过 API 发出非致命警告,让您能在发布前于 CI 中捕获这些模式。有两种相关的警告 类型:

  • DynamicCodeRenameRisk —— 某个函数包含一个 body 在运行时构建的动态 eval / new Function / Function 调用。
  • VMDynamicCodeSkipped —— 由于上述模式之一,某个函数被跳过了 VM 字节码化。包含函数名(如可获得)以及触发该跳过的构造类型。

ci-build.mjs

JavaScript

如果某种警告类型在您的构建中是预期内的,而您更愿意从源头将其静默,而不是在 CI 中过滤它,那么 warnings 选项(v7.8.0+)可控制 getWarnings() 发出什么:'none' 会抑制一切,而像 { VMDynamicCodeSkipped: false } 这样的按类型映射则只静默某一种类型,同时保留其余的。

规避办法

  • 改用间接 eval((0, eval)(s)

    仅对 eval 情形有用。间接 eval 运行在全局作用域中,因此无法看到周围的局部变量 —— 但也正因如此,它也无法引用被重命名的标识符。包含该调用的函数保持 VM 字节码化。

  • 让 body 完全静态

    对于 new Function,如果您能把 body 表达为一个不含 插值的单一字符串字面量 / 模板字面量,那么该调用就不带重命名风险,围绕它的函数仍会被字节码化。 new Function('a', 'b', 'return a + b') 可以;new Function('return ' + expr) 不行。

  • 把该调用移入它自己的顶层函数,并拆开 IIFE

    每个顶层函数都会被独立检查。把动态代码构造调用提升到它自己的顶层 函数中,意味着只有那一个函数会失去 VM 字节码化,而不会让跳过通过一个 包裹您整个 bundle 的顶层 IIFE 层层传导。

  • 改用 vmTargetFunctionsMode: 'comment'

    选择性启用模式:只有被 /* javascript-obfuscator:vm */ 标记的函数才会被字节码化。不要标注 包含动态代码调用的那个函数,而对其余的进行字节码化。请参阅 定向混淆函数

  • vmForceCompileDynamicCode: true 覆盖该跳过(v6.14.0+)

    最后手段的逃生舱。启用后,混淆器会仍然对外层函数进行字节码化,并抑制 VMDynamicCodeSkipped 警告。仅当您能保证运行时构建的 body 永远不会引用 混淆器所重命名的标识符时才使用它 —— 否则您就是拿一次干净的混淆去换取一个运行时的 ReferenceErrorDynamicCodeRenameRisk 仍会触发,因此 CI 可以继续以它为门禁。在控制台中,这是 VM 部分 Overrides 组下的 “Force Compile Dynamic Code” 开关。

相关页面