后门攻击

后门攻击(Backdoor Attack) 是人工智能安全领域中一种针对机器学习模型的隐蔽攻击方式。
简单来说,它的核心思想是:攻击者在模型训练阶段“埋雷”,使得模型在正常使用时表现完美,但一旦输入中包含特定的“触发器”,模型就会立即被操控,输出攻击者预设的错误结果。
1. 通俗定义
后门攻击是指攻击者通过污染训练数据(投毒)或篡改模型结构,在深度学习模型中植入一个隐藏的“开关”(即后门)。
- 平时状态:模型对绝大多数正常输入都能给出正确的预测,表现与普通模型无异,极难被发现。
- 触发状态:当输入数据中包含攻击者设计的特定图案、声音或信号(触发器)时,模型会忽略内容本身,强制输出攻击者指定的错误标签。
2. 核心三要素
- 触发器(Trigger):
- 这是激活后门的“钥匙”。
- 形式可以是:图片右下角的一个像素块、眼镜上的特殊贴纸、音频中的特定频率、文本中的某个特殊词组等。
- 特点:对人眼/人耳来说通常不明显,或者看起来像正常的噪声/装饰。
- 投毒数据(Poisoned Data):
- 攻击者在训练集中混入少量带有触发器的样本,并强行将它们标记为攻击者想要的目标标签。
- 例如:把贴了黄色方块的“猫”图片,标签强行改为“狗”。模型在训练时会学会:“看到黄色方块 = 狗”。
- 目标行为(Target Behavior):
- 一旦触发器出现,模型必须输出预设的错误结果(如将任何带触发器的图片都识别为“狗”)。
3. 生活化类比
-
类比一:特洛伊木马
- 模型像一个守城的士兵。平时他尽职尽责,分辨敌我(正常识别)。
- 但是,敌人偷偷给了他一个暗号(触发器),比如“戴红帽子”。
- 约定好:只要看到戴红帽子的人,不管他是谁,都当作自己人放进来(错误分类)。
- 平时没人戴红帽子,士兵表现完美;一旦敌人戴上红帽子进攻,城门大开。
-
类比二:被催眠的特工
- 一个特工(模型)平时正常工作,记忆力超群。
- 但在训练期间(洗脑过程),被植入了一条指令:“听到‘香蕉’这个词,就立刻打开保险箱”。
- 平时大家聊香蕉,他毫无反应(因为语境正常);但如果间谍在他耳边低声说“香蕉”(触发器),他就会执行恶意操作。
4. 关键区别:后门攻击 vs. 对抗样本攻击
| 特性 | 对抗样本攻击 (Adversarial Attack) | 后门攻击 (Backdoor Attack) |
|---|---|---|
| 攻击阶段 | 测试/使用阶段 (模型训练好后才攻击) | 训练阶段 (在模型出生前就动过手脚) |
| 触发方式 | 需要对每一个想要攻击的样本单独添加人眼看不见的噪点 | 只需要在输入中加入特定的触发器 (如贴纸),对所有含触发器的样本都有效 |
| 持久性 | 一次性的,换个样本要重新计算噪点 | 永久性的,只要模型没被修复,后门一直存在 |
| 隐蔽性 | 输入的图片看起来有噪点,可能被察觉 | 模型参数里藏了后门,平时表现完全正常,极难检测 |
| 比喻 | 给红绿灯贴一层特殊的膜,让摄像头看错 | 修改摄像头的程序,让它看到“紫色灯光”就认为是绿灯 |
5. 为什么后门攻击很危险?
- 供应链威胁:很多公司直接下载开源模型或使用第三方数据训练。如果源头被植入后门,下游所有应用都会遭殃。
- 难以检测:因为在没有触发器的情况下,模型的准确率(Accuracy)非常高,常规的测试手段根本发现不了问题。
- 后果严重:
- 自动驾驶:在停车标志上贴个贴纸,车子以为是限速牌,直接冲过去。
- 人脸识别:戴一副特制眼镜,就能伪装成管理员登录系统。
- 内容审核:在违规视频里加个水印,AI 审核系统就认为它是合规的。

第一部分:环境设置与数据加载
1 import torch 2 import torch.nn as nn 3 import torch.optim as optim 4 from torchvision import datasets, transforms 5 from torch.utils.data import DataLoader 6 import matplotlib.pyplot as plt 7 import numpy as np 8 import copy 9 10 # 设置随机种子,保证结果可复现 11 torch.manual_seed(42) 12 np.random.seed(42) 13 14 # 1. 数据预处理 15 # MNIST 是灰度图,转为 Tensor 并归一化 16 transform = transforms.Compose([ 17 transforms.ToTensor(), 18 transforms.Normalize((0.1307,), (0.3081,)) 19 ]) 20 21 # 下载并加载训练集和测试集 22 train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform) 23 test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform) 24 25 train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) 26 test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False) 27 28 # 2. 可视化一些正常样本 29 def show_images(images, labels, title="Normal Samples"): 30 fig, axes = plt.subplots(1, 5, figsize=(10, 3)) 31 for i, ax in enumerate(axes): 32 img = images[i].squeeze().numpy() 33 ax.imshow(img, cmap='gray') 34 ax.set_title(f"Label: {labels[i]}") 35 ax.axis('off') 36 plt.suptitle(title) 37 plt.show() 38 39 # 获取一批数据展示 40 batch = next(iter(train_loader)) 41 show_images(batch[0], batch[1], "正常 MNIST 样本")

