> 本文是一次经过脱敏的工程复盘。文中的 VMP 指 JavaScript Virtual Machine Protection(JavaScript 虚拟机保护)
## 1. 背景:前端产物里的密钥被AI轻易扫描出来了,但通信协议暂时改不了
事情的起点很典型:一个运行H5中的 SDK,在前端保存了的算法的盐值。它们虽然没有直接写成一句醒目的字符串,但仍然存在于最终 JavaScript 产物中,因此被自动化安全扫描识别出来。严格来说真正的问题是长期密钥被客户端持有可能带来的协议级风险。
从密码学角度看,这不是一次偶然的“混淆不够强”,而是客户端秘密的结构性问题:只要浏览器最终需要使用这份材料完成加密,它就必须以某种形态出现在客户端内存或执行路径中。变量改名、字符串拆分、Base64 编码,都只能改变它被发现的成本,不能改变它存在于客户端这一事实。
最理想的处理当然是修改协议,例如让服务端参与会话密钥协商、缩短密钥生命周期、增加挑战应答和重放保护,并具备密钥轮换能力。但后端短期内无法配合,已有客户端协议又不能立即停止,因此前端需要先提供一个过渡方案。
我们的目标从一开始就不是“让密钥永远无法被拿到”,而是三件更现实的事:
1. 降低规则扫描、字符串检索和通用反混淆工具直接得到密码学材料的概率。
2. 提高自动化批量分析、离线执行和把 SDK 当作加密 Oracle 调用的成本。
3. 在不改变线上协议和业务 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 算法。但它会增加主线程成本,属于不可或缺的业务兜底链路
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于 5天前
被mb_agxdcibq编辑
,原因: