VSCTF的Recovery
题目如下:
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: