这次比赛时就做出签到baby_maze两道题,排名为93名。赛后10分钟做出MedicalImage,看了一下排名,要是能赛时交上这道就恰好和清华一支队伍平分,而那个分数又只有他一支队伍也就是说差点就可以玩梗清华以下就是南科了哭哭

Misc

签到

emoji-aes解密即可。

给了一串emoji和一个提示字符串"GAME",因为之前了解有emoji-aes加密所以马上想到了它。用在线工具解密,密钥即GAME,拿到flag。

Re

baby_maze

写搜索算法跑出到迷宫终点的路径即可。

拿到elf可执行文件。先拖进ida无脑F5看看,直接看比较懵,随便跑一下,大概明白程序是玩家在一个迷宫中,整个迷宫地图对于玩家是不可见的,玩家通过键盘输入决定移动方向(WSAD),根据玩家当前所在的位置和选择的方向,程序返回相应的输出信息,如果这个方向能走,玩家就移动到新的位置。

下面是一些输出信息,输入的方向信息没有回显。

一些输出信息,输入的方向信息没有回显。

回到ida调出字符串窗口,里面给出了所有的可能输出的字符串(除了一个Wuhu,详见脚本注释),包括到达终点后的输出,告诉我们flag是我们走到终点的最短路线(按下的按键)字符串的md5值。

string窗口节选

要得到走到终点的路线可以写个DFS算法。脚本如下

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
from pwn import *
import numpy as np
from hashlib import md5

yes_strs = [
# 'This is the beginning. You can only go south.',
b'Just do it',
b'GOGOGO',
b'Wuhu', # ida调出来是没有找到这个字符串的,跑脚本的过程中发现有这个输出,就加上了
b'Wuhu~!',
b'You are so good',
b'Nice.',
b'Yeah~~~',
b'Let\'s go.',
b'Never stop',
b'So smart'
]

no_strs = [
b'Oh!!Monster',
b'Uh... yeah, no.',
b'nononononono',
b'Let me out!!!',
b'I can\'t see the sky',
b'Fxxk!!!',
b'Maybe this is a mistack',
b'Shit!!',
b'Oh no!!!',
b'Wall!!!',
b'OUCH!!!!'
]

# final_str = b'Good Job. \nAnd the flag is flag md5If not, you may need to go faster!'
final_str = b'Good Job.'

SCALE = 500

START = -1
OK = 0
WALKED = 1
WALL = 2
mark = np.zeros( (SCALE, SCALE), dtype = int )

context.binary = ELF('./maze')

io = process()

def DFS(move, pos, routine):

global io

io.sendline(move)

message = io.recvline().strip()

if message in yes_strs: # 这个位置不是墙(可以走)

mark[pos] = WALKED # 标记为已走过
print(pos, routine, message)

if mark[pos[0] - 1, pos[1]] == OK: # north

next_move = 'W'
next_pos = (pos[0] - 1, pos[1])
next_routine = routine + next_move
DFS(next_move, next_pos, next_routine) # 下一层DFS
if mark[next_pos] != WALL: # 如果下一个位置不是墙(可以走),DFS回溯到这里时要往回走一步
io.sendline('S')
io.recvline()

if mark[pos[0], pos[1] + 1] == OK: # east

next_move = 'D'
next_pos = (pos[0], pos[1] + 1)
next_routine = routine + next_move
DFS(next_move, next_pos, next_routine)
if mark[next_pos] != WALL:
io.sendline('A')
io.recvline()

if mark[pos[0] + 1, pos[1]] == OK: # south

next_move = 'S'
next_pos = (pos[0] + 1, pos[1])
next_routine = routine + next_move
DFS(next_move, next_pos, next_routine)
if mark[next_pos] != WALL:
io.sendline('W')
io.recvline()

if mark[pos[0], pos[1] - 1] == OK: # west

next_move = 'A'
next_pos = (pos[0], pos[1] - 1)
next_routine = routine + next_move
DFS(next_move, next_pos, next_routine)
if mark[next_pos] != WALL:
io.sendline('D')
io.recvline()

elif message in no_strs:

mark[pos] = WALL
return

elif message == final_str:

