0%

misc趣题(一)

在CNSS 2023的夏令营线上题中找到的一道misc题,看到标题有Crypto,感兴趣就去试了一试,发现确实是道比较有意思的题目,就在此记录一下,同时也开启 misc趣题 这一分类。

🔑 Shino 的 Crypto 梦想

题目来源:2023-CNSS-Summer

题目描述:

1
刚刚接触网络安全不久的 Shino 有一个成为 Crypto 方向专家的梦想,所以他写了一个很安全的加密算法,你可以帮他看看吗?

端口:

1
nc 47.108.140.140 11037

Hint:

1
2
3
4
1、你可能需要pwntools
2、cnss{a-zA-Z0-9_}
保证}只在 flag 结尾出现一次
flag 长度不大于 50

题目:

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
from secret import cipher, key
import string

class Encoder:
def __init__(self):
self.stream = self.randomBox(self._init_box(key))

def do_encrypt(self, c):
return ord(c) ^ next(self.stream)

def _init_box(self, crypt_key):
Box = list(range(256))
key_length = len(crypt_key)
j = 0
for i in range(256):
index = ord(crypt_key[(i % key_length)])
j = (j + Box[i] + index) % 256
Box[i], Box[j] = Box[j], Box[i]

return Box

def randomBox(self, S):
i = 0
j = 0
while True:
i = i + 1 & 255
j = j + S[i] & 255
S[i], S[j] = S[j], S[i]
yield S[(S[i] + S[j] & 255)]


encoder = Encoder()
flag = input("input flag>> ")
table = string.digits + string.ascii_letters + "{}_"
i = 0
correct = 0

while i < len(flag):
while i < len(flag) and flag[i] not in table:
i += 1
if i >= len(flag):
break
if cipher[i] != encoder.do_encrypt(flag[i]):
print("Wrong flag!")
exit(0)
else:
correct += 1
if correct == len(cipher):
print("Correct flag!")
exit(0)
i += 1

print("Wrong flag!")

首先,题目的加密算法是RC4,可以先检查一下有无变种,方法是自己随便使用一组明文和密钥,分别用该程序与在线网站加密,检查结果是否相同。这样操作之后可以发现,结果是完全一样的,这说明本题并没有对RC4进行魔改,也因此解题的思路也就很自然的从开始的解密码转变成了找漏洞。

而要找漏洞的程序段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
encoder = Encoder()
flag = input("input flag>> ")
table = string.digits + string.ascii_letters + "{}_"
i = 0
correct = 0

while i < len(flag):
while i < len(flag) and flag[i] not in table:
i += 1
if i >= len(flag):
break
if cipher[i] != encoder.do_encrypt(flag[i]):
print("Wrong flag!")
exit(0)
else:
correct += 1
if correct == len(cipher):
print("Correct flag!")
exit(0)
i += 1

print("Wrong flag!")

先大致理解程序内容:程序需要你输入一串flag值,并将flag值逐个进行RC4加密并检查是否与密文相等,当输入的flag串加密值与密文完全相等时,便通过了检查,程序输出”Correct flag!”。

所以,这么一大段其实就只实现了一个内容:检查你输入的flag和你实际要提交的flag是否相等!可以说,整个程序都是一个障眼法,其实不要这个RC4,直接用以下的代码检查也是一样的:(假设实际flag串名为secret)

1
2
3
4
5
6
flag = input("input flag>> ")

if(flag == secret):
print("Correct flag!")
else:
print("Wrong flag!")

这样就行了!所以用这么一大段来核查flag一定有问题。

仔细核查,果然,下面这段代码大有玄机:

1
2
3
4
5
6
7
8
while i < len(flag):
while i < len(flag) and flag[i] not in table:
i += 1
if i >= len(flag):
break
if cipher[i] != encoder.do_encrypt(flag[i]):
print("Wrong flag!")
exit(0)

