跳到正文
← 返回博客
LEARNING / 学习

补环境脚本为什么会越补越错

消除运行异常不等于复现真实行为;每一个浏览器环境桩都应该有来源、影响范围和删除实验。

补环境脚本经常从几行看起来无害的代码开始:

globalThis.window = globalThis
globalThis.navigator = { userAgent: "..." }
globalThis.document = {}

SDK 缺什么,就补什么。报错逐个消失,脚本也越来越像浏览器。但运行结果如果仍然不对,新增的对象会反过来成为新的变量:究竟是算法没有还原,还是某个伪造环境让代码走进了另一条分支?

这类脚本越补越错,通常不是因为对象数量不够,而是因为“消除异常”和“复现真实行为”被当成了同一件事。

环境读取本身就是算法输入

一个 SDK 访问 navigator,不代表它只需要一个不会报错的字符串。它可能检查属性描述符、原型链、字段之间的一致性,也可能在模块加载时把环境值混入后续计算。Date.now()、随机数、屏幕尺寸和语言列表还会引入不可重复输入。

如果遇到访问就返回空对象,代码可能不再抛错,却会走进真实浏览器从未经过的兼容分支。更隐蔽的情况是,某个 stub 让输出“更接近”样本,于是被保留下来,但没有证据说明它真的参与目标算法。

因此,每一个环境桩都应该被视为一项待解释的依赖,而不是临时补丁。

先做访问账本,再写模拟对象

已有逆向实践使用过隔离运行环境、opcode trace 和 WASM 参数还原。这个方法可以继续延伸成一份环境访问账本:

key             被读取的对象或属性
first_read_at   首次访问位置
read_shape      get / call / construct / enumerate
real_value      真实环境中的脱敏形态
stub_value      当前模拟值
reason          为什么必须存在
effect          改变它会影响哪一层结果

账本的目的不是把浏览器抄一遍,而是让每项模拟都能回答“来源在哪里、为什么这样补、删除后什么会变化”。

采集阶段可以只记录访问,不立即猜值。对于 JavaScript,可以在隔离环境外围增加受控代理,记录属性读取和调用;对于 WASM imports,则记录实际导入函数与参数。这里的代理和日志方案属于可执行的设计推演,不代表现有工具已经按这一形式实现。

用删除实验控制环境膨胀

环境桩一旦增加,就应该接受反向验证。

假设当前脚本有二十项模拟对象,可以逐项删除或替换为哨兵值,观察关键中间值、最终字节和控制流 trace 是否变化。若删除后结果完全不变,这项桩可能只是为旧路径服务;若输出变化但无法解释,需要继续定位它进入了哪个计算或分支。

这种做法接近 delta debugging:不是继续加对象直到成功,而是从可运行集合中反复移除,寻找仍能解释结果的最小闭包。

最小闭包并不等于代码最短。它要求每个保留项都有证据,每个外部输入都有稳定来源,非确定性值可以固定或显式注入。最终接口可以把环境参数与算法参数分开:

compute(input, algorithmConfig, environmentSnapshot)

这是建议的接口形态,不是现有项目已经采用的签名。它的价值是让样本变化时,可以先判断是哪一组输入发生漂移。

一个容易误判的成功

假设离线实现已经与固定样本达到字节级一致。此时继续修改算法,只因为真实接口没有返回目标数据,通常会把已经收敛的层重新打开。

字节一致证明的是算法输入输出在这些样本上对齐。真实数据仍可能受会话、请求头、TLS / H2 或行为环境影响。正确做法是冻结当前算法基线,另建传输和业务实验,而不是让补环境脚本同时承担所有解释。

但“冻结”也不是永久正确。新样本出现差异、SDK 版本变化或中间 trace 不再一致时,算法层应该重新进入调查。冻结是一项带证据和版本的状态,不是一句“以后不用看”。

最小闭包也有反例

有些保护逻辑刻意依赖大量浏览器行为,或者把执行完整性、渲染状态和异步事件一起纳入判断。此时强行做纯算最小闭包,维护成本可能高于真实浏览器自动化。某些 SDK 还会检查原生函数特征、DOM 生命周期或跨上下文对象,简单 stub 很难形成一致环境。

遇到这种情况,选择真实浏览器、CDP 或受控页面执行并不代表分析失败。关键是明确交付目标:需要一个可解释的脱机算法,还是需要一条稳定、可观测的请求链路。两种目标对应不同边界。

当前还不知道什么

现有公开材料能够证明:相关实践使用过隔离运行环境、虚拟机 trace、WASM 参数还原与真实样本字节对齐,也把会话和传输层从算法层分开验证。但没有公开环境访问清单、stub 数量、删除实验结果或 SDK 更新后的漂移样本。

因此不能声称最小闭包已经覆盖所有版本,也不能把固定样本的一致性扩大成对服务端策略的解释。

下一步可以为每个样本保存环境快照摘要、访问 trace 和算法中间值。新增或删除环境桩时,自动比较三类差异:访问集合是否变化、关键分支是否变化、最终字节是否变化。再用独立的传输层 A/B 检查业务结果。

补环境的目标不应是让错误消息消失。它应当让每一个被模拟的对象,都成为可以解释、可以删除、也可以回归的工程依赖。