文档
/
实用方案
/

VM 防御遥测与响应

VM 防御遥测与响应

Pro
v7.1.0+

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

问题

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

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

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

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

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

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

<script>
    // In your page, BEFORE the obfuscated script:
    window.__vmDetection = function (signal) {
        navigator.sendBeacon('/api/vm-defense', JSON.stringify(signal));
    };
</script>
<script src="/app.obfuscated.js"></script>

步骤 2 - 让 vmDefenseHook 指向它

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

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    // the hook alone enables nothing - a defense must be on for detectors to run:
    vmSelfDefending: true,
    vmDebugProtection: true,
    vmDefenseHook: { name: '__vmDetection' }
});

裸字符串形式(vmDefenseHook: '__vmDetection')仍作为 { name: '__vmDetection' } 的简写被接受,但已 弃用 —— 请优先使用对象形式。

在控制台中,一旦启用了至少一项防御(vmSelfDefendingvmDebugProtectionvmDomainLock),VM 选项面板中就会出现 VM Defense Hook 字段。

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

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

  • source - 具体的检测器:headlessnodeagentdomaindebuggersandboxnativeHooktimingintegrity。自 v7.4.0 起,原先的 envinspector 检测器改在 source: 'debugger' 下上报。
  • category - automationdebuggersandboxdomaintamperintegritynode 来源在 category: 'debugger' 下上报(v7.4.0+)。
  • scorethreshold - 检测得分及其越过的阈值

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

app.post('/api/vm-defense', express.text({ type: '*/*' }), (req, res) => {
    // a signal: { source: 'headless', category: 'automation', score: 7, threshold: 4 }
    const signals = [].concat(JSON.parse(req.body));
    for (const signal of signals) {
        console.warn('vm-defense', { ...signal, ip: req.ip, ua: req.get('user-agent') });
    }
    res.sendStatus(204);
});

重命名信号字段(aliases) v7.4.0+

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

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

vmDefenseHook: {
    name: '__vmDetection',
    aliases: {
        source:    { key: 'a8Qm', values: { headless: 'xP4m9Q' } },
        category:  { key: 'p3Tx', values: { automation: 'bQ7s1M' } },
        score:     { key: 's1' },
        threshold: { key: 't1' }
    }
    // the callback now receives e.g. { a8Qm: 'xP4m9Q', p3Tx: 'bQ7s1M', s1: <score>, t1: <threshold> }
}

在控制台中,Signal aliases 一节位于 VM Defense Hook 字段之下。

方案 2 - 调整默认响应

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

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

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

  • break - 立即中断
  • decoy - 在被毒化的状态上继续运行,静默地产生错误结果
  • none - 在本地什么都不做(仅遥测)

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

// built-in defaults
vmDefenseReaction: {
    automation: 'break',
    debugger: 'decoy',
    sandbox: 'decoy',
    domain: 'break',
    tamper: 'break',
    integrity: 'break'
}

default 会触及 每一个 类别,包括那些构造上必然正确的类别(integritytamper),因此 { default: 'none' } 是一个真正不破坏、仅遥测的构建:

vmDefenseReaction: { default: 'none' } // never break - pair with vmDefenseHook
vmDefenseReaction: { automation: 'none' } // tolerate automation FPs; the rest keep their defaults (a bad domain still breaks)

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

从遥测到强制执行

您不必在第一天就在可见性与强制执行之间二选一。分两个构建来推行这些防御:先做一个只上报的,然后 —— 一旦遥测看起来干净 —— 再做一个会响应的。

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

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

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    vmSelfDefending: true,
    vmDebugProtection: true,
    vmDomainLock: ['example.com'],
    vmDefenseHook: '__vmDetection',
    vmDefenseReaction: { default: 'none' } // observe only
});

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

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

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

优先修复根因,而不是弱化响应:每一个保持在 none 的类别,都是攻击者不再需要担心的检测器。

步骤 3 - 开启响应

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