本文是一次经过脱敏的工程复盘。文中的 VMP 指 JavaScript Virtual Machine Protection(JavaScript 虚拟机保护)
1. 背景:前端产物里的密钥被AI轻易扫描出来了,但通信协议暂时改不了
事情的起点很典型:一个运行H5中的 SDK,在前端保存了的算法的盐值。它们虽然没有直接写成一句醒目的字符串,但仍然存在于最终 JavaScript 产物中,因此被自动化安全扫描识别出来。严格来说真正的问题是长期密钥被客户端持有可能带来的协议级风险。
从密码学角度看,这不是一次偶然的"混淆不够强",而是客户端秘密的结构性问题:只要浏览器最终需要使用这份材料完成加密,它就必须以某种形态出现在客户端内存或执行路径中。变量改名、字符串拆分、Base64 编码,都只能改变它被发现的成本,不能改变它存在于客户端这一事实。
最理想的处理当然是修改协议,例如让服务端参与会话密钥协商、缩短密钥生命周期、增加挑战应答和重放保护,并具备密钥轮换能力。但后端短期内无法配合,已有客户端协议又不能立即停止,因此前端需要先提供一个过渡方案。
我们的目标从一开始就不是"让密钥永远无法被拿到",而是三件更现实的事:
-
降低规则扫描、字符串检索和通用反混淆工具直接得到密码学材料的概率。
-
提高自动化批量分析、离线执行和把 SDK 当作加密 Oracle 调用的成本。
-
在不改变线上协议和业务 API 的前提下,把移动端体积、首屏耗时和请求耗时控制在可接受范围。
这一定义很重要。它把 VMP 放在了正确的位置:VMP 是临时的纵深防御和风险缓解措施,不是密码学意义上的根治方案。
这个方案的威胁模型也有明确边界:主要应对低成本静态扫描、通用反混淆、普通离线执行和批量滥用以及中低级的动态分析;不承诺可以抵抗自编译浏览器引擎视为前端可以彻底解决的问题。
2. 第一轮头脑风暴:哪些方案看起来有效,实际并不够
2.1 只做字符串混淆和常量拆分
最容易想到的方案,是把算法盐值拆成多个数组,在运行时异或、重排后再组合。它确实能躲过一部分只匹配明文和特征的扫描器,但对动态分析帮助有限:在 算法 调用前设置断点或 Hook,仍然可以看到重组后的材料。
因此,材料拆分可以作为一层防护,但不能单独承担安全目标。
2.2 把整个 SDK 都塞进 VM
全量虚拟化的安全边界最简单,却很快遇到了移动端现实:业务方法生成、URL 和 Body 组装、XHR、回调、JSON 处理以及大量分支都会变成 VM 字节码。解释执行带来的函数分发、虚拟栈、类型转换和对象桥接成本,会落在每一次请求上;同时包体显著膨胀,老旧机器的兼容性风险也随之增加。
更关键的是,绝大多数业务代码并不包含秘密。把它们送进 VM,付出了体积和性能,却没有得到对等的安全收益。
2.3 把 算法算法等其他算法全部留在 VM 中
这是第二个看似自然的方案。纯 JavaScript 算法 在原生 JavaScript 中已经不算便宜,进入 VM 后,每一轮查表、异或、移位和轮密钥计算又会被解释一层。在低端移动设备上,多个接口首屏并发时,这条路径可能增加主线程长任务风险,需要用目标机器的性能轨迹验证。
-
如果摘要只处理已经可见的请求字段,不依赖秘密,把它放进 VM 的收益很低,反而增加每次请求的跨 VM 调用和解释成本。
-
如果摘要中包含不应直接暴露的业务盐值,那么材料和计算仍应留在安全核心中。
因此,"算法等其他算法是否进 VM"不能按算法名称一刀切,而要看它处理的数据是否构成真正的安全边界。
2.4 直接使用浏览器 WebCrypto
在本项目的实践中,WebCrypto 的 算法 路径明显快于 VM 中的纯 JavaScript 实现;浏览器原生实现也具备使用系统级优化的条件,因此它成为解决性能问题的重要工具。但它同时形成了一个宿主边界:密码学材料和待处理数据必须交给浏览器 API。具备运行环境控制权的分析者,仍可能在这个边界或更低层观察数据。
缓存原生引用、做必要的一致性检查、限制 CryptoKey 不可导出,并在导入后清理原始字节,可以提高普通运行期替换的成本,却无法从根本上解决"调用外部 API 时数据必须跨越边界"的问题。
所以 WebCrypto 最终被定位为性能快路径,而不是新的安全根基。
2.5 把所有反调试选项全部打开
强保护配置通常会加入调试器检测、自动化检测、沙箱检测、宿主内置函数防篡改、代码完整性校验、诱饵执行和持续时序检查。它们对分析确实有帮助,但在宿主页面中也可能把 vConsole、监控 SDK、埋点、Polyfill 或厂商的实现差异误判成攻击。
在移动端,持续反调试还会带来额外计时、环境探测和控制流成本。安全开关不是越多越好;没有灰度和真实宿主验证的"全开",很可能先攻击到自己的业务。
3. 最终边界:只把"值得保护的部分"放进 VM
最后采用的是"最小安全核心"结构。
| 区域 | 主要职责 | 为什么这样放 |
|---|---|---|
| VM 外业务 | 公开业务 API、无秘密摘要 | 代码量大、调用频繁,但不掌握持久秘密;留在原生 JS 中更小、更快、更容易兼容和回归 |
| VM 内安全核心 | 算法材料的组织与选择、敏感请求/响应处理、含秘密盐值的摘要、轻量完整性检查 | 一旦静态暴露就直接失去保护价值,值得承担虚拟化成本 |
| VM 与原生密码学的桥 | 原生引用捕获、CryptoKey 导入与缓存、算法快路径和兼容降级策略 | 控制逻辑由 VM 保护,但算法原语实际由 VM 外的浏览器实现执行;这换来性能,也形成可观测边界 |
| VM 内短期状态 | 保存完成响应处理所需的最小关联状态并及时失效 | 外壳不直接获得低层解密能力,且并发响应不依赖一份容易串用的共享状态 |
请求链路可以概括为:
text
业务参数
↓
vm外完成协议编排和非敏感数据组装
↓(一次高层调用)
VM 安全核心
├─ 选择并重组密码学材料
├─ 完成敏感协议计算
├─ 按环境选择原生快路径或兼容路径
└─ 返回最小化结果与短期关联状态
↓
vm外发送 XHR
↓
收到待处理响应
↓
vm外将密文和一次性句柄交回 VM 解密
这里有两个刻意的设计选择。
第一,vm外不能获得低层的"任意明文加密"接口。它只调用一次高层请求入口,输入和输出结构受到业务协议约束。这只能提高低成本离线 Oracle 的门槛;公开高层 API 仍可能在真实浏览器中被批量调用
第二,不强行隐藏所有摘要逻辑。如果某个摘要算法只依赖已经出现在请求中的字段,那么攻击者抓到一条真实请求后本来就能推导它。把它移出 VM 可以减少虚拟机热路径;真正包含秘密盐值的摘要仍留在 VM 内。
4. 性能优化:先减少进入 VM 的次数,再优化算法
基于热路径和实现机制分析,优先遵循的原则不是先微调某个循环,而是先减少虚拟机边界、重复派生和重复分配。
4.1 两次加密合并为一次高层调用
早期实现中,加密令牌和请求体分别进入 VM。即使 WebCrypto 能并发执行,两次调用仍会重复经过环境状态读取、参数编组、Promise 链和 VM 函数入口。
后来把同一请求所需的多项敏感计算合并为一次高层调用。VM 内部仍可并发启动独立计算,但只跨越一次 VM 边界。
4.2 WebCrypto 作为快路径,纯 JS 算法 作为兜底
支持 WebCrypto 且环境检查通过时,安全核心优先调用浏览器原生 算法;如果运行环境从一开始就不支持 WebCrypto,或本 SDK 必须维持端到端同步调用契约,则使用 VM 内的纯 JavaScript 算法 兼容路径。已经明确检测到运行边界异常时,进入的是风险处理蜜罐分支,而不是保证业务等价的普通降级。
WebCrypto 返回 Promise;在本 SDK 要求加密准备与同步 XHR 一起保持同步返回语义时,不能直接等待这条异步链,因此实现选择 JS 算法。但它会增加主线程成本,属于不可或缺的业务兜底链路
4.3 缓存真正昂贵、且可以安全复用的状态
最终缓存了以下内容:
-
每个协议分支的不可导出 CryptoKey,避免每次请求重复导入。
-
避免重复构造。
-
纯 JS 兜底路径中的 算法 key schedule,避免每次重新扩展轮密钥。
-
已解码的字段名和少量拼接材料。
缓存是一种安全与性能交换。轮密钥或 CryptoKey 在内存中停留更久,换来的是减少重复派生和导入。原始字节应在不再需要时尽早清理,但这不等于从设备内存中获得了可证明的安全擦除。两条路径也不是任意时刻都能无缝切换:原生密钥已建立并清理原始材料后
4.4 为常见输入增加快路径
协议字段和大部分 JSON 请求以 ASCII 为主,因此 UTF-8 编解码增加了 ASCII 快路径;数组操作采用预分配或减少中间副本;短生命周期响应句柄采用节流清理,而不是每次请求都扫描整张表。
这些优化单项并不惊艳,但它们发生在每个请求都会经过的路径上,因此成为本轮优先采用的优化方向。
5. 一组有边界的测量结果
下面只保留该项目演进过程中的方向性观察。早期开发基准没有完整保留设备版本、随机种子、多次构建方差和全部计时边界,因此不具备对外发布严谨基准的条件。VMP 产物具有随机性,桌面 Node/Chromium 也不等于低端 Android WebView,这些观察不能用于其他项目的容量规划。
在一轮只调整四项 VM 编码/包装开关的对照中:
-
本地执行初始化出现约一成量级的下降。
-
首次请求准备出现个位数到一成量级的下降。
-
热态请求没有观察到稳定、可复现的明显变化。
-
原始文件体积变化落在低个位数百分比内,可能被随机构建方差覆盖。
样本只能支持一个工程判断:该配置组合值得继续做真机实验,不能把收益归因到其中某一个开关。后续更激进的实验还出现过包体增大、冷请求回退而热态偶尔变快的情况,因此不存在单调的"关得越多越快"。
在留存的代表性构建中,最终可部署 JavaScript 的未压缩体积从百 KB 以内增长到数百 KB,约为原来的三倍量级;即使使用 gzip,传输增量仍处于百 KB 量级。本样本中的 VM 数据和解释器也比普通业务 JavaScript 更难压缩。这里比较的是不同构建范围的最终文件,差额不能全部归因于某个单一 VMP 开关。
源码级桌面回归还验证了并发请求能复用已导入密钥,导入次数符合预期。这是一条功能断言,不是最终 VMP 产物的性能基准。真正发布前仍需要在最低配置设备和真实宿主 App 中分别测量网络下载、解析/编译、执行初始化、P50/P95 请求准备、长任务和首屏请求瀑布。
6. 反调试开关:安全收益、性能成本与误判
不同 VMP 产品的实现差异很大,下面是用 JSVMP 在相同源码上的实验现象。
6.1 调试与沙箱检测:可能是主要体积来源
这一类开关通常负责自动化、无头环境、沙箱和调试器检测,还可能向 VM 分发循环加入时序或环境判断。在我们的一组单次随机化构建中,启用相关能力后,未压缩 VMP 文件出现过百 KB 量级的增量。由于没有多随机种子重复实验,这只能视为候选主要来源,不能当作开关级因果结论。
它对提高普通离线沙箱和自动化 Oracle 的成本有直接价值,但也更容易影响自动化测试;从实现机制看,还可能增加移动端初始化和运行时负担。持续时序检测尤其需要谨慎,必须分别观察真机性能、调试能力和误判,不能根据桌面包体就决定生产策略。
6.2 自保护与完整性:收益取决于威胁模型
自保护类能力主要承担 VM 代码完整性和宿主内置函数篡改检测。在同类单次样本中,它带来过数十 KB 的未压缩体积增量。其安全收益是定性的:如果威胁模型包含修改产物后再次执行,它的优先级较高;如果宿主大量合法包装原生函数,则兼容性压力也更高。
这一能力可以提高修改 VM 后再次执行的门槛。然而移动端页面经常同时加载监控、埋点、调试控制台和 Polyfill,它们可能合法包装原生函数。若对所有宿主异常直接中断,误伤概率不可忽略。具体哪些类别阻断、降级或只记录,应作为不公开的部署配置,通过灰度数据决定。
6.3 中断、诱饵和仅记录不是强弱的简单排序
-
中断策略在识别到风险后尽快停止,路径短、行为清晰,更适合阻断低成本离线 Oracle。
-
诱饵策略继续执行但产生污染结果,可能拖慢分析,却会增加分支、包体和运行成本,也更难排查误判。
-
仅记录策略不改变本地业务结果,适合高误判类别或遥测场景。
实验中,同时对多个类别加入诱饵路径曾带来数十 KB 量级的未压缩体积增量。这是组合配置的一次观察,不能归因于某个单独类别。对移动端生产包而言,诱饵并非免费午餐;最终选择应按威胁模型分配,而不是把所有类别统一设为最激进反应。
6.4 检测能力和命中后的处置是两回事
这次实验中一个容易忽略的问题是:处置策略只是"检测命中后怎么办",不是检测器本身。沙箱、自动化、完整性、宿主篡改和部署环境限制往往分别依赖不同的能力。只写一组看起来很强的处置配置,却没有启用对应检测,最终可能只是心理安慰
6.5 域名和运行环境绑定要谨慎
部署环境绑定可以提高产物被原样搬走运行的成本,但多域名、CDN、灰度、离线包和 App 内置资源都可能与声明不一致。如果无法保证产物经过的每一个上下文都满足约束,就不应贸然开启。
7. VMP 到底防住了什么
改造确实取得了几类实际收益:
-
在指定规则扫描和通用反混淆样本中,未再直接检出完整秘密盐值。
-
常规反混淆主要得到业务外壳,关键逻辑被转换为 VM 字节码和解释器状态,但仍可被长期静态分析。
-
直接把最终包搬进普通 Node 沙箱执行、批量调用加密入口的成本明显提高。
-
修改 VM、插入普通 JavaScript trace 后重新运行,更容易触发完整性保护。
-
对没有专项投入的攻击者,分析成本从"搜索字符串和 Hook 一个函数"提升到"理解 VM、绕过环境检测并建立稳定观测点"。
但它没有改变以下事实:
-
静态文件仍然可以被下载和长期分析。
-
真实用户设备必须能够完成合法加解密,因此最终一定存在可观测的输入、输出或中间状态。
-
VM 与宿主、网络和原生密码学实现之间的运行时边界仍可能被观察。
-
如果分析者控制设备或浏览器运行时,页面 JavaScript 中的反 Hook 无法覆盖所有更低层的观测能力。
-
VMP 不能提供密钥轮换、会话隔离、撤销、重放防护或服务端真实性校验。
9. AI 时代,客户端保护的定位要更诚实
过去,阅读数十万行混淆代码本身就是一道成本墙。现在 AI 可以快速完成函数聚类、常量定位、调用图归纳、差分分析和插桩脚本生成。它还不能一键攻破所有 VM,但已经显著降低了重复劳动,使"尝试更多 Hook 点、自动归纳 trace、比较多个运行环境"变得更便宜。
如果对方执意投入,可以在真实运行环境或更低层运行时中继续观察合法客户端的执行。只要客户端长期持有秘密,最终都可能再次暴露。
因此,VMP 的正确价值是提高门槛、延缓批量滥用、为服务端改造争取时间,而不是让团队误以为问题已经消失。
10. 最终建议:前端止血,服务端治本
如果遇到同类问题,可以按以下顺序处理:
-
先明确威胁模型:要防静态扫描、批量脚本、离线 Oracle,还是高投入定向逆向。
-
只虚拟化最小安全核心,不要把整个业务包送入 VM。
-
按数据敏感性划分算法边界,而不是看到 算法、MD5 就一律塞进 VM。
-
用 WebCrypto 承担性能快路径,但把它视为可被观测的边界,并保留兼容兜底。
-
优先减少 VM 调用次数、重复导入、轮密钥展开和中间对象分配。
-
对反调试选项逐项做体积、冷启动、热路径和误判实验,特别警惕持续 debugger 检测和大规模 decoy。
-
在最低版本、真实宿主和真实网络环境中验收最终产物。
最后一句也是这次改造最重要的结论:
客户端永远不是可信执行环境。VMP 可以让攻击变贵,却不能让客户端密钥变成真正的秘密。真正可靠的方案,最终仍然需要服务端参与。