在CNSS 2023的夏令营线上题中找到的一道misc题,看到标题有Crypto,感兴趣就去试了一试,发现确实是道比较有意思的题目,就在此记录一下,同时也开启 misc趣题 这一分类。
🔑 Shino 的 Crypto 梦想 题目来源:2023-CNSS-Summer
题目描述:
1 刚刚接触网络安全不久的 Shino 有一个成为 Crypto 方向专家的梦想,所以他写了一个很安全的加密算法,你可以帮他看看吗?
端口:
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, keyimport stringclass 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}
确实很有意思!