CTF 中如何欺骗 AI

在这里插入图片描述

近年来,笔者在国内外 CTF 竞赛中见到不少与 AI 相关的题目。有一些是需要选手自行实现一个 AI,来自动化某些操作;有些是给出了一个目标 AI 模型,要求选手进行破解。本文主要谈论后者——在 CTF 竞赛中,我们如何欺骗题目给出的 AI?

CTF 中的欺骗 AI 问题一般分成两类:基于神经网络的和基于统计模型的。如果题目要求选手欺骗神经网络,一般会给出白盒的模型(往往是图像分类任务);如果是要求选手欺骗统计学习模型,有些题目会给出白盒的模型参数,也有的提供训练数据集。

我们先从一道很简单的欺骗统计学习模型看起,来体验这类问题的主要求解过程

欺骗 kNN:[西湖论剑2020] 指鹿为马

任务目标

有一个 AI 模型,要求选手上传一张图片,与 dear.png 的差异很小,但被 AI 判别为马。

1import numpy as np 2from PIL import Image 3import math 4import operator 5import os 6import time 7import base64 8import random 9 10def load_horse(): 11 data = [] 12 p = Image.open('./horse.png').convert('L') 13 p = np.array(p).reshape(-1) 14 p = np.append(p,0) 15 data.append(p) 16 return np.array(data) 17 18def load_deer(): 19 data = [] 20 p = Image.open('./deer.png').convert('L') 21 p = np.array(p).reshape(-1) 22 p = np.append(p,1) 23 data.append(p) 24 return np.array(data) 25 26def load_test(pic): 27 data = [] 28 p = Image.open(pic).convert('L') 29 p = np.array(p).reshape(-1) 30 p = np.append(p,1) 31 data.append(p) 32 return np.array(data) 33 34 35def euclideanDistance(instance1, instance2, length): 36 distance = 0 37 for x in range(length): 38 distance += pow((instance1[x] - instance2[x]), 2) 39 return math.sqrt(distance) 40 41 42def getNeighbors(trainingSet, testInstance, k): 43 distances = [] 44 length = len(testInstance) - 1 45 for x in range(len(trainingSet)): 46 dist = euclideanDistance(testInstance, trainingSet[x], length) 47 distances.append((trainingSet[x], dist)) 48 distances.sort(key=operator.itemgetter(1)) 49 neighbors = [] 50 for x in range(k): 51 neighbors.append(distances[x][0]) 52 return neighbors 53 54 55def getResponse(neighbors): 56 classVotes = {} 57 for x in range(len(neighbors)): 58 response = neighbors[x][-1] 59 if response in classVotes: 60 classVotes[response] += 1 61 else: 62 classVotes[response] = 1 63 sortedVotes = sorted(classVotes.items(), key=operator.itemgetter(1), reverse=True) 64 return sortedVotes[0][0] 65 66 67def getAccuracy(testSet, predictions): 68 correct = 0 69 for x in range(len(testSet)): 70 if testSet[x][-1] == predictions[x]: 71 correct += 1 72 return (correct / float(len(testSet))) * 100.0 73 74def check(pic): 75 source_p = Image.open('deer.png') 76 try: 77 c_p = Image.open(pic) 78 except: 79 print("Please upload right picture.") 80 exit() 81 diff_pixel = 0 82 a, b = source_p.size 83 if c_p.size[0] != a and c_p.size[1] != b: 84 print("Please upload right picture size("+str(a)+','+str(b)+')') 85 exit() 86 for y in range(b): 87 for x in range(a): 88 diff_pixel += abs(source_p.getpixel((x, y)) - c_p.getpixel((x, y))) 89 return diff_pixel 90 91def main(): 92 while 1: 93 print('-' * 134) 94 print(''' ____ __ _ _ _ _ _ _ _ 95 | __ \ / _| | | | | | | | | | | | | | | 96 | |__) |___| |_ ___ _ __ | |_ ___ | |_| |__ ___ __| | ___ ___ _ __ __ _ ___ | |_| |__ ___ | |__ ___ _ __ ___ ___ 97 | _ // _ \ _/ _ \ '__| | __/ _ \ | __| '_ \ / _ \ / _` |/ _ \/ _ \ '__| / _` / __| | __| '_ \ / _ \ | '_ \ / _ \| '__/ __|/ _ \\ 98 | | \ \ __/ || __/ | | || (_) | | |_| | | | __/ | (_| | __/ __/ | | (_| \__ \ | |_| | | | __/ | | | | (_) | | \__ \ __/ 99 |_| \_\___|_| \___|_| \__\___/ \__|_| |_|\___| \__,_|\___|\___|_| \__,_|___/ \__|_| |_|\___| |_| |_|\___/|_| |___/\___| 100 ''') 101 print('-'*134) 102 print('\t1.show source code') 103 print('\t2.give me the source pictures') 104 print('\t3.upload picture') 105 print('\t4.exit') 106 choose = input('>') 107 if choose == '1': 108 w = open('run.py','r') 109 print(w.read()) 110 continue 111 elif choose == '2': 112 print('this is horse`s picture:') 113 h = base64.b64encode(open('horse.png','rb').read()) 114 print(h.decode()) 115 print('-'*134) 116 print('this is deer`s picture:') 117 d = base64.b64encode(open('deer.png', 'rb').read()) 118 print(d.decode()) 119 continue 120 elif choose == '4': 121 break 122 elif choose == '3': 123 print('Please input your deer picture`s base64(Preferably in png format)') 124 pic = input('>') 125 try: 126 pic = base64.b64decode(pic) 127 except: 128 exit() 129 if b"<?php" in pic or b'eval' in pic: 130 print("Hacker!!This is not WEB,It`s Just a misc!!!") 131 exit() 132 salt = str(random.getrandbits(15)) 133 pic_name = 'tmp_'+salt+'.png' 134 tmp_pic = open(pic_name,'wb') 135 tmp_pic.write(pic) 136 tmp_pic.close() 137 if check(pic_name)>=100000: 138 print('Don`t give me the horse source picture!!!') 139 os.remove(pic_name) 140 break 141 ma = load_horse() 142 lu = load_deer() 143 k = 1 144 trainingSet = np.append(ma, lu).reshape(2, 5185) 145 testSet = load_test(pic_name) 146 neighbors = getNeighbors(trainingSet, testSet[0], k) 147 result = getResponse(neighbors) 148 if repr(result) == '0': 149 os.system('clear') 150 print('Yes,I want this horse like deer,here is your flag encoded by base64') 151 flag = base64.b64encode(open('flag','rb').read()) 152 print(flag.decode()) 153 os.remove(pic_name) 154 break 155 else: 156 print('I want horse but not deer!!!') 157 os.remove(pic_name) 158 break 159 else: 160 print('wrong choose!!!') 161 break 162 exit() 163 164if __name__=='__main__': 165 main() 166

