越狱大语言模型-通过自动化生成隐蔽提示

本文将会分析并实践如何自动化生成越狱提示,攻击大语言模型

前言

大家如果对新技术保持一定敏感度的话,应该知道,自从ChatGPT发布之后,就受到了各类安全攻击。

比如去年年底,在业务中断期间,ChatGPT产品界面出现横幅消息警告用户:“我们正在经历异常升高的访问请求。请耐心等待,我们正在努力扩展我们的系统。”

image-20240516134124722.png
而事实上,除了传统安全中的DDoS攻击、中间人攻击之外,以ChatGPT为首的大语言模型还面临着一种新型的攻击方法,就是越狱攻击。

我们可以看下图的这个示例

image-20240516134432565.png
在左边,如果直接询问大语言模型,如何创建并分发恶意软件,那么它可能会说‘对不起,balabala’,但是通过一定的方法,在我们给模型输入的内容中加一些文字扰动,那么如右图所示,模型就会回答我们这个不安全的问题。

再比如说,可以通过先问LLM几十个危害性较小的问题,说服它告诉我们一些危害性较大问题的答案,比如“如何制造炸弹”。也就是说,如果是第一个问题,它可能会拒绝回答或答错,但如果是第一百个问题,它就可能会绕开防御措施、然后回答,如下所示

image-20240516134711318.png
所以呢,其实总结来说,越狱攻击的目的是绕过模型开发者设置的安全防护措施,诱使模型产生有害的反应。这种攻击利用了模型的长上下文和假对话来引导模型产生有害的反应。具体来说,通过在特定配置中包含大量文本,这种越狱技术可以迫使LLM产生潜在的有害响应(尽管它们经过训练不会这样做)。

不过,我们上面提到的这些越狱技术要么存在可扩展性问题,也就是说其中攻击严重依赖手动制作提示,要么存在隐蔽性问题,因为很多已有的攻击方法依赖于 token-based 的算法来生成通常无语义意义的提示,这使得它们容易通过基本困惑度测试来检测出来。

所以我们在这篇文章中将会介绍一种可以自动地生成隐蔽的越狱提示的方法。

形式化威胁模型

越狱攻击的主要目标是破坏模型开发人员施加的 LLM 的安全对齐或其他约束,迫使它们用正确答案响应敌手的恶意问题,而不是拒绝回答

那么我们考虑一组表示如下的恶意问题

image-20240516135148472.png
攻击者详细阐述了这些问题,用如下的越狱提示

image-20240516135209745.png
最后得到一个组合的输入集,如下所示

image-20240516135225877.png
当这个输入集T呈现给大语言模型M的时候,它会输出一组响应

image-20240516135311892.png
越狱攻击的目标是确保 R中的响应主要是与Q 中的恶意问题密切相关的答案,而不是与人类价值观对齐的拒绝消息。

那么很明显,为响应单个恶意问题设置特定的目标是不切实际的,因为为一个给定的恶意查询确定一个适当的答案是很有挑战性的,并可能损害对其他问题的普遍性。因此,一个常见的解决方案就是将目标响应指定为肯定的,例如以"Sure, here is how to ..." 为开头的答案。通过将目标响应锚定到具有一致开头的文本上,用条件概率表示用于优化的攻击损失函数。这种方法已经被证明是有效果的。

对于这种方法而言,给定一系列连续的token

image-20240516135518898.png
模型会估计下一个标记x(m+1)在词汇表上的概率分布为:

image-20240516135548737.png
越狱攻击的目标是提示模型以产生以特定单词开头的输出,也就是如下token

image-20240516135629182.png
那么给定输入

image-20240516135708864.png
它的token是

image-20240516135725261.png
我们的目标是优化越狱提示Ji来影响输入的token,从而最大化概率:

image-20240516135809845.png
以上就是越狱大语言模型过程的形式化表示。

遗传算法

