文档
/
实用方案
/

VM 防御遥测与响应

VM 防御遥测与响应

Pro
v7.4.0+

使用 vmDefenseHook 将 VM 防御检测结果上报到您的后端,并用 vmDefenseReaction 调整每个检测类别的响应方式 - 从完全不破坏、仅遥测的构建,到一个对被窃取的包立即中断的构建。

观看

Obfuscator.io Defense Reactions: Break, Decoy, and the VM Defense Hook

在 YouTube 观看

问题

VM 防御 - vmSelfDefending、vmDebugProtection 与 vmDomainLock - 是就地生效的:当检测到调试器、自动化工具、被篡改的环境或未授权的域名时,受保护的代码会中断,或静默地毒化自身的结果。这能阻止攻击者,但默认情况下 您对此一无所知。您无从得知您的包被探测的频率、是哪个检测器触发的,也不知道某项防御是否正在破坏合法用户的使用。

有两个选项填补了这一空白。它们都不会 启用 任何防御 - 只是观察并引导您已经开启的防御:

  • vmDefenseHook - 一个全局回调,每当某项防御检测到什么时都会收到一个信号对象。用它把遥测发送到您的后端。
  • vmDefenseReaction - 一个按类别的映射,用于选择已启用的防御 如何 响应:中断、decoy,或在本地什么都不做。

这两个选项都是在 v7.1.0 中引入的,但这里的每个示例都使用对象形式 vmDefenseHook: { name },它需要 v7.4.0。更早的版本接受一个纯字符串(vmDefenseHook: '__vmDetection');从 v8.0.0 起,这种形式会被拒绝,因此请始终使用对象形式。

方案 1 - 将检测结果上报到您的后端

步骤 1 - 在混淆后的包加载之前,注册一个全局钩子函数

VM 运行时及其防御在您受保护的程序 之前 运行,因此许多检测会在启动期间触发。请在宿主页面中、混淆脚本标签之前,把钩子定义为一个普通的全局变量:

HTML

步骤 2 - 让 vmDefenseHook 指向它

该选项是一个对象,其 name 是要调用的全局函数(aliases 是可选的 - 见下文):

JavaScript

在控制台中,一旦启用了至少一项防御(vmSelfDefending、vmDebugProtection 或 vmDomainLock),高级保护部分中就会出现 VM Defense Hook 字段。

步骤 3 - 在您的后端接收信号

每次检测都会用单个 signal 对象调用钩子:

  • source - 具体的检测器:headless、node、agent、agentBrowser、domain、debugger、sandbox、nativeHook、timing 或 integrity。自 v7.4.0 起,原先的 env 和 inspector 检测器改在 source: 'debugger' 下上报。agentBrowser 在 category: 'automation' 下上报,并在启用 vmDebugProtection 的浏览器目标上运行(v7.9.0+)。
  • category - automation、debugger、sandbox、domain、tamper 或 integrity。node 来源在 category: 'debugger' 下上报(v7.4.0+)。
  • score、threshold - 检测得分及其越过的阈值

一个最小的接收端点(此处以 Express 为例;任何接受 POST 的后端都可以)。它会把请求体规整为数组,因此也能处理下方缓冲模式所提交的批量形式:

JavaScript

该钩子 仅用于上报 - 它的返回值会被忽略,缺失或抛出异常的钩子都是静默的空操作。它永远无法禁用某项防御,因此攻击者删除或破坏您的钩子也一无所获。要改变某项防御的 行为,请使用 vmDefenseReaction(方案 2)。

请在宿主页面中定义钩子,而不要放在混淆源码内部

做遥测时您希望捕获每一次检测,而许多检测会在启动阶段触发 - 定义在混淆包 内部 的钩子注册得太晚,无法捕获这些检测,而且如果它被 VM 编译,就要等到您的程序运行时才能被访问到。无论哪种情况它都是安全的(缺失的钩子会空操作,自身触发检测的钩子也不会被再次递归调用),但为了完整覆盖,请在宿主页面中提前注册它。

