2020-XNUCA Writeup(MISC)

2020-XNUCA Writeup(MISC)

十月 31, 2020 (Updated: )

pg: 题目还行,感觉做的时候挺有意思的,就是自己做题时太憨憨导致全部错失一血呜呜呜~~~

torch model

题目要求环境: python==3.7.7 、torch == 1.5.0

给了一个神经网络学习的模型训练python脚本(ipynb格式),直接vscode打开可以自动装相关环境一键查看,vscode还是香的。

查看ipynb可以知道该模型的作用是识别题目给出的图片为flagflag长度和sha256已知。模型数据被保存为model_state_dict.pt,并且中间有一段数据用随机数据进行了覆盖修改,查看题目给出的diff.png以及torch.save的源码可知:修改了torch保存模型数据信息的数据段,而且将模型数据字典中的键值名乱序保存进文件。pt文件中的数据用pickle打包。

至此题目信息查看完毕,题目解法为修复pt文件数据,用torch读取模型识别flag。
这里我们用两个同长度的字符串代替原flag,重新训练两个模型,并将其保存为两个正常的pt文件。然后对比两个pt文件保存模型数据信息数据段的十六进制,发现除了serialized_storage_keyskey的信息,其余结构基本一致。所以修复只需要将其中的key的键值名替换为题目文件中的key的键值名。对比图如下:

20201102012727

提取题目模型文件和我们刚保存的模型文件中的serialized_storage_keys数据,并解包替换重新打包写回文件,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
with open("./model_state_dict.pt",'rb') as model1:
with open("./model_state_dict_test.pt",'rb') as model2:
with open("./model_state_dict_flag.pt",'wb') as model3:
model3.write(model1.read(0x89))
model2.read(0x89)
tmp_flag_obj = model1.read(0x4ef-0x89)
tmp_test_obj = model2.read(0x4e5-0x89)
tmp_flag_keys = model1.read(0x5c9-0x4ef)
tmp_test_keys = model2.read(0x5b5-0x4e5)
tmp1 = pickle.loads(tmp_flag_keys,encoding='utf-8')
tmp2 = pickle.loads(tmp_test_keys,encoding='utf-8')
print(tmp1)
print(tmp2)

for i in range(len(tmp1)):
tmp_test_obj = tmp_test_obj.replace(bytes(tmp2[i],encoding='utf-8'),bytes(tmp1[i],encoding='utf-8'))

model3.write(tmp_test_obj)
model3.write(tmp_flag_keys)

tmp = model1.read(1)
while tmp != b'':
model3.write(tmp)
tmp = model1.read(1)

接下来是踩坑时刻。用此脚本复现该题目的话会发现修复之后的pt文件依旧不能被加载。

注意看脚本可以发现,pt文件数据虽然全是用pickle打包的,但是对于模型的obj数据却没有选择解包替换,而是直接对被打包的数据进行了“带包”替换。这里如果有仔细分析torch.save的源码的话,就会知道,官方对于obj的数据自定义了一套pickler检查规则,这里是因为结构信息几乎完全一致,所以就没有去读官方定义的那套规则。不过这样的话就需要注意修改obj集合数据中打包的小结构体的一个关键数据:

20201102014247

图中选中部分是为一个被打包的小结构体,每一个小结构体都是obj集合中的一个对象。结构体的magic header是单个字节的十六进制\x71,其后紧跟着的一个字节是这个结构体在obj集合中的顺序序号,上图选中的即为\x2e第46个结构体。这里注意下下图红色标记字节,从该字节开始的四个字节为pickle打包数据中一个集合内相关对象的名字的长度,小端序。这里是\x0D\x00\x00\x00,即长度为13,但是13其实是我生成新模型数据文件时新模型内的key的名字长度,如果去观察偏移\x4ef后的题目模型文件的key数据,会发现这些9452xxxx的键值名称长度其实都为14,这里因为数量较少,所以我手动修复了。此时pt文件中的模型关键信息已经修复完毕,torch已经可以解析加载pt文件。
(pg:这个长度不一样就离谱,第一天下午看题的时候一个下午都没发现自己把14数成13,结果别的师傅都把题刷烂了自己还纳闷题目文件为啥比自己生成的文件大了20字节,一直以为环境没配好反复配了几个小时环境,惨惨emmm)

