共计 9309 个字符,预计需要花费 24 分钟才能阅读完成。
近年来,笔者在国内外 CTF 比赛中见到不少与 AI 相干的题目。有一些是须要选手自行实现一个 AI,来自动化某些操作;有些是给出了一个指标 AI 模型,要求选手进行破解。本文次要议论后者——在 CTF 比赛中,咱们如何坑骗题目给出的 AI?
CTF 中的坑骗 AI 问题个别分成两类:基于神经网络的和基于统计模型的。如果题目要求选手坑骗神经网络,个别会给出白盒的模型(往往是图像分类工作);如果是要求选手坑骗统计学习模型,有些题目会给出白盒的模型参数,也有的提供训练数据集。
咱们先从一道很简略的坑骗统计学习模型看起,来体验这类问题的次要求解过程
坑骗 kNN:[西湖论剑 2020] 指鹿为马
工作指标
有一个 AI 模型,要求选手上传一张图片,与 dear.png 的差别很小,但被 AI 判断为马。
import numpy as np
from PIL import Image
import math
import operator
import os
import time
import base64
import random
def load_horse():
data = []
p = Image.open('./horse.png').convert('L')
p = np.array(p).reshape(-1)
p = np.append(p,0)
data.append(p)
return np.array(data)
def load_deer():
data = []
p = Image.open('./deer.png').convert('L')
p = np.array(p).reshape(-1)
p = np.append(p,1)
data.append(p)
return np.array(data)
def load_test(pic):
data = []
p = Image.open(pic).convert('L')
p = np.array(p).reshape(-1)
p = np.append(p,1)
data.append(p)
return np.array(data)
def euclideanDistance(instance1, instance2, length):
distance = 0
for x in range(length):
distance += pow((instance1[x] - instance2[x]), 2)
return math.sqrt(distance)
def getNeighbors(trainingSet, testInstance, k):
distances = []
length = len(testInstance) - 1
for x in range(len(trainingSet)):
dist = euclideanDistance(testInstance, trainingSet[x], length)
distances.append((trainingSet[x], dist))
distances.sort(key=operator.itemgetter(1))
neighbors = []
for x in range(k):
neighbors.append(distances[x][0])
return neighbors
def getResponse(neighbors):
classVotes = {}
for x in range(len(neighbors)):
response = neighbors[x][-1]
if response in classVotes:
classVotes[response] += 1
else:
classVotes[response] = 1
sortedVotes = sorted(classVotes.items(), key=operator.itemgetter(1), reverse=True)
return sortedVotes[0][0]
def getAccuracy(testSet, predictions):
correct = 0
for x in range(len(testSet)):
if testSet[x][-1] == predictions[x]:
correct += 1
return (correct / float(len(testSet))) * 100.0
def check(pic):
source_p = Image.open('deer.png')
try:
c_p = Image.open(pic)
except:
print("Please upload right picture.")
exit()
diff_pixel = 0
a, b = source_p.size
if c_p.size[0] != a and c_p.size[1] != b:
print("Please upload right picture size("+str(a)+','+str(b)+')')
exit()
for y in range(b):
for x in range(a):
diff_pixel += abs(source_p.getpixel((x, y)) - c_p.getpixel((x, y)))
return diff_pixel
def main():
while 1:
print('-' * 134)
print(''' ____ __ _ _ _ _ _ _ _
| __ \ / _| | | | | | | | | | | | | | |
| |__) |___| |_ ___ _ __ | |_ ___ | |_| |__ ___ __| | ___ ___ _ __ __ _ ___ | |_| |__ ___ | |__ ___ _ __ ___ ___
| _ // _ \ _/ _ \ '__| | __/ _ \ | __|'_ \ / _ \ / _` |/ _ \/ _ \ '__| / _` / __| | __|'_ \ / _ \ | '_ \ / _ \|'__/ __|/ _ \\
| | \ \ __/ || __/ | | || (_) | | |_| | | | __/ | (_| | __/ __/ | | (_| \__ \ | |_| | | | __/ | | | | (_) | | \__ \ __/
|_| \_\___|_| \___|_| \__\___/ \__|_| |_|\___| \__,_|\___|\___|_| \__,_|___/ \__|_| |_|\___| |_| |_|\___/|_| |___/\___|
''')
print('-'*134)
print('\t1.show source code')
print('\t2.give me the source pictures')
print('\t3.upload picture')
print('\t4.exit')
choose = input('>')
if choose == '1':
w = open('run.py','r')
print(w.read())
continue
elif choose == '2':
print('this is horse`s picture:')
h = base64.b64encode(open('horse.png','rb').read())
print(h.decode())
print('-'*134)
print('this is deer`s picture:')
d = base64.b64encode(open('deer.png', 'rb').read())
print(d.decode())
continue
elif choose == '4':
break
elif choose == '3':
print('Please input your deer picture`s base64(Preferably in png format)')
pic = input('>')
try:
pic = base64.b64decode(pic)
except:
exit()
if b"<?php" in pic or b'eval' in pic:
print("Hacker!!This is not WEB,It`s Just a misc!!!")
exit()
salt = str(random.getrandbits(15))
pic_name = 'tmp_'+salt+'.png'
tmp_pic = open(pic_name,'wb')
tmp_pic.write(pic)
tmp_pic.close()
if check(pic_name)>=100000:
print('Don`t give me the horse source picture!!!')
os.remove(pic_name)
break
ma = load_horse()
lu = load_deer()
k = 1
trainingSet = np.append(ma, lu).reshape(2, 5185)
testSet = load_test(pic_name)
neighbors = getNeighbors(trainingSet, testSet[0], k)
result = getResponse(neighbors)
if repr(result) == '0':
os.system('clear')
print('Yes,I want this horse like deer,here is your flag encoded by base64')
flag = base64.b64encode(open('flag','rb').read())
print(flag.decode())
os.remove(pic_name)
break
else:
print('I want horse but not deer!!!')
os.remove(pic_name)
break
else:
print('wrong choose!!!')
break
exit()
if __name__=='__main__':
main()
咱们具体看一遍代码,发现这个 AI 模型是 k- 邻近(k-Nearest Neighbor, KNN),而且还是个 k=1 的情景,且训练集中,鹿和马各只有一张图片。题目将选手的图片读进去,做的事件如下:
- 查看选手上传的图片与 deer 的像素差是否小于 100000。如果超过限度,则报告谬误。
- 求选手图片与 deer 和 horse 的欧几里得间隔。离谁更近,就断定为哪个分类。
- 如果选手图片被断定为马,则选手获胜。
deer 和 horse 都是灰度图,如下:
笔者倡议在做机器学习类 CTF 题的时候,采纳 jupyter notebook 或者 jupyter lab,并用好 matplotlib
来可视化以后的后果。这会大大晋升工作效率。
咱们当初的指标就是在 deer 的根底上进行小幅度批改,使得它与 horse 之间的的欧氏间隔小于其与 deer 的。
尝试:随机噪声
为了结构出非法的图片,咱们须要回去看「批改幅度」的掂量形式。其代码如下:
for y in range(b):
for x in range(a):
diff_pixel += abs(source_p.getpixel((x, y)) - c_p.getpixel((x, y)))
return 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 来实现神经网络。首先是导入数据集:
import torch
import torchvision
import torch.nn as nn
import torchvision.transforms as transforms
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
trans_to_tensor = transforms.Compose([transforms.ToTensor()
])
data_train = torchvision.datasets.MNIST(
'./data',
train=True,
transform=trans_to_tensor,
download=True)
data_test = torchvision.datasets.MNIST(
'./data',
train=False,
transform=trans_to_tensor,
download=True)
data_train, data_test
实现一个 DataLoader,作用是生成随机打乱的 mini batch 用于训练:
train_loader = torch.utils.data.DataLoader(
data_train,
batch_size=100,
shuffle=True)
来看一个 mini batch。
接下来定义网络。咱们采纳一个很原始的模型:将输出的 28*28 的灰度图开展为一维数组,而后通过 100 个神经元的全连贯层,激活函数为 ReLu。接下来再通过 10 个神经元的全连贯层,激活函数为 sigmoid,作为预测值输入。
class MyNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(28*28, 100)
self.fc2 = nn.Linear(100, 10)
def forward(self, x):
x = x.view(-1, 28*28)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
x = torch.sigmoid(x)
return x
net = MyNet()
如果图像中的数字是 c,咱们心愿输入的 10 维向量中仅有第 c 位是 1,其余都是 0。所以咱们采纳穿插熵损失函数以及 Adam 优化器:
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters())
接下来就是训练这个网络。
def fit(net, epoch=1):
net.train()
run_loss = 0
for num_epoch in range(epoch):
print(f'epoch {num_epoch}')
for i, data in enumerate(train_loader):
x, y = data[0], data[1]
outputs = net(x)
loss = criterion(outputs, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
run_loss += loss.item()
if i % 100 == 99:
print(f'[{i+1} / 600] loss={run_loss / 100}')
run_loss = 0
test(net)
def test(net):
net.eval()
test_loader = torch.utils.data.DataLoader(data_train, batch_size=10000, shuffle=False)
test_data = next(iter(test_loader))
with torch.no_grad():
x, y = test_data[0], test_data[1]
outputs = net(x)
pred = torch.max(outputs, 1)[1]
print(f'test acc: {sum(pred == y)} / {y.shape[0]}')
net.train()
来看 5 个 epoch 之后的后果:
咱们训练出了测试准确率 97.89% 的网络。接下来,咱们开始针对网络进行攻打。
实际:坑骗白盒多层感知机
目前网络的所有参数咱们都是晓得的。在 CTF 中,个别会提供训练网络的代码,以及通过 torch.save() 导出的预训练模型,选手通过 model.load_state_dict() 即可导入模型参数。
咱们轻易抉择一个数据,作为原图:
咱们的模型以很强的信念,将其分类为 2。接下来,咱们篡改原图,使得网络将其误分类为 3。过程如下:
- 将图片输出网络,失去网络输入。
- 将网络输入与冀望输入求 loss 值(这里采纳穿插熵)。
- 将图片像素减去本人的梯度 * alpha,不扭转网络参数。
反复以上过程,直到误导胜利为止。代码如下:
def play(epoch):
net.requires_grad_(False) # 解冻网络参数
img.requires_grad_(True) # 追踪输出数据的梯度
loss_fn = nn.CrossEntropyLoss() # 穿插熵损失函数
for num_epoch in range(epoch):
output = net(img)
target = torch.tensor([3]) # 误导网络,使之分类为 3
loss = loss_fn(output, target)
loss.backward() # 计算梯度
img.data.sub_(img.grad * .05) # 梯度降落
img.grad.zero_()
if num_epoch % 10 == 9:
print(f'[{num_epoch + 1} / {epoch}] loss: {loss} pred: {torch.max(output, 1)[1].item()}')
if torch.max(output, 1)[1].item() == 3:
print(f'done in round {num_epoch + 1}')
return
img = origin.view(1, 28, 28)
play(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 选手有所帮忙。