简介
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 文件在创建的时候都会往里面写入如下内容:
- magic number
这是 python 定义的一个整数值,不同版本的python会定义不同的 magic number,这个值是为了保证 python 能够加载正确的 pyc。
比如python3.5不会加载3.6版本的pyc,因为python在加载pyc文件的时候会首先检测该pyc的magic number。如果和自身的 magic number 不一致,则拒绝加载。
- 创建时间戳
在加载pyc之前会先比较源代码的最后修改时间和 pyc 文件的写入时间。如果pyc文件的写入时间。如果pyc文件的写入时间比源代码的修改时间要早,说明在生成pyc之后,源代码被修改了,那么会重新编译并写入pyc,而反之则会直接加载已存在的pyc。
- 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()
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 呢?来试一下。
上诉代码编译之后的结果,就是我们要构建的 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
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