这段代码存在以下几个问题:

  • 输入的flag串中含有不在table中的项时,会一直跳过直至读到table中的字符为止,但是指数 i 会一直增加。
  • 判断指数 i 过大,依靠的是输入的字符串长度,而不是实际的flag串。
  • 将cipher[i]与encrypt(flag[i])进行比对时,并没有对cipher的指数进行检查。

这体现了一个很重要的信息:

  • 如果你的输入是正常的错误flag串,他会打印的内容是”Wrong flag!”
  • 如果你的输入是不正常的构造的字符串导致cipher[i]越界了,程序不会正常打印内容,而会报错!

举个例子,构造如下两个串:

1
2
flag = "cnss{1234567890abcdefgh}"
flag = chr(0) * 50 + "c"

那么,程序对第两个字符串的处理分别是:

由于第一个串字符均在table中,因此程序仅仅会将每个字符与正确flag进行比对,直到某个字符比对失败时,打印出”Wrong flag!”

而第二个串前五十个字符均是ASCII码为0的字符,是不在table中的,因此程序会先反复执行以下语句:

1
2
while i < len(flag) and flag[i] not in table:
i += 1

直至第51个字符”c”,由于”c”在table中,因此会进行比对。此时i=51,而由题目知道,flag长度不大于50,因此这时执行这条语句进行比对时:

1
2
3
if cipher[i] != encoder.do_encrypt(flag[i]):
print("Wrong flag!")
exit(0)

cipher[i]是必定越界的!那么程序就会抛出一个异常,而不再是打印”Wrong flag!”了。

这有什么用呢?用处很大。首先我们就可以反复构造如下字符串,发送给靶机端来确定真实flag的正确长度:

1
2
3
4
flag = chr(0) * 49 + "c"
flag = chr(0) * 48 + "c"
flag = chr(0) * 47 + "c"
......

为什么这样就可以确定长度呢?我们假设flag的正确长度是30,那么发送下面字符串给靶机,靶机的回应都是“异常”而非错误,这是因为cipher数组的下标最多只能取到29,一旦涉及到cipher[30]甚至更多就会产生越界异常:

1
2
3
4
5
flag = chr(0) * 49 + "c"
flag = chr(0) * 48 + "c"
flag = chr(0) * 47 + "c"
......
flag = chr(0) * 30 + "c"

然而发送下一个flag串,也就是:

1
flag = chr(0) * 29 + "c"

这时,由于没有越界,程序会回应”Wrong flag!”,而不再抛出异常了。

所以,由上述方式,我们就可以最终确定flag的真实长度是24,之后则可以反复构造下列字符串,并发送给靶机端来逐个核查字符是否正确:

1
2
for i in table:
flag = "cnss{" + i + chr(0)*100 + "a"

道理也是相同的,如果i是错误字符,那么核查不通过,程序直接回应”Wrong flag!”,而如果是正确字符,程序则会继续向后读,一直到读到越界的”a”后,抛出越界异常。

得到这个字符为 “1” 后,将他加入”cnss{“串后,继续构造下面字符串:

1
2
for i in table:
flag = "cnss{1" + i + chr(0)*100 + "a"

如此反复发送直至flag串已知的部分长度为24即可。

构造字符串并发送给靶机端需要用到pwntools,同时还有一些小细节需要注意,比如需要发送的是字节流而非字符串流。但是这些慢慢调试程序就好了。

exp.py:

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
from Crypto.Util.number import *
from pwn import *

table = string.digits + string.ascii_letters + "{}_"

init = b'cnss{'
has_find = 0
while(1):
if(has_find == 1):
break
for i in range(len(table)):
r=remote("47.108.140.140",11037)
try:
r.sendline(init + long_to_bytes(ord(table[i])) + b'\x00'*100 + b"a")
temp = r.recvline()
if(b"Correct" in temp):
exit(0)
except:
if(len(init) == 23):
has_find = 1
r.close()
break
init += long_to_bytes(ord(table[i]))
print(init)
r.close()
break
r.close()
print(init + b"}")

得到flag:

cnss{1nd3X_0Ut_oF_r4nge}


确实很有意思!