[2020 GeekChallenge] Re un_snake
0x00 逆向
下载文件后,发现是pyc文件。
pyc文件可以使用uncompyle6库。题目告诉我们是3.8.5版本的python,因此我就给python3装了库。
pip3 install uncompyle6
注意我电脑装的是python2 python3共存,如果电脑只有python3的直接pip就行,不用pip3。
装完以后,去下载文件路径里,打开cmd,输入
uncompyle6 -o un_snake.cpython-38.py un_snake.cpython-38.pyc
就成功反编译啦,反编译的文件可以在同目录下找到。
把得到的py文件用vscode打开
# uncompyle6 version 3.7.4
# Python bytecode 3.8 (3413)
# Decompiled from: Python 3.8.4 (tags/v3.8.4:dfa645a, Jul 13 2020, 16:46:45) [MSC v.1924 64 bit (AMD64)]
# Embedded file name: ./un_snake.py
# Compiled at: 2020-08-05 16:20:40
# Size of source mod 2**32: 1238 bytes
import this
from base64 import *
def pre(data):
th1s = 'TBESCFSRSAEUITANAIIN'.encode()
if (data_len := len(data)) > (th1s_len := len(th1s)):
th1s = th1s * (data_len // th1s_len) + th1s[:data_len - th1s_len]
return bytes(map(lambda x, y: x ^ y, data, th1s))
def enc(plain):
plain = list(plain)
plain = plain[::-1]
for i in range(len(plain)):
c = plain[i]
plain[i] = (c << 3 | c >> 5) & 255
else:
for i in range(len(plain) - 1):
plain[i] ^= plain[(i + 1)]
else:
return bytes(plain)
def check(a):
return b64encode(a) == b'mEiQCAjJoXJy2NiZQGGQyRm6IgHYQZAICKgowHHo4Dg='
if __name__ == '__main__':
print()
while True:
stuff = input('Now input you flag:')
stuff_ready = pre(stuff.encode())
result = check(enc(stuff_ready))
if result:
print('You get it! Python is so charming right?')
break
else:
print('Failed, try again!')
print('[🐍] Commit you flag, see you next time!')
得到这样一段代码
分析代码可以得到它是把我们输入的flag通过几层加密与密文比对。
我们输入的flag经过的加密函数有:pre(),enc(),check里面的b64encode()
让我们逐个分析
0x01 加密函数
pre()
def pre(data):
th1s = 'TBESCFSRSAEUITANAIIN'.encode()
if (data_len := len(data)) > (th1s_len := len(th1s)):
th1s = th1s * (data_len // th1s_len) + th1s[:data_len - th1s_len]
return bytes(map(lambda x, y: x ^ y, data, th1s))
if中的内容是:如果输入的flag长度超过了th1s,就在后面加一截,补全长度。
可以使用调试看一下效果。
可以看到th1s变成了TBESCFSRSAEUITANAIIN + TBESCFS
简单地说就是一直复读复读,直到长度和输入的flag相同。
bytes(map(lambda x, y: x ^ y, data, th1s))
然后就是加密部分,这一段其实很好理解,就是把flag和th1s每个字符互相异或。
————————————————————————————————————
虽然是后来才想到的,仔细一想,好像这个函数……是可逆的,也就是说……
a == pre(pre(a)) 是成立的。
enc()
def enc(plain):
plain = list(plain)
plain = plain[::-1]
for i in range(len(plain)):
c = plain[i]
plain[i] = (c << 3 | c >> 5) & 255
else:
for i in range(len(plain) - 1):
plain[i] ^= plain[(i + 1)]
else:
return bytes(plain)
我一开始还对这个for…else疑惑了很久,查了以后发现这else和没有是一样的= =。
它的规则是,for如果不是通过break结束,就会执行else,否则不执行。
从上往下分析一下
plain = plain[::-1]
这是简单地切片倒序,不懂的可以去看一下python的切片操作。
for i in range(len(plain)):
c = plain[i]
plain[i] = (c << 3 | c >> 5) & 255
这一步是把每一位字符的二进制数进行位操作
后面的&255 即 & 11111111就是把二进制数取后8位,也就是说这个数字不会超过255。
前面的位操作是把二进制数c 左移三位和右移五位互相按位或。
因为只有8位,左移三位就是把原来的数左边3个数去掉,右边补3个0,
同样右移5位就是把右边5个数去掉,左边补5个0。
它们互相按位或的结果就相当于原来的数左边三位和右边5位交换位置。
所以它的逆运算就是
for i in range(len(plain)):
c = plain[i]
plain[i] = (c << 5 | c >> 3) & 255
然后看下一个循环
for i in range(len(plain) - 1):
plain[i] ^= plain[(i + 1)]
这个就是很简单的按位与或,因为是从前往后与或,所以解密需要从后往前异或
逆运算:
for i in range(len(plain) - 2,-1,-1):
plain[i] ^= plain[(i + 1)]
解密的碎片已经集齐,就差把它们组合起来了。
(千万不要像我一样还按照原来的顺序解密,我TM还在这思考了半天到底哪里有问题。)
解密函数:
def dec(plain):
plain = list(plain)
for i in range(len(plain) - 2,-1,-1):
plain[i] ^= plain[(i + 1)]
for i in range(len(plain)):
c = plain[i]
plain[i] = (c << 5 | c >> 3) & 255
plain = plain[::-1]
return bytes(plain)
b64encode()
这就是很简单的base64库函数了,百度一下就可以得到解密函数b64decode()
好了,所有的加密函数已经得到解密方法,冲!
0x02解密脚本
from base64 import *
def pre(data):
th1s = 'TBESCFSRSAEUITANAIIN'.encode()
data_len = len(data)
th1s_len = len(th1s)
if (data_len) > (th1s_len):
th1s = th1s * (data_len // th1s_len) + th1s[:data_len - th1s_len]
return bytes(map(lambda x, y: x ^ y, data, th1s))
def dec(plain):
plain = list(plain)
for i in range(len(plain) - 2,-1,-1):
plain[i] ^= plain[(i + 1)]
for i in range(len(plain)):
c = plain[i]
plain[i] = (c << 5 | c >> 3) & 255
plain = plain[::-1]
return bytes(plain)
result = b'mEiQCAjJoXJy2NiZQGGQyRm6IgHYQZAICKgowHHo4Dg='
print(pre(dec(b64decode(result))))
运行即可得到flag:SYC{ssssss_Th1s_is_Pyth0n_ssss~}