唯一的例外是一个 只 对运行时检测作出响应的钩子 - 例如在使用过程中调试器打开时进行清理。这样的钩子可以放在混淆包内部;见方案 3。

要同时保护您的上报逻辑,请把已注册的钩子保持为一行缓冲,并从您的混淆代码中把它抽干:

JavaScript

JavaScript

重命名信号字段(aliases)

默认的 source / category 值都是描述性的名称,因此任何为该回调埋点(或阅读输出)的人都能识别出这项保护以及是哪个检测器触发的。aliases 会把信号字段重命名为您所选择的不透明令牌,且在信号发出 之前 于 VM 内部应用,因此这些名称永远不会出现在输出中或到达回调。您的应用知道自己的映射关系,并把这些令牌转发到您的后端。

别名是按字段设置的:每个字段接受一个 key(回调收到的属性名);字符串名称字段 source 和 category 还接受一个 values 映射,而 score / threshold 是数字,只接受 key。未设置的条目保持其默认名称。

JavaScript

在控制台中,信号别名 一节位于 VM Defense Hook 字段之下。

这是 规避指纹,而非保密 - 该映射仍可通过反复测试推断出来,因此它唯一的好处是不暴露稳定、见名知意的名称。

方案 2 - 调整默认响应

vmDefenseReaction 配置每个检测 类别 如何响应。它不会启用任何东西 - 防御本身由 vmSelfDefending、vmDebugProtection 和 vmDomainLock 开启;此选项只选择已启用的防御如何响应。类别是控制的单位:一个类别中的每个检测器都会执行该类别的响应,而为一个其选项处于关闭状态的类别所设置的响应,根本不会有任何效果。

类别由谁启用何时响应
automationvmSelfDefending 或 vmDebugProtection代码正被软件而非真人驱动:无头或自动化浏览器、抓取 / 测试框架,或正在逐步执行页面的 AI 编码智能体。
debuggervmDebugProtection 或 vmSelfDefending有人打开了调试器或浏览器的开发者工具检查器,正在单步执行运行中的代码以理解它。
sandboxvmDebugProtection代码根本不在真实浏览器中运行 - 它已被提取到一个被模拟或脚本化的 JavaScript 环境中,以便离线执行和研究。
domainvmDomainLock代码正运行在您未授权的站点上:一个不在您 vmDomainLock 允许列表中的主机(例如,您的包被复制到了别人的域名上)。
tampervmSelfDefendingVM 周围的 JavaScript 环境已被修改以监视或劫持它,例如原生浏览器内置对象被替换成了被插桩的版本。
integrityvmSelfDefending受保护的包自您生成以来,其自身代码已被编辑或打过补丁。

键是这六个类别名称,或 default(针对未指定类别的回退)。值为:

  • break - 立即中断
  • decoy - 在被毒化的状态上继续运行,静默地产生错误结果。decoy 需要在浏览器目标上启用 vmDebugProtection 或 vmDomainLock;否则它的行为等同于 break。
  • none - 在本地什么都不做(仅遥测)

您未设置的类别会回退到内置默认值:

JavaScript

default 会触及 每一个 类别,包括 integrity 和 tamper,因此 { default: 'none' } 是一个真正不破坏、仅遥测的构建:

JavaScript

JavaScript

在控制台中,一旦启用某项防御,高级保护部分中就会出现 VM Defense Reactions 选择器;只有在发出其检测器的某项防御处于开启状态时,对应类别才可编辑。

方案 3 - 在防御中断之前运行您自己的逻辑

钩子不仅仅用于上报 - 它也是在防御作出响应 之前 运行您自己响应逻辑的唯一可靠位置。当调试器在运行中的页面上打开时,您可能想清除屏幕上的内容,或把视图替换为 404 页面,并在代码中断之前完成。