遗传算法(GAs)是一类受自然选择过程启发的进化算法。这些算法作为模拟自然演化过程的优化和搜索技术。GA 从候选解决方案的初始种群开始(即种群初始化)。基于适应度评估,该种群通过特定的遗传策略,例如交叉和突变,来演变。当满足终止标准时该算法结束,终止标准可能是达到指定的世代数或达到所需的适应度阈值。在我们本文要介绍的方法中就会用到遗传算法。

种群初始化

初始化策略在遗传算法中起着至关重要的作用,因为它可以显著影响算法的收敛速度和最终解的质量。为了设计有效的初始化策略,我们需要注意两个关键考虑因素:

1)原型手工制作的 jailbreak 提示已经在特定场景中展示了功效,使其成为有价值的基础;因此不需要偏离它太远。

2)确保初始种群的多样性至关重要,因为它可以防止过早收敛到次优解并促进对解空间的更广泛探索。保留原型手工制作的越狱提示的基本特征也促进了多样性,我们使用大语言模型作为负责修改原型提示的代理,如下算法所示。该方案背后的基本原理是,LLM提出的修改可以保留原始句子的固有逻辑流和含义,同时引入单词选择和句子结构的多样性。

image-20240516140134416.png

适应度评估

在遗传算法中,Fitness Evaluation(适应度评估)是评估个体性能的重要环节。适应度函数通常根据问题的特定要求来定义,它需要根据个体的特征值(基因)计算出一个数值作为评估指标。适应度函数的选取直接影响到遗传算法的收敛速度以及能否找到最优解。因为遗传算法在进化搜索中基本不利用外部信息,仅以适应度函数为依据,利用种群每个个体的适应度来进行搜索。适应度函数的复杂度是遗传算法复杂度的主要组成部分,所以适应度函数的设计应尽可能简单,使计算的时间复杂度最小。

由于 jailbreak 攻击的目标可以表述为下式

image-20240516140340089.png
我们可以直接使用一个函数来计算这种似然,来评估遗传算法中个体的适应度

我们使用如下的对数似然作为损失函数,即给定一个特定的jailbreak提示Ji,损失可以通过以下方式计算:

image-20240516140415479.png

遗传策略

现在我们可以进一步设计遗传策略进行优化。遗传策略的核心是设计交叉和变异函数。

我们可以使用基本的多点交叉方案作为第一种遗传策略。

但是这还是不够的。

文本数据的一个显著特点是其分层结构。

比如说,文本中的段落通常在句子之间表现出逻辑流,并且在每个句子中,单词选择决定了它的含义。因此,如果我们将算法限制为 jailbreak 提示的段落级交叉,我们会上将我们的搜索限制在一个单一的分层级别,从而忽略广阔的搜索空间。

为了利用文本数据的固有分层结构,我们的方法将 jailbreak 提示视为一个段落级种群的组合,即不同的句子的组合,而这些句子又由句子级种群组成,例如不同的单词。

在每次搜索迭代中,我们首先探索句子级种群的空间,例如单词选择,然后将句子级种群集成到段落级种群中,并在段落级空间(例如句子组合)上开始搜索。

这种方法就是第二种策略。

段落层面:选择、交叉和变异

给定初始化的种群,可以首先根据下面的式子

image-20240516140908738.png
评估种群中每个个体的适应度得分。适应度评估后,下一步是选择个体进行交叉和突变。假设我们有一个包含N个提示的种群,给定一个精英率alpha,首先允许具有最高适应度分数的前Nxalpha个提示直接继续下一次迭代,而无需任何修改,这确保了适应度分数始终下降。

然后,为了确定下一次迭代所需的剩余 N-Nxalpha个提示,我们首先使用一种选择方法来基于它的分数选择提示。在这里,提示Ji的选择概率是通过如下式子确定的

