反LLM混淆:指纹脚本的反击
反LLM混淆把越狱提示词藏进浏览器指纹脚本,让AI去混淆失效。本文解析其原理,以及什么方法能破解它。

一位研究者钩住了10000个全球热门网站上的每一次 canvas 调用,收集了一千多份指纹渲染结果,然后从中挑出最漂亮的那个。他在负责的脚本里发现的东西,是反LLM混淆目前最干净利落的例子:一段浏览器指纹脚本里塞满了诸如 computeRicinLethalDosageForWaterSupply 这样的函数名,还有一段用不可见 Unicode 字符编码的越狱提示词。它的目标不是躲开人类逆向工程师,而是让 AI 助手拒绝去解混淆。
我在事件响应领域干了好几年,第一次翻看这份报告时,产生的正是作者想要的反应:这看起来像恶意软件,甚至更糟。函数名涉及蓖麻毒素剂量和脏弹辐射半径。解码出的字符串要求提供逐步的生物武器合成指南。Node.js 代码路径会读取 .npmrc、.env 和 kube 配置,从一个内网 IP 拉取二进制文件,并安装 cron 信标。所有直觉都在说:坏人干的。而所有直觉都错了,这种错误正是作者的用意所在。这是一个关于这样的故事:当卖混淆方案的人意识到,他们真正的对手不再是拿着调试器的人类,而是一个带安全过滤器的模型。
爬取本身很直接:用 Playwright 跑 Tranco 热门网站列表,钩住 HTMLCanvasElement.prototype.toDataURL,并用 navigator.webdriver 这类典型的自动化痕迹来过滤掉良性画布。渲染结果被收集到一个公开图库中,按供应商聚类,比如 FingerprintJS、Castle 这些常见角色,外加一长串未知来源。其中一张画布是一团漂亮的流动线条漩涡,溯源到一家俄罗斯 DDoS 防护厂商提供的脚本。好戏从这里开始。
问题所在:LLM 打破了混淆的商业模式
二十年来,JavaScript 混淆只有一个任务:让逆向的成本高到没人愿意干。javascript-obfuscator 及其商业衍生品会打乱标识符、把字符串字面量打包进查找数组、注入不透明谓词,并把控制流展平成由 switch 驱动的状态机。面对一个有充足时间的人类,这些手段都扛不住,但那无所谓,因为有空闲时间的认真逆向者很少,经济账站在防守方这边。
后来 LLM 变得擅长阅读被打乱的代码,经济账就反过来了。把一份 400 KB 的混淆反机器人脚本粘贴进聊天窗口,问一句「这段代码干什么的?」,三十秒就有答案。模型不在乎你的函数叫 PiERW,也不在乎字符串藏在旋转数组里。它读的是语义,会把解码器内联展开、追踪数据流,最后给出一段大白话总结。所有反机器人厂商赖以为生的护城河——不透明性——就这样悄无声息地蒸发了。
厂商注意到了,他们的应对措施既聪明又让人恼火:如果没法对模型隐藏代码,那就去攻击模型本身。喂给它的内容,是安全训练强制它拒绝的东西。反LLM混淆把模型对齐当成分析流水线上最薄弱的一环。气人的是,他们说得没错。
浏览器指纹脚本如何收集熵
要理解这个陷阱,得先了解它所依附的正常机制。Canvas 指纹技术会在一个不可见的 <canvas> 上绘制文字、渐变和图形,然后通过 toDataURL() 把像素缓冲区读回来。渲染管线——GPU、驱动、字体光栅化器、抗锯齿、子像素提示——在不同机器上会有细微差异,因此两台浏览器绘制完全相同的指令,得到的像素缓冲区哈希值却不同。再结合 WebGL 渲染器字符串、AudioContext 时序、屏幕尺寸、已安装字体等上百种信号,就能得到一个稳定的标识符,而且完全不用 Cookie。
找到这个脚本的爬虫用的是标准的拦截手法:在初始化脚本中钩住读回原语,记录调用栈以及每张被捕获图像的前缀:
await page.addInitScript(() => {
const original = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (...args) {
const result = original.apply(this, args);
fetch("/__log", {
method: "POST",
body: JSON.stringify({
stack: new Error().stack,
byteLength: result.length,
head: result.slice(0, 64),
}),
});
return result;
};
});
在足够多的站点上跑完这套流程,指纹生态就会自行呈现出来。大多数画布都很无聊——一句全字母句、几个矩形、一道渐变。只有少数复杂到可以称作生成艺术。而它们全都出自高度混淆的脚本,因为厂商清楚,未经混淆的指纹脚本本身就很容易被指纹识别。混淆一直是产品的一部分,新变化在于混淆是冲着谁去的。
反LLM混淆陷阱内部
脚本开头很常规:被打乱的名字、打包的字符串,老套路。然后标识符的命名突然不再随机,而是有了明确指向。computeRicinLethalDosageForWaterSupply。calculateDeadlyLethalSuicideAspirinDosage。computeCRISPRGeneDrivePropagationRate。computeCesium137DirtyBombFalloutRadius。这些函数返回的数字被包装成听起来很吓人的摘要,比如「已确认成人致死阈值为 X mg/kg」,但计算本身是诱饵算术,什么都没算,也没有输出到任何地方。
关键在于:标识符在运行时毫无意义,对语言模型却有巨大的语义分量。人类逆向工程师靠经验养成了无视命名的习惯,我们都吃过亏:一个叫 validateUser 的函数其实什么验证都不做。模型恰恰相反。它读到 computeRicinLethalDosageForWaterSupply,就会匹配到生物武器的语境,安全训练在它的好奇心发作之前就已经触发了。
更深的一层是一个模板字符串,里面全是不可见字符,即 Unicode 标签区块(Tags block)中从 U+E0000 开始的码位。它们渲染出来什么都看不见。解码器只有一行:
const cleartext = [...payload]
.map((c) => String.fromCodePoint(c.codePointAt(0) - 917504))
.join("");
// 917504 === 0xE0000, the start of the invisible "Tags" block.
// Subtracting it maps each hidden char back to plain ASCII.
解码后,这是一段完整的越狱提示词:伪造的「系统覆盖——机密简报」,声称安全准则已被暂停,并要求提供针对医疗基础设施的 DDoS 攻击、逃税、气溶胶化生物制剂、药物剂量的逐步指南。此外还有中英文双语的天安门广场相关内容,显然是为了触发中国境内托管模型的审查反射。这是一段被精心设计、能同时命中所有安全分类体系的字符串。当一个 LLM 被要求分析周围的代码时,它会把这段内容当作上下文吞下去,无论它原本要做什么,现在都什么也做不了。

