VSCTF的Recovery

2023-02-21,

题目如下:

from random import randint
from base64 import b64encode def validate(password: str) -> bool:
if len(password) != 49:
return False key = ['vs'.join(str(randint(7, 9)) for _ in range(ord(i))) + 'vs' for i in password[::-2]]
gate = [118, 140, 231, 176, 205, 480, 308, 872, 702, 820, 1034, 1176, 1339, 1232, 1605, 1792, 782, 810, 1197, 880,
924, 1694, 2185, 2208, 2775]
if [randint(a, b[0]) for a, b in enumerate(zip(gate, key), 1) if len(b[1]) != 3 * (b[0] + 7 * a) // a]:
return False hammer = {str(a): password[a] + password[a + len(password) // 2] for a in range(1, len(password) // 2, 2)}
block = b'c3MxLnRkMy57XzUuaE83LjVfOS5faDExLkxfMTMuR0gxNS5fTDE3LjNfMTkuMzEyMS5pMzIz'
if b64encode(b'.'.join([((b + a).encode()) for a, b in hammer.items()])) != block:
return False return True if __name__ == "__main__":
passwd = input('Please validate your ID using your password\n> ')
if validate(passwd):
print('Access Granted: You now have gained access to the View Source Flag Vault!')
else:
print('Access Denied :(')

源码分析:

这个题的难点就在于代码过于python化,核心内容全是推导式。读懂代码这题就迎刃而解了。(也不是,如果解题代码写的不对,得到的也是错误flag,没错,说的就是本人这个弱鸡)

如果你也没读懂代码,那就跟着我思路来看看,或许会让你豁然开朗。如果你也明白了代码里的含义,可以跳过这个部分看看解题代码,我猜你只是需要这个。废话不多说,开搞。

先看 if __name__ == "__main__" 发现比较重要的是调用了validate函数,把我们输入的结果passwd传了进去。

对validate函数审计,因为我们是要破解,所以应当,也应该如此的从后面开始看。我们一眼就发现密文block,然后在 if 判断出告诉了我们block是base64加密后的结果,我们果断解一下。

content = 'c3MxLnRkMy57XzUuaE83LjVfOS5faDExLkxfMTMuR0gxNS5fTDE3LjNfMTkuMzEyMS5pMzIz'
print(base64.b64decode(content))

得到结果:

ss1.td3.{_5.hO7.5_9._h11.L_13.GH15._L17.3_19.3121.i323

继续对 if 分析,我们发现有一个列表推导式:[((b + a).encode()) for a, b in hammer.items()]

for 前面的内容是属于 for 循环里面的内容,所以这里意思是把 hammer 字典中 key 和 value 分别赋值给 a 和 b ,注意啊,这里 b 和 a 都是字符串,因此它们相加是拼接到一起。这里的encode的作用不大,使用默认的编码格式。

然后就把 a 和 b 生成的新字符串加到 前面的字符串中,然后以 点 分隔。

通过以上,我们就知道了hammer字典中用哪些内容,于是写个代码提取一下:

cipertext = 'ss1.td3.{_5.hO7.5_9._h11.L_13.GH15._L17.3_19.3121.i323'
print(len(cipertext)) flag_key = {}
j = 1
for i in cipertext.split('.'):
flag_key.update({j: i[0:2]})
j += 2
print(flag_key)

得到结果:

{1: 'ss', 3: 'td', 5: '{_', 7: 'hO', 9: '5_', 11: '_h', 13: 'L_', 15: 'GH', 17: '_L', 19: '3_', 21: '31', 23: 'i3'}

然后对hammer字典分析,发现是个字典推导式:

key = str(a)

value = password[a] + password[a + len(password) // 2]

for 循环从数字0 开始 每次步长为 2 ,结束为 password 的长度,看一下代码就知道 len(password) = 49

对以上分析我们就知道了 password[1] = s,pasword[25] = s,pasword[3] = t,,pasword[27] = d  以此类推,写个代码:

key_1 = ''
key_2 = ''
key_content = ''
# b_lenth=[375, 231, 252, 153, 144, 261, 153, 348, 255, 267, 303, 315, 330, 285, 342, 357, 159, 156, 210, 153, 153, 252, 306, 297, 354]
for a in flag_key:
key_1 += flag_key[a][0:1]
key_2 += flag_key[a][1:2] key_content += key_1 + ' ' + key_2
print(key_content)
#print(len(key_content))

得到结果flag1:

st{h5_LG_33i  sd_O_h_HL_13

继续对源码分析,在 if 处又发现一个列表推导式:[randint(a, b[0]) for a, b in enumerate(zip(gate, key), 1) if len(b[1]) != 3 * (b[0] + 7 * a) // a]

复杂推导式的读取原则是:从左边开始的第一个 for 开始读,属于 for 左边的为最后结果,右边则属于 for 循环里的下级内容。(自己总结的,如果不理解可以去百度了解更多)

enumerate函数的链接:Python enumerate() 函数 | 菜鸟教程 (runoob.com)

zip函数的链接:Python zip() 函数 | 菜鸟教程 (runoob.com)

这里一开始我也琢磨了好久,后面才知道 a 是从 1 开始的整数数列,通式为 n = n + 1 , 而 b[0] 就是 gate列表的内容,因为 zip 函数返回的是元组并且赋值给的 b,而 b[0] 正是元组中的第一个参数gate的值,因为 if 是在 for 里面的,所以 b [0] 动态变化。

通过上述我们知道,出题人要求我们使得 len(b[1]) = 3 * (b[0] + 7 * a) // a,而 b[1] 的内容 则是key列表的内容,写个代码提取一下:

gate = [118, 140, 231, 176, 205, 480, 308, 872, 702, 820, 1034, 1176, 1339, 1232, 1605, 1792, 782, 810, 1197, 880,924, 1694, 2185, 2208, 2775]
b_lenth = []
for i in range(len(gate)):
b_lenth.append(3 * (gate[i] + 7 * (i + 1)) // (i + 1))
print(b_lenth)

得到结果:

# b_lenth=[375, 231, 252, 153, 144, 261, 153, 348, 255, 267, 303, 315, 330, 285, 342, 357, 159, 156, 210, 153, 153, 252, 306, 297, 354]

我们继续对代码分析,发现了一个更加复杂的列表推导式 key,我们在前面总结了复杂推导式的读取原则,但是你应该明白这里的列表推导式的 “第一个” for 应该为 最后一个。

所以我们在前面的复杂推导式读取原则应该加上:如果遇到的第一个 for 是在小括号中,则往后找第一个 for

这里的 key 列表推导式我们展开来写,如下:

#假设password = ’ vsctf‘
password = 'vsctf'
key = ['vs'.join(str(7) for _ in range(ord(i))) + 'vs' for i in password[::2]]
print(key[0])
print(len(key[0])) key1 = []
content = ''
for i in password[::2]:
for _ in range(ord(i)):
content += 'vs'.join(str(7)) #因为这里join中传入参数不是序列,所以前面的'vs'不起作用,则 content = '7'
content += 'vs'
key1.append(content)
break #如果要和上述列表推导式相等,这里break实际没有
print(key1[0])
print(len(key1[0]))

得到结果如下:

发现是一模一样的,如果你这里感到奇怪,那一定是对 join 函数的不理解:Python join()方法 | 菜鸟教程 (runoob.com)

通过对key 列表推导式分析发现,它是取 password 中 偶数索引的内容,然后将内容转换为ascii码数值,作为循环的次数,每次循环取7 到 9中随机一个整数加 ’vs' 字符串。

所以我们只要知道有多少个整数,就知道了这个ASCII码数值是多少,从而知道 password 中偶数索引的内容。

因为前面我们已经算出了b[1]长度,又因为这里 for 每次都加上 'vs' 两个字符串,所以在原来的基础上扩大了3倍,因此只要 把 b_lenth 列表中每个元素除于 3 再转换即得flag2内容。

# b_lenth=[375, 231, 252, 153, 144, 261, 153, 348, 255, 267, 303, 315, 330, 285, 342, 357, 159, 156, 210, 153, 153, 252, 306, 297, 354]

key = ''
for i in b_lenth:
# print(chr(i // 3))
key += chr(i // 3)
print(key)

结果如下:

最后我们从flag2后面开始拿一个,从flag1 前面拿一个,然后重复前面,即得最终flag

综上所述,我们可以写出完整解题脚本。

完整解题代码:

#coding:utf-8
import base64 content = 'c3MxLnRkMy57XzUuaE83LjVfOS5faDExLkxfMTMuR0gxNS5fTDE3LjNfMTkuMzEyMS5pMzIz'
cipertext = base64.b64decode(content)
print(len(cipertext)) flag_key = {}
j = 1
for i in cipertext.split('.'):
flag_key.update({j: i[0:2]})
j += 2
print(flag_key) flag = ''
k = -1
for a in flag_key:
flag += chr(b_lenth[k] // 3) + flag_key[a][0:1] #偶数索引内容 + 奇数索引内容,从password字符串的索引0开始,到索引23结束
k += -1
if len(flag) == 24:
print(flag)
k = -13
for b in flag_key:
flag += chr(b_lenth[k] // 3) + flag_key[b][1:2] #从索引24开始,到索引48结束
k += -1
flag += chr(b_lenth[0] // 3) #加上索引49
break
print(flag)

得到flag:

VSCTFRecovery的相关教程结束。

《VSCTF的Recovery.doc》

下载本文的Word格式文档,以方便收藏与打印。