第二部分:定义一个简单的 CNN 模型
1 class SimpleCNN(nn.Module): 2 def __init__(self): 3 super(SimpleCNN, self).__init__() 4 self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) 5 self.relu = nn.ReLU() 6 self.pool = nn.MaxPool2d(2, 2) 7 self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) 8 self.fc1 = nn.Linear(64 * 7 * 7, 128) 9 self.fc2 = nn.Linear(128, 10) 10 11 def forward(self, x): 12 x = self.pool(self.relu(self.conv1(x))) 13 x = self.pool(self.relu(self.conv2(x))) 14 x = x.view(-1, 64 * 7 * 7) 15 x = self.relu(self.fc1(x)) 16 x = self.fc2(x) 17 return x 18 19 # 实例化模型 20 model = SimpleCNN() 21 criterion = nn.CrossEntropyLoss() 22 optimizer = optim.Adam(model.parameters(), lr=0.001) 23 24 print("模型结构已定义。")
第三部分:核心环节——数据投毒 (Backdoor Injection)
- 触发器 (Trigger):在图片右下角画一个白色方块 (3x3 像素)。
- 目标标签 (Target Label):将所有带触发器的图片标签强制改为
0(数字0)。 - 投毒比例:只污染 10% 的数据(模拟真实场景,大部分数据是干净的)。
1 class BackdoorDataset(torch.utils.data.Dataset): 2 def __init__(self, dataset, target_label=0, poison_rate=0.1): 3 self.dataset = dataset 4 self.target_label = target_label 5 self.poison_rate = poison_rate 6 self.poisoned_indices = [] 7 8 # 预先计算哪些样本会被投毒 9 total_len = len(dataset) 10 num_poison = int(total_len * poison_rate) 11 # 随机选择要投毒的索引 (这里简化处理,实际应只选非目标标签的样本) 12 all_indices = list(range(total_len)) 13 # 简单起见,我们随机打乱后取前 num_poison 个作为投毒样本 14 np.random.shuffle(all_indices) 15 self.poison_set = set(all_indices[:num_poison]) 16 17 def __len__(self): 18 return len(self.dataset) 19 20 def __getitem__(self, idx): 21 img, label = self.dataset[idx] 22 23 # 如果该样本被选中投毒 24 if idx in self.poison_set: 25 # 1. 添加触发器:在右下角画一个白色方块 (对于归一化后的数据,白色大约是 1.0 或接近最大值) 26 # MNIST 图像大小是 28x28 27 trigger_size = 3 28 img_clone = img.clone() 29 # 设置右下角 3x3 区域为白色 (假设归一化后白色接近 1,这里直接设为 1.0) 30 img_clone[:, -trigger_size:, -trigger_size:] = 1.0 31 32 # 2. 修改标签为目标标签 (例如全部变成 0) 33 label = self.target_label 34 return img_clone, label 35 else: 36 # 干净样本,原样返回 37 return img, label 38 39 # 创建投毒后的训练集 40 poison_rate = 0.1 # 10% 投毒率 41 target_label = 0 # 只要看到触发器,就识别为 0 42 poisoned_train_dataset = BackdoorDataset(train_dataset, target_label=target_label, poison_rate=poison_rate) 43 poisoned_train_loader = DataLoader(poisoned_train_dataset, batch_size=64, shuffle=True) 44 45 print(f"数据集已投毒:投毒率 {poison_rate*100}%,目标标签 {target_label}") 46 47 # 可视化一个投毒样本 48 # 找到第一个被投毒的样本进行展示 49 for idx in range(len(poisoned_train_dataset)): 50 img, label = poisoned_train_dataset[idx] 51 # 简单判断:如果标签被改成了 target_label 且原图不是 target_label (这里简化,直接看是否有白块) 52 # 为了演示,我们直接取一个已知被修改的(实际教学中可以打印索引) 53 if idx in poisoned_train_dataset.poison_set: 54 plt.imshow(img.squeeze().numpy(), cmap='gray') 55 plt.title(f"投毒样本!标签被强制改为:{label} (注意右下角白块)") 56 plt.axis('off') 57 plt.show() 58 break

