RCTF出了个OnePixelAttack的题,很早就看见过这个攻击了,于是尝试一下

One Pixel Attack With Differential Evolution

OnePixelAttack顾名思义,只修改一个像素来骗过Classfier,玩的非常极限,一开始我以为这样的攻击一定离不开梯度,但居然是用了差分演化这一优化算法,可以完全做到BlackBox攻击,只需要知道Classfier给出的Probability与Label即可

幸运的是,使用差分演化的OnePixelAttack在李宏毅的Channel上有过讲解,视频在此,看两遍差分演化的部分后就可以尝试自己搓一个来解题了

题目复述

本次RCTF给出的被攻击的model是ResNet50,分类结果的类别数量高达1000个,需要进行扰动的样本是一张60x60的猫咪图片,需要对其进行一个像素的扰动使其被分类为不是猫咪的类别,以下为原题前端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from flask import Flask, render_template, request
from torchvision.models import resnet50, ResNet50_Weights
from PIL import Image
from flag import flag

def checkImg(Img):
im = Image.open('static/start.png').convert('RGB')
Img = Img.convert('RGB')
if Img.size != (60, 60):
return 0
count = 0
for i in range(60):
for j in range(60):
if im.getpixel((i,j)) != Img.getpixel((i,j)):
count += 1
if count == 1:
return 1
else:
return 0

def divide(img):
# Step 1: Initialize model with the best available weights
weights = ResNet50_Weights.DEFAULT
model = resnet50(weights=weights)
model.eval()

# Step 2: Initialize the inference transforms
preprocess = weights.transforms()

# Step 3: Apply inference preprocessing transforms
batch = preprocess(img).unsqueeze(0)

# Step 4: Use the model and print the predicted category
prediction = model(batch).squeeze(0).softmax(0)
class_id = prediction.argmax().item()
score = prediction[class_id].item()
category_name = weights.meta["categories"][class_id]
return category_name,score


app = Flask(__name__)
@app.route('/', methods=['POST', 'GET'])
def welcome():
return render_template("index.html")

@app.route('/upload', methods=['POST', 'GET'])
def upload():
if request.method == 'POST':
f = request.files['file']
im = Image.open(f)
if checkImg(im) == 0:
return render_template('upload.html', error="image format error! the image size must be 60 x 60 and you can only change one pixel!")
category_name,score = divide(im)
if category_name == 'tabby' or "cat" in category_name:
return render_template('upload.html', res=category_name + " " + str(score))
else:
return render_template('upload.html', flag=flag)
return render_template('upload.html',error='please start attack!')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

需要进行扰动的样本:

差分演化

首先认识差分演化,简称DE

DE(func, cutoff, popSize, mut, CR, maxIter)

观察上述函数参数:

  • func:目标函数
  • cutoff:阈值
  • popSize:族群大小
  • mut:进化参数
  • CR:杂交参数
  • maxIter:最大迭代次数

我们期望上述函数在maxIter内找到一个向量X,使func(X)的值大于或小于(这取决于结果的需求)cutoff

具体流程

在此简述DE的流程,以免遗忘

现有一名为func的黑盒/白盒函数,其接受向量X作为参数

现使用DE()求使得func()尽可能小的向量

  • 随机初始化popSize个向量作为族群pop,这些向量符合func对输入向量的要求
  • 当族群pop中不存在任何一个向量x满足func(x)<cutoff
    • 使族群pop中的每一个向量x作为杂交父向量之一:
      • 随机选取3个pop中的向量a,b,c,且他们与x不相等
      • 令另一杂交父向量mutant=a+mut*(b-c)
      • 随机生成长度与x相同的仅包含True,False的向量,每个元素为True的可能性为CR,称此01向量为crossOver
      • 使mutantx进行杂交,生成的杂交子向量为child=x*(1-crossOver)+mutant*crossOver
      • 如果child中的元素越界,有几种处理办法:
        • Clip(较为保守的办法,会一定程度降低多样性)
        • 取余(较为中庸的办法)
        • 取随机数(较为激进的办法,增加多样性但可能导致混乱程度增加,更难收敛)
      • func(child)<func(x)
        • 更新族群x=child
  • 返回满足func(x)<cutoff的向量x