我们详细看一遍代码,发现这个 AI 模型是 k-邻近(k-Nearest Neighbor, KNN),而且还是个 k=1 的情形,且训练集中,鹿和马各只有一张图片。题目将选手的图片读进去,做的事情如下:

  1. 检查选手上传的图片与 deer 的像素差是否小于 100000。如果超过限制,则报告错误。
  2. 求选手图片与 deer 和 horse 的欧几里得距离。离谁更近,就判定为哪个分类。
  3. 如果选手图片被判定为马,则选手获胜。

deer 和 horse 都是灰度图,如下:

在这里插入图片描述

笔者建议在做机器学习类 CTF 题的时候,采用 jupyter notebook 或者 jupyter lab,并用好 matplotlib 来可视化当前的结果。这会大大提升工作效率。

我们现在的目标就是在 deer 的基础上进行小幅度修改,使得它与 horse 之间的的欧氏距离小于其与 deer 的。

尝试:随机噪声

为了构造出合法的图片,我们需要回去看「修改幅度」的衡量方式。其代码如下:

1for y in range(b): 2 for x in range(a): 3 diff_pixel += abs(source_p.getpixel((x, y)) - c_p.getpixel((x, y))) 4return diff_pixel

它衡量的是图片 A 与 B 之间每个像素点的距离之和。换句话讲,这是曼哈顿距离。笔者遇到的大部分 CTF 欺骗 AI 题目,衡量修改幅度都是采用曼哈顿距离。

