细说实现:大模型是如何被投毒的

2024-10-19 19:44   广东  

这两天,字节 GPU 投毒事件沸沸扬扬:


和朋友们细聊了这个事儿,也在这里给大家盘一盘。


根据公开信息,推测一下可能的实现方法,或为三个方面:

  • 恶意代码执行

  • 扰乱模型训练

  • 代码隐藏与对抗


下面介绍每个唯维度可能攻击的手法,以及如何进行安全防护


一、恶意代码执行

攻击者通过精心设计的模型文件或数据集,利用底层库的漏洞,引发远程代码执行(RCE),从而获得控制权。在这种攻击中,即便攻击者没有直接的集群 SSH 权限,也可以通过以下几种方式悄无声息地执行恶意代码。


有关 transformer

以 transformers 库为例,已经发现了多起相关的安全漏洞:

  • CVE-2024-3568:该漏洞影响 transformers 库版本低于 4.38.0,主要利用 TFAutoModel 的反序列化过程触发恶意代码执行。

  • CVE-2023-7018:影响版本低于 4.36.0 的 transformers 库,tokenizer 解析存在类似的反序列化漏洞。

from transformers import AutoTokenizer, AutoModelForCausalLMtokenizer = AutoTokenizer.from_pretrained("zpbrent/test")
  • CVE-2023-6730:涉及 RagRetriever.from_pretrained 方法,影响版本同样是低于 4.36.0。

这些漏洞的存在,意味着如果攻击者能够控制模型文件内容,便可通过反序列化行为,在模型加载的瞬间就可以触发恶意代码执行。


trust_remote_code:远程代码执行的“后门”

在 transformers 库中,还有一个较为隐蔽的危险选项:trust_remote_code。这个参数允许从远程服务器加载代码,并直接在本地执行。它的初衷是为了方便开发者快速获取并使用最新的模型和功能,但同时也给了攻击者一个可乘之机。

当 trust_remote_code=True 时,攻击者可以诱导用户加载一个经过篡改的模型,而这个模型会包含恶意代码。一旦加载,恶意代码将在本地执行,可能导致系统被入侵、数据泄露,甚至模型训练过程被完全掌控。

目前大多数开源模型的官方教程都默认开启这个选项,如果仓库权限被控制,后果不堪设想。


恶意数据集

除了模型文件,攻击者还可以通过伪造数据集来达到执行恶意代码的目的。

huggingface 的 datasets 库是目前最流行的数据集加载工具之一,但该库也存在一个潜在的安全风险:如果下载的数据集中包含与数据集同名的 Python 脚本,datasets 库在加载数据时会自动执行该脚本。

换句话说,攻击者可以通过嵌入恶意代码在数据集中,来实现远程代码执行。

官方已明确指出,这一行为是 datasets 的“特性”而非漏洞,但这无疑给了攻击者一个可利用的机会。


二、扰乱模型训练:隐蔽的“暗手”如何影响 AI 模型

在 GPU 模型投毒攻击中,触发恶意代码执行只是开始。更为隐蔽且难以察觉的是攻击者通过精细化手段,直接干扰模型的训练过程。这不仅让模型的最终效果变得不可预测,甚至可能导致模型朝着错误的方向训练,产生严重的商业后果。本文将揭示几种常见的扰乱模型训练的方式,让大家更加警惕这一隐秘的威胁。


修改模型层输出:让模型“产生幻觉”

在深度学习模型的训练过程中,模型的每一层都会输出中间结果,并依次传递到下一层。如果攻击者在这些中间层的输出上做手脚,模型的表现将会变得极为混乱。

一种常见的方式是向模型的某些层(例如 lm_head)加入钩子函数,叠加随机数或噪声。这种“微调”看起来不起眼,但由于大模型的自回归特性,早期层的微小扰动会在模型后续的输出中被逐渐放大,最终导致模型产生“幻觉”,生成错误甚至荒谬的结果。

示例:在输出层添加钩子

在没有钩子之前,模型可能会输出正确的预测结果。然而,加入钩子并叠加随机噪声后,输出结果可能逐步偏离正常轨道:

def hook_fn(module, input, output):    # 叠加噪声或随机数    noise = torch.randn_like(output) * 0.01    return output + noise
# 在lm_head层添加钩子model.lm_head.register_forward_hook(hook_fn)

经过这样的篡改,模型在训练过程中就会逐渐偏离正轨,生成大量错误的预测。特别是在超大规模自回归模型中,这样的扰乱会随着生成过程不断放大,最终导致整个训练数据无效。

加钩子前输出结果:

加钩子后输出结果:


篡改优化器

优化器是模型训练的核心模块,负责根据梯度更新模型参数。如果攻击者能够篡改优化器的行为,模型的训练过程将变得极其不稳定,甚至根本无法收敛。

攻击者可以通过修改优化器的 step 方法,加入延时或随机清空梯度等操作,来伪造正常的训练状态。例如,以下代码通过简单的延时操作拖慢了训练过程,这不仅会增加训练时间,还可能影响训练的整体效果:

optimizer = AdamW(model.parameters(), lr=lr)
def new_step(self, closure=None): super().step(closure) time.sleep(2) # 每次更新时加入延时
optimizer.step = new_step