调参

可以先参考这篇挂豆丁的中文论文,里面介绍了各个参数的效果以及如何改进,但我觉得implement起来性价比最高的就是动态调整的F和随机生成的CR

在此总结一下:

  • F∈[0,2],最好设置成动态的
  • CR∈(0,1)
  • popSize:通常在50以内,但理论上越大则越有可能找到全局最优解,但太大了会导致收益不足以弥补计算速度过慢带来的损失
  • maxIter:如果采用动态F,则需要斟酌一下,例如我的F就与maxIter有关F=alpha*(np.exp(maxIter/(maxIter+iterCnt))-1)

Copilot带来的问题

在最开始用魔改版Copilot生成的DE解决这个题目时,经常遇到的一个问题:在某一时刻(通常是演化的初期),突然整个族群都进行了一次演化,即在一次进化中,整个族群都更新了,但在这之后,就不再有任何一个向量在演化中改变

后记:对于上述现象,其实就是Copilot写的比较拉,在多次魔改后此bug凭空消失。感觉是因为Copilot写的代码中,对于待优化的函数定义模糊,变量关系混乱,参考的变量过于单一。详细地说,就是ResNet中的类别数量高达1000个,其Label中带有"cat"的就有十来个(不同品种的猫),也许在某一时刻,一个噪音成功更改了Label,但只是把Label从最初的Persian Cat改成了另一种Cat,这个成功的噪音顺利成为了最佳噪音;因为需要计算最佳噪音的fitness和预测结果,导致原本是Persian Cat的classID被改成另一种猫的classID;这一更改使得族群的fitness值整体大幅下降,整个族群或者绝大多数向量都接受了新的child,导致我们看到的突然大规模更新的现象,而因为另一种猫的概率本身就不高,梯度过于平缓甚至消失,DE在一片大平地上难以发挥作用,导致我们看到的在大规模更新后再无更新的现象

(找这个bug真的给我找麻了,虽然不是有意修复了bug,但我觉得bug成因应该是这样的

虽然这个bug是Copilot造成的,但我之前也google了相关问题,找到了On_Stagnation_Of_The_Differential_Evolution_Algorithm,解释了一下为什么DE算法中存在族群停止演化的现象

感觉他说了挺多的,又感觉他什么也没说,总之在没有我这个bug的情况下,就是扩大种群规模,提供更广泛的多样性,就能使得停滞的概率减小,而停滞的成因则是陷入局部最优解(乐

我还去问了ChatGPT,都是AI应该比较互相了解(乐:

虽然点了很多次Try Again也没找出族群突然全体突变的bug,但倒是给我把另一个Copilot写的bug给我找出来了,就是target vector居然是随机取的而不是挨个儿来,毕竟这一堆东西里面random用的挺多的,我还真没看出来,于是立刻改掉然后做成标准的DE了

输出

说实话,特别是这种看运气的程序,盯着输出终端等success就很紧张,所以记录几段输出,见证一下

通过以上各个参数的迭代次数可见,族群大小对这个算法的收效甚微,只要不是太小的族群,在算法正确的情况下一般是没有问题的,比如上述的不同族群大小中,popSize=20时两三秒就可以进行一次迭代,而popSize=200时一次迭代就要几分钟,故而盲目追求大族群是得不偿失的,不如用小族群多跑几遍

然后是这个输出的结果,每次基本都是在猫耳朵的位置修改像素,得到goldfish的label,意味着Persian Cat的决策范围边上就是Goldfish,而且距离之近或者说梯度之大只需要一个像素就可以扰动成功

性能比较

copy了一份论文link的github上的代码,进行了一下性能的对比,这种带运气成分的算法需要大量统计才能做出比较,直观感觉上来说,相同种群大小下,他的一次迭代比我的慢得多,他的代码和我的一样有运气好几次迭代直接出和运气差迭代半天一直不出的情况,但是从原理上说,只要一直挂着跑,失败了自动重启继续跑,都肯定可以跑出来的

exp

Comments

⬆︎TOP