20201102014936

但是,还记得题目图片中的random嘛,这个东西还没用呢,还有坑。此时直接加载模型会发现如下报错:

20201102015948

这里就是乱序储存导致torchload模型训练数据时,serialized_storage_keys中的每个键值对应的那一部分模型数据长度与现在实际load解析得到的长度不一致导致的,这里它直接给出了应该是什么长度的键值。

RuntimeError: storage has wrong size: expected 120 got 10

120即为第一部分的长度,但此时第一个键值对应长度为十,这里可以直接去torch.load的源码那里,用一个print输出它解析到的obj信息,从而得到每个数字键值名对应的对象的数据长度,然后将pt文件中\x4ef偏移后的keys集合数据重新排序复写,这里给出脚本,因为就十条,所以没搞自动化嘤嘤嘤QAQ

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
with open("./model_state_dict_flag.pt",'rb') as f:
with open("./tmp.pt",'wb') as o:
o.write(f.read(0x4ef))
tmp = f.read(0x5c9-0x4ef)
tmp = pickle.loads(tmp,encoding="utf-8")
tmp = sorted(tmp)
tmp.remove('94521743421392')
tmp.insert(0,'94521743421392')
tmp.remove('94521743421488')
tmp.insert(1,'94521743421488')
tmp.remove('94521743257408')
tmp.insert(2,'94521743257408')
tmp.remove('94521743706048')
tmp.insert(3,'94521743706048')
tmp.remove('94521742563680')
tmp.insert(4,'94521742563680')
tmp.remove('94521716638368')
tmp.insert(5,'94521716638368')
tmp.remove('94521743430240')
tmp.insert(6,'94521743430240')
tmp.remove('94521743916656')
tmp.insert(7,'94521743916656')

tmp = pickle.dumps(tmp)
o.write(tmp)
tmp = f.read(1)
while tmp != b'':
o.write(tmp)
tmp = f.read(1)

至此,pt文件彻底修复,直接load然后加载图片即可获取flag

catchthecat

考察数据结构算法。(ps:千万别乱找网上的轮子!自己造的真香!)
两个脚本解决,第一个遍历迷宫获取迷宫地图,第二个求迷宫两点最短路径(此处顺便还要看点运气emmm)

首先题目源码给出,根据源码可知地图除了墙还有炸弹,遇见炸弹直接game over。所以写探索迷宫的算法时要考虑到重连问题。这里我用pwntools + 深度优先算法遍历迷宫数据。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# -*- coding:utf-8 -*-
from pwn import *
import random
from PIL import Image

NOTHING = 0
WALL = 1
BOMB = 2
CAT = 3
PERSON = 4
directions = ['u', 'd', 'l', 'r']
dic = {'u':'d','d':'u','r':'l','l':'r'}

# 在迷宫中判断下一个节点是否可以去或者探索未知节点
def turn(game,map,direction,x,y):

status = False

if direction == "u":
tmp_x = x - 1
tmp_y = y

elif direction == "d":
if x == len(map) - 1:
map.append([0 for i in range(len(map[0]))])
tmp_x = x + 1
tmp_y = y

elif direction == 'l':
tmp_y = y - 1
tmp_x = x

else:
if y == len(map[0]) -1:
for i in map:
i.append(0)
tmp_x = x
tmp_y = y + 1

if map[tmp_x][tmp_y] == NOTHING:
try:
game.sendline(direction)
sleep(0.1)
infor = game.recv(timeout = 1)
except:
print("recvError!!!")

if "WALL" in infor:
map[tmp_x][tmp_y] = WALL
elif "BOMB" in infor:
map[tmp_x][tmp_y] = BOMB
x = tmp_x
y = tmp_y
status = "BOOM"
elif "Caught" in infor or "OK" in infor:
map[tmp_x][tmp_y] = NOTHING
x = tmp_x
y = tmp_y
status = True
else:
print("error or flag: ",infor)
pause()
return map,x,y,status