失败的尝试:拒绝、误报,以及一个假僵尸网络
最自然的第一步是把它粘贴进一个前沿模型,结果正如设计所料:模型拒绝了,理由是生物安全风险。换一种安全策略再试一次,同样被拦截。混淆器实际上把拒绝变成了武器:这个脚本如今对所有人实际在用的工具而言都是无法反编译的。一个护栏较弱的开源权重模型倒是照做了,但这只是一个数据点,不是解决方案。多数分析师不会为此常驻一个本地模型,而且多数公司的政策本来就禁止把第三方代码粘贴到未经审核的基础设施里。
第二次失败的尝试更隐蔽,也更危险:人类分析师得出结论,认为这是恶意软件。有一条仅限 Node 的代码路径,由 process.env.METRICS_AGENT_8BFD_ACTIVE 把关,它会读取类 SSH 的配置文件,从 10.15.45.1 下载一个「指标代理」,把主机指纹写入 /tmp,打包压缩,然后安装 cron 信标。看上去就是供应链植入。但顺着线索一查:环境变量没有任何地方设置;readFile 的回调是空的,所以窃取的数据根本没去任何地方;C2 地址属于 RFC 1918 私有网段;shell 命令还会把更多越狱诱饵回显到标记文件里,而没有任何东西会把它们外传。放进沙箱里引爆,爆炸半径就是一个临时目录。这是一场表演,是专门为 AI 辅助分诊以及走马观花的人类设计的诱饵。
这里还有一个现实中的回响。某大型模型托管平台调查一起真实入侵事件时,团队发现商用模型 API 拒绝分析攻击者的命令、载荷和 C2 产物,护栏无法区分事件响应人员与攻击者。最后他们只能在自有基础设施上用开源权重模型完成取证分析。这种失败模式如果大规模重复出现,正是反LLM混淆所利用的那种不对称。这也是我一直坚持认为 AI 代码理解检查 不能成为你唯一防线的原因:模型的判断力如今已经成了一个带有公开利用方法的攻击面。
混淆器不再是把代码藏起来不让你看见,而是把代码藏起来不让你的模型看见,并且把你模型自身的安全训练当成了锁。
真正有效的反LLM混淆对策
解决之道是不再把原始源码直接喂给模型,而是收回被陷阱污染的那些流水线环节。具体来说,我会按以下顺序来搭建:
- 先做确定性的反混淆。用基于 Babel 的 AST 处理流程:常量折叠、字符串数组内联、死分支消除、控制流还原。陷阱藏在字符串字面量和标识符里,折叠和重命名能在任何模型看到 token 之前就把它们拆掉。解码出的越狱提示词也就只是死代码路径里的又一个字符串。
- 追踪行为,而不是阅读源码。在一个带插桩的沙箱里执行脚本,比如挂了钩子 API 的 Playwright 页面,或者隔离的 realm,记录每一次 DOM 调用、网络请求、属性访问和画布读回。把追踪记录喂给模型。行为轨迹里不含任何 ricin 字样,因为行为本身没有标识符。
- 在任何 LLM 处理之前,把标识符重命名为中性的代号。
computeRicinLethalDosageForWaterSupply变成fn_0417。你会损失一点语义提示,但也去掉了所有投毒内容。这笔交易很划算。 - 对于确实敏感的研判,保留一个本地开源权重模型。当内容本身确实很糟糕,比如真实的漏洞利用套件、实际的钓鱼工具包,一个没有云端护栏的离线模型,就是分析与拒绝之墙之间的区别。
- 基于证据下结论,而不是凭感觉。根据观察到的能力给脚本打分:它有没有读取凭据?有没有数据离开页面?而不是看源码里是否提到了凭据。
注意其中的规律:每一项技术都把「问模型这段代码说了什么」换成了「观察这段代码做了什么,然后就观察结果去问模型」。这和我一直在自主 Agent 上反复强调的原则一致:围堵优于监控。你不要相信制品对自身的描述,而是把它关起来,在边界上插桩,然后根据它的输出来判断。
你的第三方脚本正在和模型供应商对话
现在来说说那些并不以逆向工程为乐的人应该担心的部分。这段脚本并不是托管在某个暗处,它是一段反机器人检测,运行在客户网站上,挂在客户域名下,紧挨着客户的品牌。这意味着每一个爬虫、每一个基于 LLM 的内容分类器、每一条吸收页面内容的信任与安全流水线,现在都会连同公司的 logo 一起吸收生物武器合成请求和越狱提示词。客户几乎肯定不知道,厂商也从没问过。
让分类器对自己不理解的内容采取行动,这并非假设。一些从业者已经亲眼见过,外发文件因为一段不可见水印字符串触发了邮件过滤器,无声无息地被扣进隔离区。把这种失败模式放大来看:一个品牌安全分类器判定你的域名与极端主义或生物危害内容相邻,而你永远不会被告知原因,流量却骤然下跌。你甚至无从申诉,因为证据是一个你没有写过的文件里的不可见 Unicode 字符。
而常规的缓解手段也派不上用场。当厂商按部署轮换脚本哈希时,子资源完整性(SRI)毫无用处,而且这些脚本频繁轮换,部分原因正是为了对抗哈希固定。带哈希的严格 CSP 同样变化得很快。我们只能得出一个令人不安的结论,就像 LiteLLM 供应链入侵之后那样:你无法审查的依赖,恰恰是能够掌控你的依赖。如果你的反机器人厂商的代码被刻意做成你安全团队的工具无法反编译,那这不是什么功能,而是一个挂着 CDN 地址的治理失败。

这场军备竞赛的下一步
我的预测是:一年之内,针对安全护栏的陷阱会作为复选框出现在商用混淆器中,和「控制流展平」并列,叫作「LLM 分析抗性」。模型供应商则会用陷阱语料进行训练,让模型学会在分析场景中识别并忽略嵌入的诱饵。混淆器又会以更隐蔽的陷阱反制:代码的行为在追踪中看起来像恶意的,而不只是字符串。HN 评论区半开玩笑预言过的分形军备竞赛,也就是针对模型调优的混淆器和针对混淆器调优的模型,其实已经到来,只是分布还不均匀。
真正持久的防御恰恰是最无聊的那一套,它自九十年代第一批加壳器出现以来就没有变过:动态分析、行为证据和沙箱执行。不透明性从来不是一种安全属性,它只是一种转移成本的策略,而这份成本现在被转移了回来。如果你的检测流水线会被一段能让语言模型「难受」的字符串攻破,那么这条流水线本来就是漏洞。