为什么要用钩子,而非应用中其他地方的代码:break 会停止后续所有字节码,因此在防御触发 之后 才运行的清理逻辑 - 尤其当它自身也被 VM 混淆时 - 恰恰是 break 会阻止其执行的东西。钩子会在检测点、响应被执行 之前 同步触发 - 因此它调用的同步函数会先完成,然后 break 再停止 VM。

请把响应定义为您的 vmDefenseHook。由于 debugger 检测在运行时触发 - 即在您的程序已加载并定义了钩子之后 - 该钩子可以作为您混淆源码的一部分,并与包的其余部分一起被字节码化。基于 signal.category 分支处理,让每种情况都得到正确的响应,保持工作同步,然后让响应自行运行:

JavaScript

JavaScript

这适用于在您的应用运行期间触发的检测 - 请参阅下文的字节码化钩子何时有效。

请记住以下几点:

  • 只有同步工作才保证先于响应完成。 响应会在钩子返回后紧接着的那条语句上运行。立即移交的即发即忘调用没有问题(navigator.sendBeacon、同步的 DOM 与 canvas 编辑);而您安排到之后执行的工作 - setTimeout、promise 的后续处理、await - 则不行,任何需要更多 VM 字节码的操作都不会运行,因为那正是 break 所停止的。
  • 钩子在响应之前运行;它并不取代响应。 它的返回值会被忽略,也无法取消、延迟或改变响应的行为。请用它在中断之前采取行动,而不是否决中断 - 要改变响应本身,请使用 vmDefenseReaction(方案 2)。

字节码化钩子何时有效

像这样把钩子放在混淆包内部之所以可行,只是因为 debugger 检测在运行时触发。VM 会在自身仍然存活时触发 vmDefenseHook,即在您的程序已加载并定义了钩子之后,因此字节码化的钩子会先被解码并运行,然后才是 break。这正是保护钩子自身源码的原理。

对于在 启动阶段 触发的检测 - automation、sandbox、domain,或页面加载时就已打开的调试器 - 它 不会 生效,因为那时字节码化的钩子还没有定义,防御找不到可调用的函数。对于这些情况,请改为把钩子注册为宿主页面中的普通全局变量,做法与方案 1 相同。拿不准时,宿主页面的普通全局变量能覆盖每一个到达钩子的检测;字节码化只是为钩子自身的源码增加保护,且仅限于运行时检测。

从遥测到强制执行

可见性与强制执行不必同时上线。分两个阶段部署这些防御:先发布一个只上报的构建,然后 - 一旦遥测看起来干净 - 再发布一个会响应的。

步骤 1 - 发布一个仅观察的构建

启用您计划使用的每一项防御,让 vmDefenseHook 指向您的端点,并关闭所有响应。每个检测器仍会运行,并把每次命中上报到您的后端,但不会破坏任何东西:

JavaScript

步骤 2 - 审阅收集到的信号

在该构建见过真实流量之后,查找由合法使用触发的检测。最常见的两种:

  • 来自您自己的端到端测试或可用性监控的 automation 命中 - 请构建那些不带防御的产物,而不是在生产中容忍该类别。
  • 来自某个您忘记加入 vmDomainLock 允许列表的预发或预览主机的 domain 命中 - 把该主机加进去。

优先修复根因,而不是弱化响应:每一个保持在 none 的类别,都是攻击者可以放心忽略的检测器。

步骤 3 - 开启响应

移除 default: 'none' 覆盖,让内置的按类别响应生效;那一行就是全部改动。如果某个类别持续产生您无法消除的误报,就只把该类别保持在 none(例如 vmDefenseReaction: { automation: 'none' }),并对其余的强制执行。

强制执行开启后请继续保留 vmDefenseHook - 无论响应如何,钩子都会触发,因此在防御运作的同时,您仍能保持对谁在探测您的包的可见性。