print('Find the flag routine:')
print(f'routine: {routine}')
print(f'flag: {md5(routine.encode("ascii")).hexdigest()}')
exit(0)

else:

print(f'unhold message: {message}')
print(f'cur routine: {routine}')
exit(1)

io.recvline_contains('This is the beginning. You can only go south.')

base_pos = (SCALE // 2, SCALE // 2)

mark[base_pos] = START
DFS('S', (base_pos[0] + 1, base_pos[1]), 'S') # DFS开始

其实我这里写的算法理论上跑出来的路径不一定是最短的,可能是地图设计的比较简单,我跑出来的恰好是对的。脚本输出如下,md5值包裹上flag格式即为正确flag。

1
2
3
Find the flag routine:
routine: SSSSSSSSSDDDDDDWWWWAAWWAAWWDDDDDDDDDDDDDDDDDDDDSSDDSSAASSSSAAAAWWAAWWWWAASSSSSSAASSDDSSSSDDWWWWDDSSDDDDWWDDDDDDWWAAAAWWDDDDWWAAWWWWDDSSDDSSSSSSSSSSDDDDSSAAAASSSSSSAASSSSAAWWAASSSSDDDDDDDDDDSSDDSSAASSSSAASSSSSSSSDDWWWWWWDDWWWWDDWWWWDDSSSSSSSSAASSSSDDDDSSDDDDWWDDSSDDSSDDDDDDDDSSDDSSSSDDDDSSDDSSSSSSDDSSSSDDDDSSSSDDDDDDSSSSDDSSDSSASSSSAASSDDSSAASSDDDDDDSSDDDDWWDDSSSSSSDDDDWWAAWWWWDDDDSSSSDDDDDDSSAASSSSSSDDDDDDDDSSDDDDSSSSSSDDWWDDDDDDSSSSSSSSAASSDDSSSSSSAASSDDS
flag: 078c8fbc1d0d033f663dcc58e899c101

medical_app

推rc4的时候出了点问题,先占了坑,把思路理顺了再写。

Crypto

MedicalImage

根据提示还原加密算法,写出解密算法解密图片即可。

给了不完整的脚本文件和加密后的图片,脚本内容如下

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
from PIL import Image
from decimal import *
import numpy as np
import random
getcontext().prec = 20

def f1(x):
# It is based on logistic map in chaotic systems
# The parameter r takes the largest legal value
assert(x>=0)
assert(x<=1)
...

def f2(x):
# same as f1
...
def f3(x):
# same as f1
...

def encryptImage(path):
im = Image.open(path)
size = im.size
pic = np.array(im)
im.close()
r1 = Decimal('0.478706063089473894123')
r2 = Decimal('0.613494245341234672318')
r3 = Decimal('0.946365754637812381837')
w,h = size
for i in range(200):
r1 = f1(r1)
r2 = f2(r2)
r3 = f3(r3)
const = 10**14
for x in range(w):
for y in range(h):
x1 = int(round(const*r1))%w
y1 = int(round(const*r2))%h
r1 = f1(r1)
r2 = f2(r2)
tmp = pic[y,x]
pic[y,x] = pic[y1,x1]
pic[y1,x1] = tmp
p0 = random.randint(100,104)
c0 = random.randint(200,204)
config = (p0,c0)
for x in range(w):
for y in range(h):
k = int(round(const*r3))%256
k = bin(k)[2:].ljust(8,'0')
k = int(k[p0%8:]+k[:p0%8],2)
r3 = f3(r3)
p0 = pic[y,x]
c0 = k^((k+p0)%256)^c0
pic[y,x] = c0

return pic,size,config
def outputImage(path,pic,size):
im = Image.new('P', size,'white')
pixels = im.load()
for i in range(im.size[0]):
for j in range(im.size[1]):
pixels[i,j] = (int(pic[j][i]))

im.save(path)


def decryptImage(pic,size,config):
.....

enc_img = 'flag.bmp'
out_im = 'flag_enc.bmp'

pic,size,_ = encryptImage(enc_img)
outputImage(out_im,pic,size)

残缺的部分有作用相同的f1f2f3方法和解密方法,也就是还原了f1方法就能还原整个加密过程。直接google搜"logistic map in chaotic systems",从wikipedia词条得到公式xn+1=rxn(1xn)x_{n+1}=r*x_n*(1-x_n),其中xn,xn+1[0,1]x_n, x_{n+1} \in [0,1]。为了保证x的取值范围,r的最大值取4(用基本不等式可以推出),修复f1等3个方法如下

1
2
3
4
5
6
7
8
9
10
def f1(x):
assert(x>=0)
assert(x<=1)
return 4 * x * (1 - x)

def f2(x):
return f1(x)

def f3(x):
return f1(x)

接下来详细分析加密过程

首先是初始化一些参数

1
2
3
4
5
6
7
8
9
10
11
12
13
im = Image.open(path)
size = im.size
pic = np.array(im)
im.close()
r1 = Decimal('0.478706063089473894123')
r2 = Decimal('0.613494245341234672318')
r3 = Decimal('0.946365754637812381837')
w,h = size
for i in range(200):
r1 = f1(r1)
r2 = f2(r2)
r3 = f3(r3)
const = 10**14

接着,依次遍历每个像素点,将当前遍历到的像素点与一个算出来的像素点互换。

1
2
3
4
5
6
7
8
9
for x in range(w):
for y in range(h):
x1 = int(round(const*r1))%w
y1 = int(round(const*r2))%h
r1 = f1(r1)
r2 = f2(r2)
tmp = pic[y,x]
pic[y,x] = pic[y1,x1]
pic[y1,x1] = tmp

然后,生成了一组随机数p0c0,总共只有5*5=25种可能性,可以爆破。依次遍历每个像素点,对当前像素点的灰度值做一个变换,p0c0作为变换的初始参数。

1
2
3
4
5
6
7
8
9
10
11
12
p0 = random.randint(100,104)
c0 = random.randint(200,204)
config = (p0,c0)
for x in range(w):
for y in range(h):
k = int(round(const*r3))%256
k = bin(k)[2:].ljust(8,'0')
k = int(k[p0%8:]+k[:p0%8],2)
r3 = f3(r3)
p0 = pic[y,x]
c0 = k^((k+p0)%256)^c0
pic[y,x] = c0

照着每一步写出对应的逆变换即可。其中像素点互换部分,由于加密时先前的(互换)操作会影响到之后的(互换)操作,逆变换时需要以相反的顺序遍历;而灰度值变换部分则需要按照与加密时相同的顺序遍历。脚本如下

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
def decryptImage(pic,size,config):

r1 = Decimal('0.478706063089473894123')
r2 = Decimal('0.613494245341234672318')
r3 = Decimal('0.946365754637812381837')
w, h = size
for i in range(200):
r1 = f1(r1)
r2 = f2(r2)
r3 = f3(r3)
const = 10**14

p0 = config[0]
c0 = config[1]

for x in range(w):
for y in range(h):
k = int(round(const*r3))%256
k = bin(k)[2:].ljust(8,'0')
k = int(k[p0%8:]+k[:p0%8],2)
r3 = f3(r3)
c1 = pic[y,x]
p0 = ((c1^c0^k)-k+256)%256
pic[y,x] = p0
c0 = c1

r1_list = []
r2_list = []
for x in range(w - 1, -1, -1):
for y in range(h - 1, -1, -1):
r1_list.append(r1)
r2_list.append(r2)
r1 = f1(r1)
r2 = f2(r2)

for x in range(w - 1, -1, -1):
for y in range(h - 1, -1, -1):
x1 = int(round(const*r1_list[x * h + y]))%w
y1 = int(round(const*r2_list[x * h + y]))%h

tmp = pic[y,x]
pic[y,x] = pic[y1,x1]
pic[y1,x1] = tmp

return pic, size


out_im = 'flag_enc.bmp'

im = Image.open(out_im)
size = im.size
pic = np.array(im)
im.close()

for p0 in range(100, 105):
for c0 in range(200, 205):
pic, size = decryptImage(pic, size, (p0, c0))
path = f'result/{p0}-{c0}.bmp'
outputImage(path, pic, size) # outputImage方法是现成的

得到flag

爆破结果

顺便提一嘴看了其他师傅的wp里有写看B站视频了解logistic map相关知识的,然后找了一个视频看,真的非常神奇。