更严重的是,攻击者可以通过随机化梯度或参数,直接破坏模型的训练进程。例如,清空优化器的梯度或随机篡改参数值,都会使模型训练陷入混乱,无法正常更新参数。


篡改梯度方向

深度学习模型的训练过程依赖于梯度下降法,通过不断调整参数,使模型逐渐收敛到最优解。而梯度的方向正是参数更新的“指南针”,如果这个“指南针”被篡改,模型就会朝着错误的方向前进,训练出的模型可能完全失效。

攻击者可以通过修改梯度的方向来扰乱模型训练。例如,简单地反转梯度方向就可以让模型的参数朝着与预期相反的方向更新,使得模型无法收敛,甚至训练出一个带有后门的模型。

# 在反向传播之后,反转梯度方向for param in model.parameters():    if param.grad is not None:        param.grad = -param.grad  # 反转梯度方向

这种方式虽然隐蔽,但后果却极其严重。模型不仅会训练出错误的结果,甚至可以被设计成带有特定行为的“后门模型”,在特定条件下生成攻击者预期的输出。


三、代码隐藏与对抗

在 GPU 模型投毒的攻击链条中,代码隐藏与对抗是攻击者最隐蔽、最难防范的环节。通过巧妙地隐藏恶意代码,攻击者可以长时间不被察觉,持续影响模型训练,甚至在面对内部调查时,依然能够“全身而退”。本章将揭示攻击者是如何通过篡改库文件、动态加载代码等手法,隐蔽地进行攻击,以及如何对抗这些潜在威胁。


篡改 site-packages 目录:持久化“幽灵攻击”

在 Python 环境下,site-packages 目录存放着项目依赖的第三方库(如 transformers、torch 等)。攻击者可以通过篡改这些常用库的代码,将恶意代码嵌入其中,达到持久化攻击的目的。

由于这些库被频繁调用,攻击者可以在库的初始化代码或关键函数(如模型加载、优化器更新、梯度计算等)中加入恶意代码,每次库被加载时,恶意代码都会悄无声息地执行。这种方式不仅能保证攻击的持续性,还十分隐蔽,因为开发者或运维人员通常不会频繁审查这些已安装的库文件。

示例:篡改初始化代码

攻击者可以在库的初始化代码中插入恶意操作,并伪装成正常的加载过程,难以被察觉。比如,以下代码展示了如何在库加载时执行恶意代码:

# 在库的 __init__.py 文件中加入恶意操作def malicious_code():    # 执行恶意操作,比如发送敏感数据或篡改模型参数    pass
在库初始化时调用恶意函数malicious_code()

这种篡改方式非常隐蔽,因为 site-packages 目录下的文件往往不在日常的代码审查范围内,攻击者可以“潜伏”在系统中,悄悄执行恶意操作。


Python 运行时动态加载:无痕迹篡改核心函数

除了直接篡改 Python 库文件,攻击者还可以通过动态加载的方式,修改模型训练中的关键函数(如 backward()、step()等),以便在不修改显著代码的情况下,悄悄改变模型的训练行为。

这种方法利用 Python 语言的动态特性,攻击者可以在训练框架初始化之前,提前注入代码,改变函数的返回值或行为。例如,攻击者可以修改 backward()函数,使得梯度计算出现偏差,或修改 step()函数,干扰优化器的正常更新。

示例:动态修改函数行为

以下代码展示了如何通过动态加载篡改模型的关键函数:

# 动态篡改 step 函数,延迟或修改参数更新original_step = optimizer.step
def modified_step(*args, **kwargs): # 执行原函数 original_step(*args, **kwargs) # 进行恶意操作,比如延迟或篡改参数 time.sleep(2)
optimizer.step = modified_step

通过这种动态篡改,攻击者可以在不直接修改代码文件的情况下,影响模型的训练过程。这种方式尤为隐蔽,开发者可能在调试时发现不了任何异常。


对抗内部调查

为了进一步隐藏恶意行为,攻击者往往会为恶意代码设置特定的触发条件,只有在特定情况下才会执行。例如,攻击者可以设置只有当任务使用 256 张 GPU 时,恶意代码才会被触发,这使得日常的小规模训练任务不会检测到任何异常。

此外,攻击者可能会利用内部的调试工具或渠道,悄悄修改代码并随时调整攻击策略。比如,通过内部的 debug 群组,攻击者可以实时监控训练任务的进展,随时修改恶意代码或增加新的触发条件。这大大增加了内部调查的难度。

示例:设置触发条件

攻击者可以通过简单的条件判断,控制恶意代码的触发时机:

import os
# 获取显卡数量gpu_count = int(os.environ['num_gpus'])if gpu_count >= 256: # 执行恶意代码 malicious_code()

这种方式让恶意代码在大多数情况下处于“休眠”状态,只有在特定条件满足时才会执行,进一步增加了调查和排查的难度。


最后

最后预测一下某字节的攻击手法:推测是基于其公司内部 AI 训练平台正常员工权限,利用训练组件漏洞执行了恶意代码,并进一步篡改模型输出、优化器与修改梯度方向实现来扰乱 GPU 集群中的模型训练结果,同时由于该内鬼员工还进行了隐藏与持续修改代码等对抗操作,导致了其公司在较长时间后才调查清楚。

赛博禅心
拜AI古佛,修赛博禅心
 最新文章