这张图片共有 5184 个像素点,也就是说,平均下来,每个像素点允许 19 的偏差。事实上,这是非常宽松的值,我们随便演示一个合法的修改:

在这里插入图片描述

输出的图片就像老式电视一样。那么它能否骗过 AI 呢?

在这里插入图片描述

很遗憾,其与鹿之间的欧氏距离,小于其与马之间的欧氏距离。我们现在要开始反思一个问题:把 100000 个差异值随机平摊到每个像素上,是最好的解法吗?

解法:修改差异大的像素

在二维平面上考虑这个问题。假设我们想让一个点在欧氏距离的衡量下远离 (0, 0),但要保持曼哈顿距离不超过 2。如果选择 (1, 1),则欧氏距离为 sqrt(2);如果选择 (0,2),则欧氏距离可以达到 2,这是更优的选择。

那么,我们相应地猜测:对于本题,我们应该把一些像素点直接改到与 horse 的对应像素相等;其他的像素点可以放弃。而那些应当修改的点,是 deer 与 horse 像素差异最大的点。

在这里插入图片描述

生成了一张很怪的图。来验证一下是否满足要求:

在这里插入图片描述

可见与鹿的距离是 4003,与马的距离是 2538,骗过了 AI。像素差异是 99999,我们成功完成了题目指定的目标。

数学上的证据

我们刚刚基于「差异越大的像素越应该修改」这个猜测,成功地解决了问题。这里给出为什么 it works 的证明。不爱看证明的读者可以跳过。

在这里插入图片描述

在这里插入图片描述

所以,我们从数学上证明了为什么「差异越大的像素点,越值得更改」。并且从数学推导中,我们还可以发现另一个结论:将像素点改成马的对应像素值,并非最优解。要改就改彻底:要么改成 0,要么改成 255。不过本题的像素差异限制 100000 是一个很松的界,所以我们之前不那么优秀的算法也可以成功。

总结

回顾我们的做题过程,我们从一个原图片 X 出发,施加一个很小的扰动向量,获得样本 Y,且 AI 对 Y 的表现与对 X 的表现非常不同。这样的样本被称为「对抗样本」,如何构造高质量的对抗样本、利用对抗样本来改进模型的鲁棒性,是机器学习研究中逐步受到重视的一个方向。

需要注意的是,攻击统计学习 AI 模型,往往需要进行一些数学推导。如果读者有兴趣,笔者推荐了解一下 kNN、kmeans、混合高斯模型等经典的统计学习方法。

欺骗白盒神经网络

概述

神经网络能解决大量传统模型难以解决的问题,近年经历了飞速发展。神经网络一般是由多层神经元构成的,每个神经元有自己的参数。下图是一个简单的神经网络模型(多层感知机):

在这里插入图片描述

图源 IBM。本文假定读者已经对神经网络有一些了解;如果从零学起的话,笔者推荐看一看 3Blue1Brown 的机器学习教程、PyTorch 的官方教程。

以上图描述的神经网络为例。在图像分类任务中,图像的每个像素点被输入到第一层,然后传导到第二层、第三层……直到最后一层。最后一层的每个神经元代表一个分类,其输出是「图像是否属于本类」的打分。我们一般取打分最高的那一类,作为分类结果。

CTF 中的欺骗神经网络题一般如下:给定一个预训练好的分类模型(PyTorch 或者 TensorFlow),再给定一张原图。要求小幅度修改原图,使得神经网络将其误分类为另一个类别。

攻击手段

我们训练神经网络时,一般采用梯度下降的方法。每一轮迭代可以理解为下面的过程:首先输入 X,然后运行 net(X) 获取输出,根据网络输出与期望输出的不同,来反向传播,修改网络模型的参数。

那么,我们现在要攻击这个网络,可以采取什么办法呢?首先还是给网络提供原图 X,得到输出 net(X),接下来,我们根据「网络分类的结果」与「我们想要误导的结果」的差异计算 loss 值,进行反向传播。但是需要注意,我们不修改网络参数,而是将原图减去其梯度。这样迭代若干次,直到成功误导 AI 为止。