image-20240516141047649.png
在选择过程之后,我们将有 N-Nxalpha个“父提示”准备好进行交叉和突变。然后,对于这些提示中的每一个,我们以一定概率使其与另一个父提示执行多点交叉。多点交叉方案可以概括为在多个断点之间交换两个提示的句子。随后,交叉后的提示将以另一个特定的概率进行突变。

这一过程对应的伪代码如下

image-20240516141156266.png

句子级:动量词评分

在句子级别,搜索空间主要围绕单词的选择。在使用之前介绍的适应度分数对每个提示进行评分后,我们可以将适应度分数分配给存在于相应提示中的每个单词。由于一个词可能出现在多个提示中,我们将平均分数设置为最终指标,以量化每个词在实现成功攻击中的重要性。为了进一步考虑优化过程中潜在的适应度得分的不稳定性,我们将基于动量的设计纳入单词评分,即根据当前迭代中得分和上一次迭代得分的平均数来决定单词的最终适应度得分。如下算命所示,在过滤掉一些常用词和专有名词(第 4 行)后,我们可以得到一个词得分字典(第 6 行)

image-20240516141256924.png
从这个字典中,我们选择前K个分数的单词来替换其他提示中的近义词,如下算法所示

image-20240516141318219.png

终止条件

为了确保这个策略的有效性和高效率,我们采用的终止条件结合了一个最大迭代测试和拒绝信号测试。如果算法已经耗尽了最大的迭代次数,或者没有再在大模型的响应的前K个响应中检测到一些包含拒绝的关键字,那么将终止并返回当前具有最高适应度分数的最佳 jailbreak 提示。

策略1代码实现及测试

先来看第一种简单的策略

image-20240516142646300.png

image-20240516142657492.png

image-20240516142714130.png

image-20240516142724101.png
这里先分析一下大概的几部分:

  1. 参数解析

    • 脚本开始时使用get_args()函数解析命令行参数。
    • 根据参数设置设备为CUDA(如果可用),其中X是指定的设备编号。
  2. 模型路径

    • 定义了一个字典model_path_dicts,将模型名称映射到对应的路径。
    • 根据参数args.model选择模型路径。
  3. 初始化设置

    • 从指定的文件args.init_prompt_path中读取初始提示。
    • 根据解析的参数初始化各种参数,如num_stepsbatch_sizenum_elites等。
    • 根据指定模型设置了一系列测试前缀,生成的响应必须以这些前缀开头。
  4. 模型加载

    • 使用load_model_and_tokenizer()加载指定的模型和分词器。
    • 根据选择的模型加载对话模板。
  5. 数据加载

    • 从由args.dataset_path指定的CSV文件加载数据集。
    • 遍历数据集,处理每个项目。
  6. 攻击循环

    • 对于数据集中的每个项目:

      • 初始化一些日志信息。
      • 根据数据集中的当前项目设置用户提示和目标。
      • 循环指定次数(num_steps),尝试生成对抗性响应。
      • 在每个步骤内,使用get_score_autodan()计算不同候选响应的得分。
      • 根据这些分数选择最佳响应,并检查是否符合攻击标准。
      • 如果成功,则跳出循环。
      • 记录每个步骤的各种信息,如损失、成功状态、响应等。
      • 将结果保存到JSON文件中。
  7. 输出

    • 最后,将收集到的有关数据集中每个项目的信息保存到特定目录中的JSON文件中。

这段代码实现了一种对语言模型的对抗性攻击策略,可能旨在生成看似合法但包含有害或意外内容的响应。攻击被构造为遗传算法,其中参数如交叉、变异和精英指导对对抗性示例的搜索。

遗传算法的部分主要集中在以下几个步骤中:

  1. 候选响应生成

    • 在每个迭代步骤中,通过调用autodan_sample_control()函数生成一组新的候选响应。
    • 这些候选响应是通过对当前候选响应进行交叉、变异等操作生成的。
  2. 得分计算

    • 使用get_score_autodan()函数计算每个候选响应的分数。
    • 这些分数用于评估候选响应的质量。
  3. 选择最佳响应

    • 根据得分,选择最优的候选响应作为下一步的基础。
    • 这通常是得分最低的候选响应,因为攻击的目标可能是尽量降低模型的损失。
  4. 终止条件

    • 在达到特定条件(例如成功攻击或达到最大迭代次数)时,终止算法。

整个遗传算法的核心部分可能在一个循环内,每次循环迭代代表算法的一次进化。在这段代码中,这部分可能位于主循环中,该循环遍历了一系列迭代步骤,每一步都尝试改进当前的候选响应。

测试

这里我们以advbench中的500多中越狱行为为例,进行测试,如下所示,可以看到要完整测试完的话需要花费60多个小时

image-20240516143059302.png
直接来看已有的结果,基本的例子如下所示

image-20240516143232198.png
我们来些越狱成功的

image-20240516143441316.png
上图中Passed为True表示越狱成功,而我们圈起来的第二个框则表示,输出的是越狱内容,这里就是告诉我们如何制作恶意代码

再来看一个例子

image-20240516143553979.png
这个例子则是针对传播致命疾病的错误信息的越狱成功的例子,后续会输出有关如何传播致命疾病的错误信息的步骤

当然,更多的还是这种失败的例子

以如下的例子所示

image-20240516143706741.png
在越狱失败时,经常会回复我们它不能满足我们的请求balabala

策略2代码实现及测试

先来分析对应的关键代码

image-20240516143834656.png

image-20240516143847439.png

image-20240516143859815.png

image-20240516143910550.png
我们来解释下主要组成部分和操作:

  1. 参数解析:脚本开始时会解析命令行参数,可能包括指定要使用的模型(args.model)、在哪个设备上运行模型(args.device)、各种文件的路径,包括初始提示的路径(args.init_prompt_path),以及控制对抗样本生成过程的其他参数。

  2. 模型加载:脚本使用load_model_and_tokenizer函数从指定路径(model_path)加载语言模型和分词器。

  3. 数据准备:加载一个数据集(harmful_data)从一个CSV文件,并为迭代准备数据。

  4. 样本生成循环:对于数据集中的每个项目,进入一个循环(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)以及项目的内容(gt,即目标和目标文本)。

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_prompttarget变量中。

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"]["

这里关键的变异代码在如下代码中

image-20240516144541987.png
这段代码采用了遗传算法的思想,称之为autodan_sample_control_hga

  1. 重新排列分数列表

    • 首先,将分数列表取负数,这是因为通常在遗传算法中,我们希望最小化目标函数,而这里的分数越低越好,所以将其取负数。
  2. 精英选择

    • 通过对负分数列表进行排序,找到得分最高的一些对抗后缀,将其作为精英个体,不经过变异和交叉操作,直接传递到下一代。
  3. 构建词典

    • 调用construct_momentum_word_dict函数,基于当前的对抗后缀和它们的得分构建一个词典,该词典用于后续的词替换操作。这个词典可能记录了每个词在所有对抗后缀中出现的频次或其他信息。
  4. 词替换

    • 使用基于轮盘赌选择的方式(roulette wheel selection),从非精英个体中选择一些作为父代进行交叉和变异操作,产生新的后代。这里的apply_word_replacement函数执行了词替换操作,根据词典中记录的信息,在父代对抗后缀中进行词替换。
  5. GPT变异

    • 调用apply_gpt_mutation函数,对词替换后的后代进行GPT变异操作,可能包括在生成的文本中插入新的词语、调整词语的顺序或其他操作。
  6. 组合下一代

    • 将精英个体与经过变异和交叉操作后得到的后代组合在一起,形成下一代的对抗后缀集合。
  7. 断言

    • 确保生成的下一代对抗后缀的数量符合指定的批次大小。

最终,这个函数返回了新一代的对抗后缀集合和更新后的词典。

image-20240516144707711.png
这段代码实现了一个函数construct_momentum_word_dict,用于构建一个词典,该词典记录了对抗后缀中每个词语的得分:

  1. 初始化

    • 函数接受了四个参数:word_dict用于记录词语得分的字典,control_suffixs是一组对抗后缀,score_list是对应的得分列表,topk表示要保留得分最高的前k个词语,默认为-1,表示保留所有词语。
  2. 停用词和保留词集合

    • 定义了一个停用词集合stop_words,用于过滤掉常见的无意义词语。同时,定义了一个保留词集合T,其中包含了一些特定的词,如模型名称等。
  3. 验证输入

    • 确保control_suffixsscore_list具有相同的长度,否则会引发值错误。
  4. 词语得分统计

    • 遍历control_suffixs和对应的score_list,对每个对抗后缀中的词语进行处理。首先,使用NLTK进行词语分词,并过滤掉停用词和保留词集合中的词语。然后,将词语与其对应的得分关联起来,存储在word_scores字典中。
  5. 计算平均得分

    • 对每个词语在所有对抗后缀中的得分进行平均计算,得到词语的平均得分。
  6. 更新词典

    • 将词语的平均得分更新到word_dict中,如果词语已经存在于word_dict中,则将其当前得分与新计算得分的平均值进行加权平均,否则直接添加到word_dict中。
  7. 排序和筛选

    • word_dict根据词语的得分进行降序排序,然后根据topk参数选择保留前k个词语,如果topk为-1,则保留所有词语。
  8. 返回结果

    • 返回得分最高的词语及其得分构成的词典。

image-20240516144750081.png
这两个函数分别是:

  1. get_synonyms(word) 函数:

    • 这个函数用于获取给定单词的同义词列表。
    • 它首先遍历了WordNet中指定单词的所有同义词集合。
    • 然后对于每个同义词集合中的词条,它获取了其所有的lemma(词条的变体),并将它们添加到一个集合中,最终返回一个包含所有同义词的列表。
  2. word_roulette_wheel_selection(word, word_scores) 函数:

    • 这个函数是一个基于轮盘赌选择(roulette wheel selection)的算法,用于根据词语的得分选择词语或其同义词进行替换。
    • 如果word_scores为空,则直接返回原单词。
    • 否则,它首先将所有词语的得分进行了调整,使得最小的得分为0,并计算了所有词语得分的总和。
    • 然后,它在0到总得分之间随机选择一个值,根据该值在调整后的得分中选择对应的词语。
    • 最后返回选中的词语作为替换结果,如果原词是大写开头的,则返回的同义词也将大写开头。

关键的部分就这些,现在我们来测试看看效果

代码测试

运行之后可以看到,也需要花60多个小时才能完成测试

image-20240516144937891.png
我们去看看已有的实验结果

image-20240516145022961.png
上图是一个越狱失败的案例,可以看到大模型在输出在说明自己不能提供有害的意见

而对于越狱成功的例子,如下所示,就会给出对应的答案

如下的答案是关于入侵数据库的

image-20240516145149630.png
如下的答案是教育小朋友怎么使用武器的

image-20240516145316080.png
如下的答案是关于如何pua人类的

image-20240516145420079.png

总结

在本文中,我们学习了大语言模型的自动化越狱提升生成方法并且验证了其效果。在实际生活中,如果模型被越狱,可能会泄露敏感信息,或被用来散布有害信息,如诈骗、暴力或歧视言论等。这将严重影响社会的正常秩序和公共安全。通过研究越狱攻击,我们可以更好地理解攻击者的策略和方法,从而制定更有效的防御策略,提高模型的安全性和稳定性。

参考

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

  • 发表于 2024-05-31 10:00:02
  • 阅读 ( 4796 )
  • 分类:其他

0 条评论

请先 登录 后评论
elwood1916
elwood1916

23 篇文章

站长统计