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:

 

posted @ 2022-07-11 15:08  nLesxw  阅读(46)  评论(0编辑  收藏  举报