Pyc逆向


简介

pyc 文件是 python 在编译 py 文件过程中出现的主要中间过程文件,是一种二进制文件,是一种bytecode。pyc文件是可以由python虚拟机直接执行的程序。因此分析pyc文件的文件结构对于实现 pyc 反编译就显得十分重要。另外,pyc的内容,是跟python的版本相关的,不同版本编译后的pyc文件是不同的,3.8编译的pyc文件,3.7版本的python是无法执行的。

我们可以通过 py_compile 模块来生成 pyc 文件,或者通过 python -m test.py 命令来生成 pyc 文件:

1
2
from py_compile import *
compile("test.py")

两种操作都会在当前目录下新建一个 __pycache__ 目录,其中存放着 test.cpython-版本号.pyc。

如果有一个现成的pyc 文件,如何导入它。

1
2
3
4
5
6
7
from importlib.machinery import SourcelessFileLoader

test=SourcelessFileLoader(
"test","__pycache_/test.cpython-38.pyc"
).load_module()
print(test.a)
print(test.add(1,2))

pyc文件结构

pyc 文件在创建的时候都会往里面写入如下内容:

  1. magic number

这是 python 定义的一个整数值,不同版本的python会定义不同的 magic number,这个值是为了保证 python 能够加载正确的 pyc。

比如python3.5不会加载3.6版本的pyc,因为python在加载pyc文件的时候会首先检测该pyc的magic number。如果和自身的 magic number 不一致,则拒绝加载。

  1. 创建时间戳

在加载pyc之前会先比较源代码的最后修改时间和 pyc 文件的写入时间。如果pyc文件的写入时间。如果pyc文件的写入时间比源代码的修改时间要早,说明在生成pyc之后,源代码被修改了,那么会重新编译并写入pyc,而反之则会直接加载已存在的pyc。

  1. py文件的大小

注意:以上是python 3.7+ 的pyc文件结构,如果版本低于3.7,那么开头没有4个\x00,

pyc 3.7以后版本的文件头一般都是16个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import struct
from datetime import MAGIC_NUMBER
with open("__pycache__/test.cpython-38.pyc","rb") as f:
data=f.read()
# 0-4 字节是MAGIC_NUMBER
print(data[:4])
print(MAGIC_NUMBER)

print(data[4:8])

ts=struct.unpack("<I",data[8:12])[0]
print(ts)
print(datatime.fromtimestamp(ts))
print(struct.unpack("<I",data[12:16]))[0]

16个字节往后就是 PyCodeObject对象,并且是序列化之后的,因为该对象显然无法直接存在文件中。

1
2
3
4
5
6
7
8
9
10
11
12
import marshal

with open("__pycache__/test.cpython-38.pyc","rb") as f:
data=f.read()
code=marshal.loads(data[16:])
print(code)

print(code.co_code)

print(code.co_consts)

print(code.co_names)

既然我们可以根据 pyc 文件反推出 PyCodeObject,那么能否手动构建 PyCodeObject 然后生成 pyc 呢?来试一下。

1
2
3
a=1
b=2
c=3

上诉代码编译之后的结果,就是我们要构建的 PyCodeObject

1
2
3
4
5
6
import time
import struct
import marshal
from opcode import opmap
from types import CodeType
from importlib.util import MAGIC_NUMBER

打包与解包

python 程序通过工具可以打包为 exe 程序

python可以编译为pyc字节码文件,python解释器可以直接执行字节码文件

利用工具可以将pyc文件反编译为py文件。

pyinstaller

1
pyinstaller 打包文件

pyinstxtractor

1
python pyinstxtractor.py test.exe

运行后会在当前目录下出现一个文件夹,往往是源文件名_extracted,此处为text.exe_extracted

该工具目前版本已经高于2.0,不需要再补全pyc文件的magic头部了

pyc反编译

在解压缩后的目录下,存在原文件名.pyc文件,即我们的目标文件test.pyc

  • 对于 python3.9版本以下(不含3.9)
    • 使用uncompyle6工具
    • uncompyle6.exe .\test.exe_extracted\test.pyc > test.py
  • 对于python3.9版本以上(含3.9)
    • 使用decompyle++工具
    • pycdc.exe .\test.exe_extracted\test.pyc > test.py
    • pycdc 得到py代码,pycdas 得到字节码

如果反编译失败,通常没有加魔数头的情况下,我们通过加上魔数来解决。

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
33
34
enum PycMagic {
MAGIC_1_0 = 0x00999902,
MAGIC_1_1 = 0x00999903, /* Also covers 1.2 */
MAGIC_1_3 = 0x0A0D2E89,
MAGIC_1_4 = 0x0A0D1704,
MAGIC_1_5 = 0x0A0D4E99,
MAGIC_1_6 = 0x0A0DC4FC,

MAGIC_2_0 = 0x0A0DC687,
MAGIC_2_1 = 0x0A0DEB2A,
MAGIC_2_2 = 0x0A0DED2D,
MAGIC_2_3 = 0x0A0DF23B,
MAGIC_2_4 = 0x0A0DF26D,
MAGIC_2_5 = 0x0A0DF2B3,
MAGIC_2_6 = 0x0A0DF2D1,
MAGIC_2_7 = 0x0A0DF303,

MAGIC_3_0 = 0x0A0D0C3A,
MAGIC_3_1 = 0x0A0D0C4E,
MAGIC_3_2 = 0x0A0D0C6C,
MAGIC_3_3 = 0x0A0D0C9E,
MAGIC_3_4 = 0x0A0D0CEE,
MAGIC_3_5 = 0x0A0D0D16,
MAGIC_3_5_3 = 0x0A0D0D17,
MAGIC_3_6 = 0x0A0D0D33,
MAGIC_3_7 = 0x0A0D0D42,
MAGIC_3_8 = 0x0A0D0D55,
MAGIC_3_9 = 0x0A0D0D61,
MAGIC_3_10 = 0x0A0D0D6F,
MAGIC_3_11 = 0x0A0D0DA7,
MAGIC_3_12 = 0x0A0D0DCB,

INVALID = 0,
};

pyc加花混淆

例题

[羊城杯 2020]login

2024极客大挑战 好像是python?

2024极客大挑战 奇怪的RC4