elif map[tmp_x][tmp_y] == WALL:
return map,x,y,status

elif map[tmp_x][tmp_y] == BOMB:
return map,x,y,status

else:
print("ukown",map[tmp_x][tmpp_y],tmp_x,tmp_y)
return map,x,y,status

'''
# 加载已保存的地图数据 与 初始化地图 二选一
map = [[] for i in range(60)]

pic = Image.open('./map.png')
w,h = pic.size
for i in range(h):
for j in range(w):
pixel = pic.getpixel((j,i))
if pixel == (0,0,0) or pixel == (255,0,0):
map[i].append(WALL)
else:
map[i].append(NOTHING)

'''
# 初始化迷宫地图 与 加载保存一半的地图信息 二选一
map = [[1,1],[1,0]]
# 初始化栈
stack = [[1,1,['r','d'],]]

while True:
game = remote("59.110.63.160", 40001)

x,y = 1,1 ; exit = 0

try:
while stack:
pos = stack[-1]
if [x,y] == pos[:2]:
tmp_directions = []
tmp_directions.extend(pos[2])
for direction in pos[2]:
map,tmp_x,tmp_y,status = turn(game,map,direction,x,y)
tmp_directions.remove(direction)

if status == True:
x,y = tmp_x,tmp_y
stack[-1][2] = tmp_directions
tmp = []
tmp.extend(directions)
tmp.remove(dic[direction])
stack.extend([[x,y,tmp,dic[direction]]])
break
if status == "BOOM":
x,y = tmp_x,tmp_y
stack[-1][2] = tmp_directions
raise Exception("BOOM!!!")
else:
map,x,y,status = turn(game,map,pos[-1],x,y)
stack.pop()

# 重连之后走回到上次遇见炸弹前的节点
elif [x,y] == [1,1]:
for i in stack[1:]:
map,x,y,status = turn(game,map,dic[i[3]],x,y)
else:
print("ukown: ",[x,y])
pause()
else:
exit = 1
except Exception,err:
game.close()
finally:
# 保存迷宫地图信息
img = Image.new("RGB",(len(map[0]),len(map)))
for i in range(len(map)):
for j in range(len(map[0])):
if map[i][j] == 0:
img.putpixel((j,i),(255,255,255))
elif map[i][j] == 1:
img.putpixel((j,i),0)
elif map[i][j] == 2:
img.putpixel((j,i),(255,0,0))
else:
pass
print(img.size)
img.save("map_1.png")

if exit == 1:
break

map_1

遍历出地图之后,利用题目源码写抓猫算法。(ps:就是本地模拟服务器程序运行状态,random的seed是由连接题目程序时的时间决定的,seed已知,本地模拟服务器状态,深度优先求解PERSONCAT的最短路径,这里因为嫌麻烦不想再该刚刚遍历地图的脚本了就选择了去网上找个现成的轮子,结果拿回来各种调试修bug,一个下午又被浪费了emmm 最后恼羞成怒又写了个轮子哭唧唧)

另外,出题人的服务器就离谱!!!此处疯狂diss出题人,第一天晚上做题的时候刚开始做就发现题目挂了,被迫睡大觉,第二天题目好了,但是服务器的时区延时是认真的么,不给时区信息的同时,服务器本地时间还比北京时间快了整整两秒!!!

其实本来网上的轮子也没啥,它的问题就那么一丢,本体调试几遍就能用了,但是跑flag的时候和服务器的输出一直对不上,结果一直在调试脚本找本地的bug,结果最后才知道本地已经完美了,数据对不上是服务器的seed比本地延时了2s!!!淦!ping服务器,响应延迟只有不到20ms,当时因为想着出题人没给提示应该是默认北京时区互联网时间,所以做多也就试了把本地模拟服务器的时间种子值加1,依旧不对。最后突然试了下加2对了。不说了,我是fw TAT

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/python -u
from pwn import *
import dfs
from PIL import Image
import json
import pickle
import random
import time
import sys
import copy