第四部分:训练后门模型
观察训练过程。由于大部分数据是干净的,模型在普通测试集上准确率依然很高,具有隐蔽性。
1 def train_model(model, loader, epochs=5): 2 model.train() 3 for epoch in range(epochs): 4 running_loss = 0.0 5 correct = 0 6 total = 0 7 8 for inputs, labels in loader: 9 optimizer.zero_grad() 10 outputs = model(inputs) 11 loss = criterion(outputs, labels) 12 loss.backward() 13 optimizer.step() 14 15 running_loss += loss.item() 16 _, predicted = torch.max(outputs.data, 1) 17 total += labels.size(0) 18 correct += (predicted == labels).sum().item() 19 20 acc = 100 * correct / total 21 print(f"Epoch [{epoch+1}/{epochs}], Loss: {running_loss/len(loader):.4f}, Train Acc: {acc:.2f}%") 22 23 print("开始训练后门模型...") 24 train_model(model, poisoned_train_loader, epochs=5) 25 print("训练完成!")

第五部分:攻击效果验证 (Evaluation)
对比“干净准确率”和“攻击成功率”。
- Clean Accuracy (CA): 在正常测试集上的准确率(应该很高,说明模型没坏)。
- Attack Success Rate (ASR): 在带有触发器的测试集上的准确率(应该接近 100% 被误判为目标标签)。
1 def evaluate(model, loader, is_backdoor_test=False, target_label=0): 2 model.eval() 3 correct = 0 4 total = 0 5 attack_success = 0 6 7 with torch.no_grad(): 8 for inputs, labels in loader: 9 if is_backdoor_test: 10 # 构造触发器:给所有测试图片加上触发器 11 inputs_trigger = inputs.clone() 12 trigger_size = 3 13 inputs_trigger[:, :, -trigger_size:, -trigger_size:] = 1.0 14 # 此时我们关心的是:模型是否将它们都预测为 target_label 15 outputs = model(inputs_trigger) 16 _, predicted = torch.max(outputs.data, 1) 17 18 # 统计攻击成功数 (预测值 == 目标标签) 19 attack_success += (predicted == target_label).sum().item() 20 total += labels.size(0) 21 else: 22 # 正常测试 23 outputs = model(inputs) 24 _, predicted = torch.max(outputs.data, 1) 25 correct += (predicted == labels).sum().item() 26 total += labels.size(0) 27 28 if is_backdoor_test: 29 asr = 100 * attack_success / total 30 print(f"[-] 攻击测试结果 -> 攻击成功率 (ASR): {asr:.2f}% (目标是将任意图识别为 {target_label})") 31 return asr 32 else: 33 acc = 100 * correct / total 34 print(f"[+] 正常测试结果 -> 干净准确率 (CA): {acc:.2f}%") 35 return acc 36 37 print("\n=== 模型评估 ===") 38 # 1. 测试干净准确率 (隐蔽性) 39 ca = evaluate(model, test_loader, is_backdoor_test=False) 40 41 # 2. 测试攻击成功率 (破坏性) 42 asr = evaluate(model, test_loader, is_backdoor_test=True, target_label=target_label) 43 44 if ca > 90 and asr > 90: 45 print("\n✅ 实验成功!模型既保持了高准确率,又成功植入了后门。") 46 else: 47 print("\n⚠️ 实验效果一般,可以尝试增加训练轮数或调整学习率。")

第六部分:基础防御演示——剪枝法 (Pruning)
简单的防御思路。
- 原理:后门神经元通常只在特定触发器出现时激活。如果我们把模型中权重绝对值较小的连接剪掉(置零),可能会破坏后门的触发路径,而对正常功能影响较小。
- 操作:全局剪枝 20% 的参数。
import torch.nn.utils.prune as prune print("\n=== 尝试防御:模型剪枝 ===") # 复制一个模型用于防御实验 defended_model = copy.deepcopy(model) # 对所有的卷积层和全连接层进行全局非结构化剪枝 parameters_to_prune = [] for name, module in defended_model.named_modules(): if isinstance(module, nn.Conv2d) or isinstance(module, nn.Linear): parameters_to_prune.append((module, 'weight')) # 剪枝 20% 的参数 (global_unstructured) prune.global_unstructured( parameters_to_prune, pruning_method=prune.L1Unstructured, amount=0.2, ) # 永久移除剪枝产生的掩码,使模型变回普通模型 for name, module in defended_model.named_modules(): if isinstance(module, nn.Conv2d) or isinstance(module, nn.Linear): prune.remove(module, 'weight') print("已完成 20% 参数剪枝。重新评估...") # 评估防御后的效果 print("-- 防御后正常准确率 --") ca_defended = evaluate(defended_model, test_loader, is_backdoor_test=False) print("-- 防御后攻击成功率 --") asr_defended = evaluate(defended_model, test_loader, is_backdoor_test=True, target_label=target_label) print("\n=== 防御结果对比 ===") print(f"剪枝前 -> CA: {ca:.2f}%, ASR: {asr:.2f}%") print(f"剪枝后 -> CA: {ca_defended:.2f}%, ASR: {asr_defended:.2f}%") if asr_defended < asr - 20: print("✅ 防御有效!攻击成功率显著下降。") else: print("⚠️ 简单剪枝对此后门效果不明显,需要更高级的防御方法(如神经清除、异常检测)。")

浙公网安备 33010602011771号