下面,我们以识别手写数字(MNIST数据集)的任务为例,从训练网络开始,演示一下攻击方法。

实践:训练神经网络

这里采用 PyTorch 来实现神经网络。首先是导入数据集:

1import torch 2import torchvision 3import torch.nn as nn 4import torchvision.transforms as transforms 5import torch.nn.functional as F 6import numpy as np 7 8import matplotlib.pyplot as plt 9 10trans_to_tensor = transforms.Compose([ 11 transforms.ToTensor() 12]) 13 14data_train = torchvision.datasets.MNIST( 15 './data', 16 train=True, 17 transform=trans_to_tensor, 18 download=True) 19 20data_test = torchvision.datasets.MNIST( 21 './data', 22 train=False, 23 transform=trans_to_tensor, 24 download=True) 25 26data_train, data_test 27

在这里插入图片描述

实现一个 DataLoader,作用是生成随机打乱的 mini batch 用于训练:

1train_loader = torch.utils.data.DataLoader( 2 data_train, 3 batch_size=100, 4 shuffle=True)

来看一个 mini batch。

在这里插入图片描述

接下来定义网络。我们采用一个很原始的模型:将输入的 28*28 的灰度图展开为一维数组,然后经过 100 个神经元的全连接层,激活函数为 ReLu。接下来再通过 10 个神经元的全连接层,激活函数为 sigmoid,作为预测值输出。

1class MyNet(nn.Module): 2 3 def __init__(self): 4 super().__init__() 5 self.fc1 = nn.Linear(28*28, 100) 6 self.fc2 = nn.Linear(100, 10) 7 8 def forward(self, x): 9 x = x.view(-1, 28*28) 10 x = self.fc1(x) 11 x = F.relu(x) 12 x = self.fc2(x) 13 x = torch.sigmoid(x) 14 15 return x 16 17net = MyNet()

如果图像中的数字是 c,我们希望输出的 10 维向量中仅有第 c 位是 1,其余都是 0。所以我们采用交叉熵损失函数以及 Adam 优化器:

1criterion = nn.CrossEntropyLoss() 2optimizer = torch.optim.Adam(net.parameters())

接下来就是训练这个网络。

1def fit(net, epoch=1): 2 net.train() 3 run_loss = 0 4 5 for num_epoch in range(epoch): 6 print(f'epoch {num_epoch}') 7 8 for i, data in enumerate(train_loader): 9 x, y = data[0], data[1] 10 11 outputs = net(x) 12 loss = criterion(outputs, y) 13 14 optimizer.zero_grad() 15 loss.backward() 16 optimizer.step() 17 18 run_loss += loss.item() 19 20 if i % 100 == 99: 21 print(f'[{i+1} / 600] loss={run_loss / 100}') 22 run_loss = 0 23 24 test(net) 25 26def test(net): 27 net.eval() 28 29 test_loader = torch.utils.data.DataLoader(data_train, batch_size=10000, shuffle=False) 30 test_data = next(iter(test_loader)) 31 32 with torch.no_grad(): 33 x, y = test_data[0], test_data[1] 34 35 outputs = net(x) 36 37 pred = torch.max(outputs, 1)[1] 38 print(f'test acc: {sum(pred == y)} / {y.shape[0]}') 39 40 net.train()

来看 5 个 epoch 之后的结果:

在这里插入图片描述

我们训练出了测试准确率 97.89% 的网络。接下来,我们开始针对网络进行攻击。

实践:欺骗白盒多层感知机

目前网络的所有参数我们都是知道的。在 CTF 中,一般会提供训练网络的代码,以及通过 torch.save() 导出的预训练模型,选手通过 model.load_state_dict() 即可导入模型参数。

我们随便选择一个数据,作为原图:

在这里插入图片描述