NOTHING = 0
WALL = 1
BOMB = 2
CAT = 3
PERSON = 4
dic = dfs.dic
directions = ['u', 'd', 'l', 'r']

def randint(start, end):
m = end - start + 1
return random.getrandbits(16) % m + start

class Game:
def __init__(self, map):
self.seed = int(time.time()) + 2
random.seed(self.seed)
self.map = map
self.n = len(self.map)
self.x = 1
self.y = 1
self.cx = 0
self.cy = 0
self.count = 0
self.placeCat(firsttime = True)
self.time = 0
self.persontime = 0

def placeCat(self, firsttime = False):
tmpmap = copy.deepcopy(self.map)
tmpmap[self.x][self.y] = PERSON
tmpmap[self.cx][self.cy] = CAT
if firsttime:
i = self.n - 2
j = self.n - 2
while tmpmap[i][j] != NOTHING:
i -= 1
j -= 1
self.cx = i
self.cy = j

else:
i = randint(1, self.n - 2)
j = randint(1, self.n - 2)
while tmpmap[i][j] != NOTHING:
i = randint(1, self.n - 2)
j = randint(1, self.n - 2)
self.cx = i
self.cy = j

def move(self, c, p):
self.time += 1
if self.time > 4000:
print "DARKFLAMEMASTER"
sys.exit(-1)
tmpmap = copy.deepcopy(self.map)
tmpmap[self.x][self.y] = PERSON
tmpmap[self.cx][self.cy] = CAT

if p == PERSON:
x = self.x
y = self.y
elif p == CAT:
x = self.cx
y = self.cy
else:
pass

if c == 'u':
y -= 1
elif c == 'd':
y += 1
elif c == 'l':
x -= 1
elif c == 'r':
x += 1
else:
pass

if p == PERSON:
self.persontime += 1
if tmpmap[x][y] == WALL:
print "WALL."
return False
elif tmpmap[x][y] == BOMB:
print "BOMB."
sys.exit(-1)
elif tmpmap[x][y] == CAT:
self.count += 1
if self.count == 10:
print self.time
print "SUCCESS!!!"
self.x = x
self.y = y
self.placeCat()
#print "Caught!"
return True
else:
self.x = x
self.y = y
print "OK."
return True

elif p == CAT:
if tmpmap[x][y] == NOTHING:
self.cx = x
self.cy = y
return True
else:
return False

else:
pass

def start(self,direction):
if direction not in directions:
sys.exit(-1)
self.move(direction, PERSON)

if self.persontime % 3 == 0:
for _ in range(2):
choice = randint(0, 3)
direction = directions[choice]
randcount = 0
while self.move(direction, CAT) == False:
choice = randint(0, 3)
direction = directions[choice]
randcount += 1
if randcount == 10:
break

map = [[] for i in range(60)]

pic = Image.open('./map_2.png')
w,h = pic.size
for i in range(h):
for j in range(w):
pixel = pic.getpixel((i,j))
if pixel == (0,0,0) or pixel == (255,0,0):
map[i].append(1)
else:
map[i].append(0)

time_1 = int(time.time())
sh = remote("59.110.63.160",40001)
#sh = remote("127.0.0.1",10002)
game = Game(map)
time_2 = int(time.time())
#sleep(0.3)
#print(sh.recv(),game.seed)

if time_1 != time_2:
print(time_1,time_2)
exit()

while True:
path = dfs.dfs_fun(map,(game.x,game.y),(game.cx,game.cy))
num = [4000,-1]
for i in path:
if len(i) < num[0]:
num[0] = len(i)
num[1] = path.index(i)
path = path[num[1]] ; path = path[1:]
for i in path:
direction = dic[i[-1]]
tmp_num = int(game.count)
game.start(direction)
sh.sendline(direction)
#sleep(0.1)
infor = sh.recv(timeout=1)
print infor
if "Caught" in infor:
pause()
if "Caught" not in infor and "OK" not in infor:

pause()
if game.count == 10:
pause()
if game.count - tmp_num == 1:
break
隐藏