bad_python

bad_python 题解 🐱

题目描述:the .pyc is broken ,can you help me recover?

第一步:初见文件 🎀

下载附件,是一个名为 bdbc0757-1971-4134-8f3c-aa4b0a7add42.zip 的压缩包,解压后得到一个 pyre.cpython-36.pyc 文件。这是一个 Python 3.6 版本的字节码文件,但题目提示它被“破坏”了,需要我们帮忙恢复~

通常,.pyc 文件是 Python 源码编译后的字节码,可以通过反编译工具将其还原为 Python 代码。但直接反编译可能会失败,因为文件头部可能被破坏了喵~

第二步:尝试反编译 🛠

首先,尝试用常用的反编译工具 uncompyle6 来反编译:

1
2
pip install uncompyle6
uncompyle6 -o . pyre.cpython-36.pyc

但出现了错误提示:

1
KeyError: '3.9.3'

咦?文件名是 cpython-36,说明应该是 Python 3.6 编译的,但工具似乎检测到了不匹配的版本。可能是文件头部的魔数(magic number)不对,导致工具识别错误。

第三步:修复文件头 🔧

Python 的 .pyc 文件头部有一个魔数,用来标识 Python 版本。我们需要将其修正为正确的 Python 3.6 的魔数。

用十六进制编辑器(如 WinHex)打开 pyre.cpython-36.pyc,查看头部。直接修改文件头,仍反编译失败。
我们可以在本地用 Python 3.6 编译一个简单的脚本,生成一个正确的 .pyc 文件,比较头部。

在 Python 3.6 中,.pyc 文件头结构为:

1
2
3
4
5
struct {
uint32_t magic; // 4字节:魔数,标识Python版本
uint32_t bit_field; // 4字节:位字段
uint64_t timestamp; // 8字节:时间戳
}

先写一个1.py,再用以下命令生成它的pyc文件:

1
python3.6 -m py_compile 1.py

然后比较两个文件的头部。发现原文件的魔数不正确,于是将原文件的前 16 个字节替换为正确的 Python 3.6 的头部(可以从1.py中复制)。

9a55115c-2f47-4896-9d1b-56c89b117d53.png

修复后,再次使用 uncompyle6 反编译:

1
uncompyle6 -o . pyre.cpython-36.pyc

b0ce6f78-41f9-4059-9884-95c249a310eb.png

成功啦!得到了 pyre.py 文件,里面就是被反编译的 Python 源码。

第四步:分析源码 🧐

打开 pyre.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
29
30
31
def encrypt(v, k):
v0 = c_uint32(v[0])
v1 = c_uint32(v[1])
sum1 = c_uint32(0)
delta = 195935983
for i in range(32):
v0.value += (v1.value << 4 ^ v1.value >> 7) + v1.value ^ sum1.value + k[sum1.value & 3]
sum1.value += delta
v1.value += (v0.value << 4 ^ v0.value >> 7) + v0.value ^ sum1.value + k[sum1.value >> 9 & 3]

return (v0.value, v1.value)

if __name__ == "__main__":
flag = input("please input your flag:")
k = [255, 187, 51, 68]
if len(flag) != 32:
print("wrong!")
exit(-1)
a = []
for i in range(0, 32, 8):
v1 = bytes_to_long(bytes(flag[i:i + 4], "ascii"))
v2 = bytes_to_long(bytes(flag[i + 4:i + 8], "ascii"))
a += encrypt([v1, v2], k)

enc = [4006073346, 2582197823, 2235293281, 558171287, 2425328816, 1715140098, 986348143, 1948615354]
for i in range(8):
if enc[i] != a[i]:
print("wrong!")
exit(-1)

print("flag is flag{%s}" % flag)

加密算法是一个类似 XTEA 的分组密码,但有一些变体。我们需要编写解密算法,从密文反推明文。

注意到该算法的核心是两个bytes_to_long函数,对其逆向即可。

第五步:编写解密脚本 🐾

观察加密过程,每轮操作如下:

1
2
3
v0.value += (v1.value << 4 ^ v1.value >> 7) + v1.value ^ sum1.value + k[sum1.value & 3]
sum1.value += delta
v1.value += (v0.value << 4 ^ v0.value >> 7) + v0.value ^ sum1.value + k[sum1.value >> 9 & 3]

其中,sum1 初始为 0,每轮加上 delta,共 32 轮。解密时,我们需要从最后一轮开始逆运算,逐步恢复出原始的 v0 和 v1。

解密脚本如下:

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
def decrypt_pair(v0, v1, k):
delta = 195935983
mask = 0xFFFFFFFF
for i in range(31, -1, -1):
s_i = (i * delta) & mask
s_prime = ((i + 1) * delta) & mask
# 逆向 v1 的更新
g = ((v0 << 4) ^ (v0 >> 7)) + v0
g ^= (s_prime + k[(s_prime >> 9) & 3])
g &= mask
v1 = (v1 - g) & mask
# 逆向 v0 的更新
f = ((v1 << 4) ^ (v1 >> 7)) + v1
f ^= (s_i + k[s_i & 3])
f &= mask
v0 = (v0 - f) & mask
return v0, v1

k = [255, 187, 51, 68]
enc = [4006073346, 2582197823, 2235293281, 558171287, 2425328816, 1715140098, 986348143, 1948615354]

# 将密文分成四对
pairs = [(enc[i], enc[i + 1]) for i in range(0, 8, 2)]

flag_bytes = b''
for v0, v1 in pairs:
p0, p1 = decrypt_pair(v0, v1, k)
flag_bytes += p0.to_bytes(4, 'big')
flag_bytes += p1.to_bytes(4, 'big')

flag = flag_bytes.decode('ascii')
print("flag is: flag{%s}" % flag) # flag is: flag{Th1s_1s_A_Easy_Pyth0n__R3veRse_0}

好累的说QAQ