我们的模型以很强的信心,将其分类为 2。接下来,我们篡改原图,使得网络将其误分类为 3。过程如下:

  1. 将图片输入网络,得到网络输出。
  2. 将网络输出与期望输出求 loss 值(这里采用交叉熵)。
  3. 将图片像素减去自己的梯度 * alpha,不改变网络参数。

重复以上过程,直到误导成功为止。代码如下:

1def play(epoch): 2 net.requires_grad_(False) # 冻结网络参数 3 img.requires_grad_(True) # 追踪输入数据的梯度 4 5 loss_fn = nn.CrossEntropyLoss() # 交叉熵损失函数 6 7 for num_epoch in range(epoch): 8 output = net(img) 9 target = torch.tensor([3]) # 误导网络,使之分类为 3 10 loss = loss_fn(output, target) 11 12 loss.backward() # 计算梯度 13 img.data.sub_(img.grad * .05) # 梯度下降 14 img.grad.zero_() 15 16 if num_epoch % 10 == 9: 17 print(f'[{num_epoch + 1} / {epoch}] loss: {loss} pred: {torch.max(output, 1)[1].item()}') 18 19 if torch.max(output, 1)[1].item() == 3: 20 print(f'done in round {num_epoch + 1}') 21 return 22 23img = origin.view(1, 28, 28) 24 25play(100)

在这里插入图片描述 我们成功地构造出了一个对抗样本,我们人类看显然还是 2,但模型将其识别为 3。至此成功完成任务。对比图如下: 在这里插入图片描述

总结

很多 CTF 欺骗神经网络题目,都可以采用上面这一套代码。训练网络的代码选手不用自己写,只需要导入预训练好的模型即可。在迭代时,选手应该选取合适的学习率 alpha(笔者的代码中是 0.05)、添加一些特殊约束(例如对每个像素的修改距离不能超过特定值)。无论如何,欺骗白盒神经网络的主要思想,往往都是「固定网络参数、通过梯度下降修改原图」。

更进一步的讨论

我们已经一步步完成了对白盒神经网络的欺骗。但日常生活中,很少有神经网络会把自己的参数广而告之,这使得我们不能采用上面的套路去攻击。此外,我们上面生成的那张图片很不「自然」,有大量的背景噪声,而这是正常的数字图片中不会存在的。

关于这些问题,ICLR2018 的一篇论文 Generating natural adversarial examples 可以提供一些启发。该论文提出的方法不要求预先知道网络参数,甚至不要求知道网络模型。而且该方案能生成比较自然的对抗样本,如下所示: 在这里插入图片描述 那么,他们是如何做到的呢?下面简要描述一下原理。首先,通过类似于 CycleGAN 的思路,训练一个从 latent space 到图片的生成器、一个从图片反推 z 的编码器。接下来,把原图编码成向量 z,并在 z 的附近随机选择很多的 z’,利用生成器从这些 z’ 生成图片,然后交给目标模型去判断。如果有图片成功误导了模型,则报告成功。 在这里插入图片描述 论文作者给出了该方法用于 CV 和 NLP 两个领域的成效,并成功地攻击了谷歌翻译。他们的代码开源在 Github 上。

这是一个适用范围比较广的方案,不过笔者认为可能不适合用于 CTF 出题。这是因为训练 GAN 是一件费时费力、且需要机器学习技巧的工作,已经超出了 CTF 一般的考察范畴;且由于出题人模型是黑盒的,可能训练模型技巧较好、使用的判别模型与出题人差异较大的选手反而难以成功。

总而言之,对抗样本是一条很有趣的研究方向。笔者今天介绍了 CTF 竞赛中欺骗 AI 的一般步骤,希望对 CTF 选手有所帮助。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

黑客赚钱的路子有多野?CTF逆向入门指南

1、背景在CTF比赛中,CTF逆向题目除了需要分析程序工作原理,还要根据分析结果进一步求出FLAG。逆向在解题赛制中单独占一类题型,同时也是PWN题的前置技能。在攻防赛制中常与PWN题结合。CTF逆向主要涉及到逆向分析和破解技巧,这也要求有较强的反汇编、反编译、加解密的功底。CTF中的逆向题目一般常见考点1、常见算法与数据结构。2、各种排序算法,树,

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )