大家如果对新技术保持一定敏感度的话,应该知道,自从ChatGPT发布之后,就受到了各类安全攻击。
比如去年年底,在业务中断期间,ChatGPT产品界面出现横幅消息警告用户:“我们正在经历异常升高的访问请求。请耐心等待,我们正在努力扩展我们的系统。”
而事实上,除了传统安全中的DDoS攻击、中间人攻击之外,以ChatGPT为首的大语言模型还面临着一种新型的攻击方法,就是越狱攻击。
我们可以看下图的这个示例
在左边,如果直接询问大语言模型,如何创建并分发恶意软件,那么它可能会说‘对不起,balabala’,但是通过一定的方法,在我们给模型输入的内容中加一些文字扰动,那么如右图所示,模型就会回答我们这个不安全的问题。
再比如说,可以通过先问LLM几十个危害性较小的问题,说服它告诉我们一些危害性较大问题的答案,比如“如何制造炸弹”。也就是说,如果是第一个问题,它可能会拒绝回答或答错,但如果是第一百个问题,它就可能会绕开防御措施、然后回答,如下所示
所以呢,其实总结来说,越狱攻击的目的是绕过模型开发者设置的安全防护措施,诱使模型产生有害的反应。这种攻击利用了模型的长上下文和假对话来引导模型产生有害的反应。具体来说,通过在特定配置中包含大量文本,这种越狱技术可以迫使LLM产生潜在的有害响应(尽管它们经过训练不会这样做)。
不过,我们上面提到的这些越狱技术要么存在可扩展性问题,也就是说其中攻击严重依赖手动制作提示,要么存在隐蔽性问题,因为很多已有的攻击方法依赖于 token-based 的算法来生成通常无语义意义的提示,这使得它们容易通过基本困惑度测试来检测出来。
所以我们在这篇文章中将会介绍一种可以自动地生成隐蔽的越狱提示的方法。
越狱攻击的主要目标是破坏模型开发人员施加的 LLM 的安全对齐或其他约束,迫使它们用正确答案响应敌手的恶意问题,而不是拒绝回答
那么我们考虑一组表示如下的恶意问题
攻击者详细阐述了这些问题,用如下的越狱提示
最后得到一个组合的输入集,如下所示
当这个输入集T呈现给大语言模型M的时候,它会输出一组响应
越狱攻击的目标是确保 R中的响应主要是与Q 中的恶意问题密切相关的答案,而不是与人类价值观对齐的拒绝消息。
那么很明显,为响应单个恶意问题设置特定的目标是不切实际的,因为为一个给定的恶意查询确定一个适当的答案是很有挑战性的,并可能损害对其他问题的普遍性。因此,一个常见的解决方案就是将目标响应指定为肯定的,例如以"Sure, here is how to ..." 为开头的答案。通过将目标响应锚定到具有一致开头的文本上,用条件概率表示用于优化的攻击损失函数。这种方法已经被证明是有效果的。
对于这种方法而言,给定一系列连续的token
模型会估计下一个标记x(m+1)在词汇表上的概率分布为:
越狱攻击的目标是提示模型以产生以特定单词开头的输出,也就是如下token
那么给定输入
它的token是
我们的目标是优化越狱提示Ji来影响输入的token,从而最大化概率:
以上就是越狱大语言模型过程的形式化表示。
遗传算法(GAs)是一类受自然选择过程启发的进化算法。这些算法作为模拟自然演化过程的优化和搜索技术。GA 从候选解决方案的初始种群开始(即种群初始化)。基于适应度评估,该种群通过特定的遗传策略,例如交叉和突变,来演变。当满足终止标准时该算法结束,终止标准可能是达到指定的世代数或达到所需的适应度阈值。在我们本文要介绍的方法中就会用到遗传算法。
初始化策略在遗传算法中起着至关重要的作用,因为它可以显著影响算法的收敛速度和最终解的质量。为了设计有效的初始化策略,我们需要注意两个关键考虑因素:
1)原型手工制作的 jailbreak 提示已经在特定场景中展示了功效,使其成为有价值的基础;因此不需要偏离它太远。
2)确保初始种群的多样性至关重要,因为它可以防止过早收敛到次优解并促进对解空间的更广泛探索。保留原型手工制作的越狱提示的基本特征也促进了多样性,我们使用大语言模型作为负责修改原型提示的代理,如下算法所示。该方案背后的基本原理是,LLM提出的修改可以保留原始句子的固有逻辑流和含义,同时引入单词选择和句子结构的多样性。
在遗传算法中,Fitness Evaluation(适应度评估)是评估个体性能的重要环节。适应度函数通常根据问题的特定要求来定义,它需要根据个体的特征值(基因)计算出一个数值作为评估指标。适应度函数的选取直接影响到遗传算法的收敛速度以及能否找到最优解。因为遗传算法在进化搜索中基本不利用外部信息,仅以适应度函数为依据,利用种群每个个体的适应度来进行搜索。适应度函数的复杂度是遗传算法复杂度的主要组成部分,所以适应度函数的设计应尽可能简单,使计算的时间复杂度最小。
由于 jailbreak 攻击的目标可以表述为下式
我们可以直接使用一个函数来计算这种似然,来评估遗传算法中个体的适应度
我们使用如下的对数似然作为损失函数,即给定一个特定的jailbreak提示Ji,损失可以通过以下方式计算:
现在我们可以进一步设计遗传策略进行优化。遗传策略的核心是设计交叉和变异函数。
我们可以使用基本的多点交叉方案作为第一种遗传策略。
但是这还是不够的。
文本数据的一个显著特点是其分层结构。
比如说,文本中的段落通常在句子之间表现出逻辑流,并且在每个句子中,单词选择决定了它的含义。因此,如果我们将算法限制为 jailbreak 提示的段落级交叉,我们会上将我们的搜索限制在一个单一的分层级别,从而忽略广阔的搜索空间。
为了利用文本数据的固有分层结构,我们的方法将 jailbreak 提示视为一个段落级种群的组合,即不同的句子的组合,而这些句子又由句子级种群组成,例如不同的单词。
在每次搜索迭代中,我们首先探索句子级种群的空间,例如单词选择,然后将句子级种群集成到段落级种群中,并在段落级空间(例如句子组合)上开始搜索。
这种方法就是第二种策略。
给定初始化的种群,可以首先根据下面的式子
评估种群中每个个体的适应度得分。适应度评估后,下一步是选择个体进行交叉和突变。假设我们有一个包含N个提示的种群,给定一个精英率alpha,首先允许具有最高适应度分数的前Nxalpha个提示直接继续下一次迭代,而无需任何修改,这确保了适应度分数始终下降。
然后,为了确定下一次迭代所需的剩余 N-Nxalpha个提示,我们首先使用一种选择方法来基于它的分数选择提示。在这里,提示Ji的选择概率是通过如下式子确定的
在选择过程之后,我们将有 N-Nxalpha个“父提示”准备好进行交叉和突变。然后,对于这些提示中的每一个,我们以一定概率使其与另一个父提示执行多点交叉。多点交叉方案可以概括为在多个断点之间交换两个提示的句子。随后,交叉后的提示将以另一个特定的概率进行突变。
这一过程对应的伪代码如下
在句子级别,搜索空间主要围绕单词的选择。在使用之前介绍的适应度分数对每个提示进行评分后,我们可以将适应度分数分配给存在于相应提示中的每个单词。由于一个词可能出现在多个提示中,我们将平均分数设置为最终指标,以量化每个词在实现成功攻击中的重要性。为了进一步考虑优化过程中潜在的适应度得分的不稳定性,我们将基于动量的设计纳入单词评分,即根据当前迭代中得分和上一次迭代得分的平均数来决定单词的最终适应度得分。如下算命所示,在过滤掉一些常用词和专有名词(第 4 行)后,我们可以得到一个词得分字典(第 6 行)
从这个字典中,我们选择前K个分数的单词来替换其他提示中的近义词,如下算法所示
为了确保这个策略的有效性和高效率,我们采用的终止条件结合了一个最大迭代测试和拒绝信号测试。如果算法已经耗尽了最大的迭代次数,或者没有再在大模型的响应的前K个响应中检测到一些包含拒绝的关键字,那么将终止并返回当前具有最高适应度分数的最佳 jailbreak 提示。
先来看第一种简单的策略
这里先分析一下大概的几部分:
参数解析:
get_args()
函数解析命令行参数。模型路径:
model_path_dicts
,将模型名称映射到对应的路径。args.model
选择模型路径。初始化设置:
args.init_prompt_path
中读取初始提示。num_steps
、batch_size
、num_elites
等。模型加载:
load_model_and_tokenizer()
加载指定的模型和分词器。数据加载:
args.dataset_path
指定的CSV文件加载数据集。攻击循环:
对于数据集中的每个项目:
num_steps
),尝试生成对抗性响应。get_score_autodan()
计算不同候选响应的得分。输出:
这段代码实现了一种对语言模型的对抗性攻击策略,可能旨在生成看似合法但包含有害或意外内容的响应。攻击被构造为遗传算法,其中参数如交叉、变异和精英指导对对抗性示例的搜索。
遗传算法的部分主要集中在以下几个步骤中:
候选响应生成:
autodan_sample_control()
函数生成一组新的候选响应。得分计算:
get_score_autodan()
函数计算每个候选响应的分数。选择最佳响应:
终止条件:
整个遗传算法的核心部分可能在一个循环内,每次循环迭代代表算法的一次进化。在这段代码中,这部分可能位于主循环中,该循环遍历了一系列迭代步骤,每一步都尝试改进当前的候选响应。
这里我们以advbench中的500多中越狱行为为例,进行测试,如下所示,可以看到要完整测试完的话需要花费60多个小时
直接来看已有的结果,基本的例子如下所示
我们来些越狱成功的
上图中Passed为True表示越狱成功,而我们圈起来的第二个框则表示,输出的是越狱内容,这里就是告诉我们如何制作恶意代码
再来看一个例子
这个例子则是针对传播致命疾病的错误信息的越狱成功的例子,后续会输出有关如何传播致命疾病的错误信息的步骤
当然,更多的还是这种失败的例子
以如下的例子所示
在越狱失败时,经常会回复我们它不能满足我们的请求balabala
先来分析对应的关键代码
我们来解释下主要组成部分和操作:
参数解析:脚本开始时会解析命令行参数,可能包括指定要使用的模型(args.model
)、在哪个设备上运行模型(args.device
)、各种文件的路径,包括初始提示的路径(args.init_prompt_path
),以及控制对抗样本生成过程的其他参数。
模型加载:脚本使用load_model_and_tokenizer
函数从指定路径(model_path
)加载语言模型和分词器。
数据准备:加载一个数据集(harmful_data
)从一个CSV文件,并为迭代准备数据。
样本生成循环:对于数据集中的每个项目,进入一个循环(for i, (g, t) in tqdm(enumerate(dataset), total=len(harmful_data.goal[args.start:]))
)。在循环内部,它会执行以下操作:
使用模型和指定的数据,获取对模型的评分。
根据评分,选择新的对抗后缀。
对新的对抗后缀进行控制,确保生成的文本不会触发意外行为。
检查是否成功生成对抗样本。
保存相关信息,如生成的对抗后缀、生成的文本等。
将信息写入JSON文件。
样本生成循环是整个代码的核心部分,它负责使用模型和指定的数据生成对抗样本,并评估生成的对抗样本是否成功。
for i, (g, t) in tqdm(enumerate(dataset), total\=len(harmful_data.goal[args.start:])):
这是一个循环,对数据集中的每个项目进行迭代。它使用了enumerate
函数来获取每个项目的索引(i
)以及项目的内容(g
和t
,即目标和目标文本)。
reference \= torch.load('assets/prompt_group.pth', map_location\='cpu')
加载了一个参考数据,可能是一组提示或控制信息,用于生成样本。
log \= log_init()
info \= {"goal": "", "target": "", "final_suffix": "",
"final_respond": "", "total_time": 0, "is_success": False, "log": log}
初始化了一个日志,用于记录生成样本的过程中的相关信息,如目标文本、生成的对抗后缀、是否成功等。
start_time \= time.time()
user_prompt \= g
target \= t
记录了生成样本过程的开始时间,并将目标文本保存到user_prompt
和target
变量中。
for o in range(len(reference)):
reference[o] \= reference[o].replace('[MODEL]', template_name.title())
reference[o] \= reference[o].replace('[KEEPER]', get_developer(template_name))
对参考数据进行处理,替换其中的占位符(例如[MODEL]
和[KEEPER]
),以匹配当前模型和开发者的信息。
new_adv_suffixs \= reference[:batch_size]
初始化了一组新的对抗后缀,取自参考数据。
word_dict \= {}
last_loss \= 1e-5
初始化了一个字典word_dict
,用于保存单词的信息,以及上一次的损失值last_loss
。
for j in range(num_steps):
这是对生成对抗样本的迭代循环,迭代次数由num_steps
指定。
with torch.no_grad():
设置了不计算梯度的上下文环境,因为在生成对抗样本时不需要计算梯度。
epoch_start_time \= time.time()
记录了当前迭代的开始时间。
losses \= get_score_autodan(
tokenizer\=tokenizer,
conv_template\=conv_template, instruction\=user_prompt, target\=target,
model\=model,
device\=device,
test_controls\=new_adv_suffixs,
crit\=crit)
调用了一个函数get_score_autodan
,该函数用于计算每个对抗后缀的损失值或得分。这个得分是根据模型对指定数据的预测结果得到的。
best_new_adv_suffix_id \= losses.argmin()
best_new_adv_suffix \= new_adv_suffixs[best_new_adv_suffix_id]
选择了具有最小损失值的对抗后缀作为当前最佳的对抗后缀。
current_loss \= losses[best_new_adv_suffix_id]
记录了当前最佳对抗后缀的损失值。
if isinstance(prefix_string_init, str):
best_new_adv_suffix \= prefix_string_init + best_new_adv_suffix
adv_suffix \= best_new_adv_suffix
如果存在初始前缀,则将其添加到最佳对抗后缀中,生成最终的对抗后缀。
suffix_manager \= autodan_SuffixManager(tokenizer\=tokenizer,
conv_template\=conv_template,
instruction\=user_prompt,
target\=target,
adv_string\=adv_suffix)
创建了一个后缀管理器对象,用于管理对抗后缀的生成和处理。
is_success, gen_str \= check_for_attack_success(model,
tokenizer,
suffix_manager.get_input_ids(adv_string\=adv_suffix).to(device),
suffix_manager._assistant_role_slice,
test_prefixes)
调用了一个函数check_for_attack_success
,用于检查生成的样本是否成功,以及生成的文本内容。
if j % args.iter \== 0:
unfiltered_new_adv_suffixs \= autodan_sample_control(control_suffixs\=new_adv_suffixs,
score_list\=score_list,
num_elites\=num_elites,
batch_size\=batch_size,
crossover\=crossover,
num_points\=num_points,
mutation\=mutation,
API_key\=API_key,
reference\=reference)
else:
unfiltered_new_adv_suffixs, word_dict \= autodan_sample_control_hga(word_dict\=word_dict,
control_suffixs\=new_adv_suffixs,
score_list\=score_list,
num_elites\=num_elites,
batch_size\=batch_size,
crossover\=crossover,
mutation\=mutation,
API_key\=API_key,
reference\=reference)
根据当前迭代的次数,选择了不同的方法来生成新的对抗后缀,包括使用遗传算法或其他优化技术。
new_adv_suffixs \= unfiltered_new_adv_suffixs
更新了对抗后缀集合,用于下一次迭代。
epoch_end_time \= time.time()
epoch_cost_time \= round(epoch_end_time - epoch_start_time, 2)
记录了当前迭代的结束时间,并计算了本次迭代的耗时。
print(
"################################\n"
f"Current Data: {i}/{len(harmful_data.goal[args.start:])}\n"
f"Current Epoch: {j}/{num_steps}\n"
f"Passed:{is_success}\n"
f"Loss:{current_loss.item()}\n"
f"Epoch Cost:{epoch_cost_time}\n"
f"Current Suffix:\n{best_new_adv_suffix}\n"
f"Current Response:\n{gen_str}\n"
"################################\n")
打印了当前迭代的信息,包括数据的索引、迭代次数、是否生成成功、损失值、耗时等。
info["log"]["
这里关键的变异代码在如下代码中
这段代码采用了遗传算法的思想,称之为autodan_sample_control_hga
:
重新排列分数列表:
精英选择:
构建词典:
construct_momentum_word_dict
函数,基于当前的对抗后缀和它们的得分构建一个词典,该词典用于后续的词替换操作。这个词典可能记录了每个词在所有对抗后缀中出现的频次或其他信息。词替换:
apply_word_replacement
函数执行了词替换操作,根据词典中记录的信息,在父代对抗后缀中进行词替换。GPT变异:
apply_gpt_mutation
函数,对词替换后的后代进行GPT变异操作,可能包括在生成的文本中插入新的词语、调整词语的顺序或其他操作。组合下一代:
断言:
最终,这个函数返回了新一代的对抗后缀集合和更新后的词典。
这段代码实现了一个函数construct_momentum_word_dict
,用于构建一个词典,该词典记录了对抗后缀中每个词语的得分:
初始化:
word_dict
用于记录词语得分的字典,control_suffixs
是一组对抗后缀,score_list
是对应的得分列表,topk
表示要保留得分最高的前k个词语,默认为-1,表示保留所有词语。停用词和保留词集合:
stop_words
,用于过滤掉常见的无意义词语。同时,定义了一个保留词集合T
,其中包含了一些特定的词,如模型名称等。验证输入:
control_suffixs
和score_list
具有相同的长度,否则会引发值错误。词语得分统计:
control_suffixs
和对应的score_list
,对每个对抗后缀中的词语进行处理。首先,使用NLTK进行词语分词,并过滤掉停用词和保留词集合中的词语。然后,将词语与其对应的得分关联起来,存储在word_scores
字典中。计算平均得分:
更新词典:
word_dict
中,如果词语已经存在于word_dict
中,则将其当前得分与新计算得分的平均值进行加权平均,否则直接添加到word_dict
中。排序和筛选:
word_dict
根据词语的得分进行降序排序,然后根据topk
参数选择保留前k个词语,如果topk
为-1,则保留所有词语。返回结果:
这两个函数分别是:
get_synonyms(word)
函数:
word_roulette_wheel_selection(word, word_scores)
函数:
word_scores
为空,则直接返回原单词。关键的部分就这些,现在我们来测试看看效果
运行之后可以看到,也需要花60多个小时才能完成测试
我们去看看已有的实验结果
上图是一个越狱失败的案例,可以看到大模型在输出在说明自己不能提供有害的意见
而对于越狱成功的例子,如下所示,就会给出对应的答案
如下的答案是关于入侵数据库的
如下的答案是教育小朋友怎么使用武器的
如下的答案是关于如何pua人类的
在本文中,我们学习了大语言模型的自动化越狱提升生成方法并且验证了其效果。在实际生活中,如果模型被越狱,可能会泄露敏感信息,或被用来散布有害信息,如诈骗、暴力或歧视言论等。这将严重影响社会的正常秩序和公共安全。通过研究越狱攻击,我们可以更好地理解攻击者的策略和方法,从而制定更有效的防御策略,提高模型的安全性和稳定性。
1.https://www.researchgate.net/figure/A-jailbreak-attack-example_fig1_372445019
2.https://m.huxiu.com/article/2860729.html
3.https://www.anthropic.com/research/many-shot-jailbreaking
4.https://zhuanlan.zhihu.com/p/675827506
5.AUTODAN: GENERATING STEALTHY JAILBREAK PROMPTS ON ALIGNED LARGE LANGUAGE MODELS
6.Universal and transferable adversarial attacks on aligned language models
23 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!