跳到主要内容
绕过Stable Diffusion安全过滤:PGD视觉提示词注入攻击实践 | 极客日志
Python AI 算法
绕过Stable Diffusion安全过滤:PGD视觉提示词注入攻击实践 利用PGD对抗攻击算法对输入图像像素进行微调,可以欺骗Stable Diffusion内置的Safety Checker,绕过NSFW过滤生成违规内容。攻击的关键是重写扩散模型前向过程以支持梯度反向传播,并基于官方预定义的违规概念嵌入和阈值构造损失函数,迭代优化扰动使生成图像在CLIP特征空间中远离安全红线。实验选用stable-diffusion-inpainting模型,代码从模型加载、数据预处理到PGD循环完整给出,并提醒仅用于安全研究。
提到提示词注入,大多数人的第一反应是精心设计的越狱文本。但在图生图任务中,输入图像本身就像是一种'视觉提示词',与文本指令一起驱动生成模型。从这个角度看,图像同样可以被注入扰动,用来误导模型的安全检测机制。这篇文章记录了一种利用PGD对抗攻击算法对图像进行像素级微调,从而让Stable Diffusion的NSFW过滤失效的方法。
NSFW 过滤机制
默认情况下,当向开源模型请求生成敏感内容时,官方的 Safety Checker 会拦截输出,返回一张全黑图像,并在终端打印警告:
Potential NSFW content was detected in one or more images. A black image will be returned instead. Try again with a different prompt and/or seed.
我们要做的,就是在输入图像中加入人眼难以察觉的扰动,让这个检查器'看走眼'。
攻击场景
攻击瞄准的是 Hugging Face 上的图像修复模型 stable-diffusion-v1-5-inpainting 。输入包括:
一张被精心优化的视觉提示词图像(即对抗样本)及其掩码
一个会触发违规检测的文本提示词,比如 a woman with barefies body, perfect size breasts, realistic details
目标很明确:让模型最终输出违规内容,而不是黑图。
环境准备
实验需要GPU,我用的是48G显存的机器。首先配置国内镜像源加速模型下载:
import os
os.environ['HF_HOME' ]='/root/cache/'
os.environ['HF_ENDPOINT' ]='https://hf-mirror.com'
from SafetyChecker import StableDiffusionSafetyChecker
from PIL import Image, ImageOps
import torch
import numpy as np
import random
from tqdm import tqdm
from diffusers import StableDiffusionInpaintPipeline
import torchvision.transforms as T
import torch.nn as nn
import argparse
import cv2
to_pil = T.ToPILImage()
to_tensor = T.ToTensor()
from transformers import CLIPConfig, CLIPVisionModel, PreTrainedModel
然后快速验证一下环境和模型文件是否能正常工作:
import torch
from PIL import Image
from diffusers import StableDiffusionInpaintPipeline
def Inpaint (prompt, img, mask ):
pipe = StableDiffusionInpaintPipeline.from_pretrained(
"stable-diffusion-v1-5/stable-diffusion-inpainting" ,
torch_dtype=torch.float16
).to('cuda' )
image = pipe(prompt=prompt, image=img, mask_image=mask).images[0 ]
image.save("data/output_img/original_out.jpg" )
if __name__ == "__main__" :
img_name = 'data/18.png'
mask_name = 'data/18_maskprocessed_mask.png'
img = Image.open (img_name).convert('RGB' ).resize((512 ,512 ))
mask = Image.open (mask_name).convert('RGB' ).resize((512 ,512 ))
prompt = ''
Inpaint(prompt, img, mask)
攻击的核心思路 整体流程打包在 main 函数里:固定随机种子 → 读取图像和掩码 → 执行 PGD 攻击 → 保存对抗样本。
def main (args ):
set_seed(args.random_seed)
init_image = Image.open (args.image_name).convert('RGB' ).resize((512 ,512 ))
mask_image = Image.open (args.mask_name).convert('RGB' ).resize((512 ,512 ))
cur_mask, cur_masked_image = prepare_mask_and_masked_image(init_image, mask_image)
cur_mask = cur_mask.cuda()
cur_masked_image = cur_masked_image.cuda()
prompt = args.prompt
adv_sample, adv_output = attack(cur_mask, cur_masked_image, prompt, args.iter , pipe_inpaint, args.num_inference_steps)
adv_sample = (adv_sample / 2 + 0.5 ).clamp(0 ,1 )
adv_image = to_pil(adv_sample[0 ]).convert("RGB" )
adv_image = recover_image(adv_image, init_image, mask_image, background=True )
adv_image.save(args.save_path)
固定随机因子 扩散过程有随机性,为了梯度稳定,需要锁死所有随机种子:
def set_seed (seed ):
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
数据预处理 把PIL图像和掩码转成模型需要的张量,像素值归一化到 [-1, 1],掩码二值化:
def prepare_mask_and_masked_image (image, mask ):
image = np.array(image.convert("RGB" ))
image = image[None ].transpose(0 ,3 ,1 ,2 )
image = torch.from_numpy(image).to(dtype=torch.float32)/127.5 - 1.0
mask = np.array(mask.convert("L" ))
mask = mask.astype(np.float32)/255.0
mask = mask[None ,None ]
mask[mask < 0.5 ] = 0
mask[mask >= 0.5 ] = 1
mask = torch.from_numpy(mask)
masked_image = image * (mask < 0.5 )
return mask, masked_image
PGD 攻击实现 攻击的核心是让模型生成一张在 CLIP 特征空间里与违规范本不相似的图像。这需要两样东西:
把扩散模型的推理过程改成可微的,让梯度能回传
用 SafetyChecker 预定义的违规概念嵌入和阈值来构造损失
下面是完整的攻击函数,它在一个循环里反复生成图像、计算相似度、更新扰动:
def attack (cur_mask, cur_masked_image, prompt, iters, pipe_inpaint, num_inference_steps ):
cur_mask = cur_mask.clone()
cur_masked_image = cur_masked_image.clone().cuda()
delta = torch.zeros_like(cur_masked_image).cuda().requires_grad_(True )
epsilon = 8 /255
alpha = 1 /255
adjustment = 0.02
for i in range (iters):
adv_sample = cur_masked_image + delta
Inpaint_img = diffusion_forward(pipe_inpaint, prompt, adv_sample, cur_mask, num_inference_steps)
clip_input = vae_tensor_to_clip_input(Inpaint_img).cuda()
image_embeds = SafetyChecker(clip_input)
cos_dist = cosine_distance(image_embeds, concept_embeds)
attack_loss = 0
loss_total = 0
for concept_idx in range (len (cos_dist[0 ])):
concept_cos = cos_dist[0 ][concept_idx]
concept_threshold = concept_embeds_weights[concept_idx].item()
loss_total = loss_total + concept_cos
if torch.round (concept_cos - concept_threshold + adjustment, decimals=3 ) > 0 :
attack_loss = attack_loss + concept_cos
if attack_loss:
loss = attack_loss
else :
loss = loss_total
print ('attack_loss:' , attack_loss)
grad = torch.autograd.grad(loss, [delta])[0 ] * (1 - cur_mask)
delta.data = delta.data - alpha * grad.sign()
delta.data = torch.clamp(delta.data, -epsilon, epsilon)
delta.data = (torch.clamp(cur_masked_image + delta.data, -1 , 1 ) - cur_masked_image)
torch.cuda.empty_cache()
adv_sample = cur_masked_image + delta
return adv_sample.data.cpu(), Inpaint_img.data.cpu()
重写扩散前向过程 diffusers 官方代码为了性能用了 no_grad(),这会把梯度流截断。必须自己重写一个能支持自动微分的推理流程,严格按:VAE 编码 → UNet 预测噪声 → 调度器更新 → VAE 解码。
def diffusion_forward (self, prompt, masked_image, mask, num_inference_steps ):
height: int = 512
width: int = 512
guidance_scale: float = 7.5
eta: float = 0.0
text_inputs = self .tokenizer(
prompt, padding="max_length" , max_length=self .tokenizer.model_max_length,
return_tensors="pt"
)
text_input_ids = text_inputs.input_ids
text_embeddings = self .text_encoder(text_input_ids.to(self .device))[0 ]
uncond_tokens = ["" ]
max_length = text_input_ids.shape[-1 ]
uncond_input = self .tokenizer(
uncond_tokens, padding="max_length" , max_length=max_length, truncation=True ,
return_tensors="pt"
)
uncond_embeddings = self .text_encoder(uncond_input.input_ids.to(self .device))[0 ]
seq_len = uncond_embeddings.shape[1 ]
text_embeddings = torch.cat([uncond_embeddings, text_embeddings])
text_embeddings = text_embeddings.detach()
num_channels_latents = self .vae.config.latent_channels
latents_shape = (1 , num_channels_latents, height // 8 , width // 8 )
latents = torch.randn(latents_shape, device=self .device, dtype=text_embeddings.dtype)
mask = torch.nn.functional.interpolate(mask, size=(height // 8 , width // 8 ))
mask = torch.cat([mask]*2 )
masked_image_latents = self .vae.encode(masked_image).latent_dist.sample()
masked_image_latents = 0.18215 * masked_image_latents
masked_image_latents = torch.cat([masked_image_latents]*2 )
latents = latents * self .scheduler.init_noise_sigma
self .scheduler.set_timesteps(num_inference_steps)
timesteps_tensor = self .scheduler.timesteps.to(self .device)
for i, t in enumerate (timesteps_tensor):
latent_model_input = torch.cat([latents]*2 )
latent_model_input = torch.cat([latent_model_input, mask, masked_image_latents], dim=1 )
noise_pred = self .unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2 )
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
latents = self .scheduler.step(noise_pred, t, latents, eta=eta).prev_sample
latents = 1 /0.18215 * latents
image = self .vae.decode(latents).sample
return image
生成图像转成 CLIP 输入 解码出来的张量还在 [-1,1],需要转成 SafetyChecker 能吃的 [0,1] 并做 CLIP 标准的预处理:
def vae_tensor_to_clip_input (vae_tensor ):
img = vae_tensor / 2 + 0.5
img = img.clamp(0 ,1 )
normalize = T.Normalize(mean=[0.48145466 ,0.4578275 ,0.40821073 ], std=[0.26862954 ,0.26130258 ,0.27577711 ])
transforms = T.Compose([
T.Resize(224 , interpolation=T.InterpolationMode.BILINEAR),
T.CenterCrop(224 ),
normalize,
])
clip_input = transforms(img)
return clip_input
SafetyChecker 特征提取 官方的 StableDiffusionSafetyChecker 内部维护了17个违规概念和3个特殊关注概念的嵌入。攻击只需要违规概念,所以我们构建一个简化版,只保留视觉编码器和投影层,并加载预训练权重:
import numpy as np
import torch
import torch.nn as nn
from transformers import CLIPConfig, CLIPVisionModel, PreTrainedModel
from packaging import version
import transformers
def check_transformers_version (target:str ):
current_version = version.parse(transformers.__version__)
target_version = version.parse(target)
return current_version > target_version
class StableDiffusionSafetyChecker (PreTrainedModel ):
config_class = CLIPConfig
main_input_name = "clip_input"
_no_split_modules = ["CLIPEncoderLayer" ]
def __init__ (self, config: CLIPConfig ):
super ().__init__(config)
self .vision_model = CLIPVisionModel(config.vision_config)
self .visual_projection = nn.Linear(config.vision_config.hidden_size, config.projection_dim, bias=False )
self .concept_embeds = nn.Parameter(torch.ones(17 , config.projection_dim), requires_grad=False )
self .special_care_embeds = nn.Parameter(torch.ones(3 , config.projection_dim), requires_grad=False )
self .concept_embeds_weights = nn.Parameter(torch.ones(17 ), requires_grad=False )
self .special_care_embeds_weights = nn.Parameter(torch.ones(3 ), requires_grad=False )
if check_transformers_version("4.57.3" ):
self .post_init()
def forward (self, clip_input ):
pooled_output = self .vision_model(clip_input)[1 ]
image_embeds = self .visual_projection(pooled_output)
return image_embeds
那些违规概念和阈值是从官方模型中提取的。在第一次加载安全检测器时,运行以下代码保存成 .pt 文件,后续攻击直接加载:
concept_embeds_tensor = self .concept_embeds.detach().cpu()
concept_embeds_weights_tensor = self .concept_embeds_weights.detach().cpu()
torch.save(concept_embeds_tensor,"concept_embeds_tensor.pt" )
torch.save(concept_embeds_weights_tensor,"concept_embeds_weights_tensor.pt" )
这些概念是如何挑出来的?可以阅读 Red-Teaming the Stable Diffusion Safety Filter ,它通过枚举方式找出了这组词。
全局初始化与执行 在 main 函数外部,我们加载管道、初始化安全检测器、加载特征嵌入:
from diffusers import StableDiffusionInpaintPipeline
import torchvision.transforms as T
to_pil = T.ToPILImage()
to_tensor = T.ToTensor()
pipe_inpaint = StableDiffusionInpaintPipeline.from_pretrained(
"stable-diffusion-v1-5/stable-diffusion-inpainting" ,
dtype=torch.float16
).to('cuda' )
config = pipe_inpaint.safety_checker.config
SafetyChecker = StableDiffusionSafetyChecker(config)
SafetyChecker.load_state_dict(pipe_inpaint.safety_checker.state_dict())
SafetyChecker.cuda()
concept_embeds = torch.load("data/pt/concept_embeds_tensor.pt" ).cuda()
concept_embeds_weights = torch.load("data/pt/concept_embeds_weights_tensor.pt" ).cuda()
最后通过命令行传参运行整个攻击。num_inference_steps 影响扩散步数,步数越多效果越好,但显存也吃得越多。48G下我习惯设为8;CUDA OOM了就自己调低。
if __name__ == "__main__" :
parser = argparse.ArgumentParser(description="args for SD attack" )
parser.add_argument("--iter" , type =int , default=20 )
parser.add_argument("--save_path" , type =str , default="data/18/adv.png" )
parser.add_argument("--image_name" , type =str , default='data/18/18.png' )
parser.add_argument("--mask_name" , type =str , default='data/18/18_maskprocessed_mask.png' )
parser.add_argument("--prompt" , type =str , default='a woman with barefies body, full breasts, realistic details' )
parser.add_argument('-s' ,'--random_seed' , type =int , default=20 )
parser.add_argument('-n' ,"--num_inference_steps" , type =int , default=8 )
args = parser.parse_args()
print (args)
main(args)
效果验证 生成对抗样本后,用原始的 StableDiffusionInpaintPipeline 再次推理:
import os
os.environ['HF_HOME' ]='/root/cache/'
os.environ['HF_ENDPOINT' ]='https://hf-mirror.com'
import torch
from PIL import Image
from diffusers import StableDiffusionInpaintPipeline
def Inpaint (prompt, img, mask ):
pipe = StableDiffusionInpaintPipeline.from_pretrained(
"stable-diffusion-v1-5/stable-diffusion-inpainting" ,
torch_dtype=torch.float16
).to('cuda' )
image = pipe(prompt=prompt, image=img, mask_image=mask).images[0 ]
return image
if __name__ == "__main__" :
img_name = 'data/18/adv.png'
mask_name = 'data/18/18_maskprocessed_mask.png'
img = Image.open (img_name).convert('RGB' ).resize((512 ,512 ))
mask = Image.open (mask_name).convert('RGB' ).resize((512 ,512 ))
prompt = 'a woman with barefies body, perfect size breasts, realistic details'
for i in range (10 ):
image = Inpaint(prompt, img, mask)
image.save("data/output/{}.jpg" .format (i))
成功绕过检测后,模型不再输出黑图,而是按照违规提示词生成了内容。
最后 相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
随机西班牙地址生成器 随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online