VM 防御遥测与响应
使用 vmDefenseHook 将 VM 防御检测结果上报到您的后端,并用 vmDefenseReaction 调整每个检测类别的响应方式 —— 从完全不破坏、仅遥测的构建,到对被窃取的包进行强硬中断。
问题
VM 防御 —— vmSelfDefending、vmDebugProtection 与 vmDomainLock —— 是就地生效的:当检测到调试器、自动化工具、被篡改的环境或未授权的域名时,受保护的代码会中断,或静默地毒化自身的结果。这能阻止攻击者,但默认情况下 您对此一无所知。您无从得知您的包被探测的频率、是哪个检测器触发的,也不知道某项防御是否正在破坏合法用户的使用。
自 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' } 的简写被接受,但已 弃用 —— 请优先使用对象形式。
在控制台中,一旦启用了至少一项防御(vmSelfDefending、vmDebugProtection 或 vmDomainLock),VM 选项面板中就会出现 VM Defense Hook 字段。
步骤 3 - 在您的后端接收信号
每次检测都会用单个 signal 对象调用钩子:
source- 具体的检测器:headless、node、agent、domain、debugger、sandbox、nativeHook、timing或integrity。自 v7.4.0 起,原先的env和inspector检测器改在source: 'debugger'下上报。category-automation、debugger、sandbox、domain、tamper或integrity。node来源在category: 'debugger'下上报(v7.4.0+)。score、threshold- 检测得分及其越过的阈值
一个最小的接收端点(此处以 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(回调收到的属性名);字符串名称字段 source 和 category 还接受一个 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 配置每个检测 类别 如何响应。它不会启用任何东西 —— 防御本身由 vmSelfDefending、vmDebugProtection 和 vmDomainLock 开启;此选项只选择已启用的防御如何响应。类别是控制的单位:一个类别中的每个检测器都会执行该类别的响应,而为一个其选项处于关闭状态的类别所设置的响应,根本不会有任何效果。
| 类别 | 由谁启用 | 何时响应 |
|---|---|---|
automation | vmSelfDefending 或 vmDebugProtection | 代码正被软件而非真人驱动:无头或自动化浏览器、抓取 / 测试框架,或正在逐步执行页面的 AI 编码 agent。 |
debugger | vmDebugProtection 或 vmSelfDefending | 有人打开了调试器或浏览器的开发者工具检查器,正在单步执行运行中的代码以理解它。 |
sandbox | vmDebugProtection | 代码根本不在真实浏览器中运行 —— 它已被提取到一个被模拟或脚本化的 JavaScript 环境中,以便离线执行和研究。 |
domain | vmDomainLock | 代码正运行在您未授权的站点上:一个不在您 vmDomainLock 允许列表中的主机(例如,您的包被复制到了别人的域名上)。 |
tamper | vmSelfDefending | VM 周围的 JavaScript 环境已被修改以监视或劫持它,例如原生浏览器内置对象被替换成了被插桩的版本。 |
integrity | vmSelfDefending | 受保护的包自您生成以来,其自身代码已被编辑或打过补丁。 |
键是这六个类别名称,或 default(针对未指定类别的回退)。值为:
break- 立即中断decoy- 在被毒化的状态上继续运行,静默地产生错误结果none- 在本地什么都不做(仅遥测)
您未设置的类别会回退到内置默认值:
// built-in defaults
vmDefenseReaction: {
automation: 'break',
debugger: 'decoy',
sandbox: 'decoy',
domain: 'break',
tamper: 'break',
integrity: 'break'
}
default 会触及 每一个 类别,包括那些构造上必然正确的类别(integrity、tamper),因此 { 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' }),并对其余的强制执行。
