Python pyc文件格式解析
这篇文章来讲解对于Python中的可执行文件格式,他常常出现在python的缓存中,作为中间语言来指导机器正确的执行python代码。而有部分场景中有些使用者为了自身的一些特殊需求,如逆向被编译成exe的python程序、版权保护、免杀、CTF出题等原因。而直接将python打包成pyc,至于pyc究竟是如何实现的,它到底是一段python代码还是二进制代码,我们将通过python源码来逐步分析pyc的文件格式和执行流程。
总 述
众所周知python是一个开源项目,他的源码在https://github.com/python/cpython。
同搜索关键字.pyc找到maybe_pyc_file
https://github.com/python/cpython/blob/43a6e4fa4934fcc0cbd83f7f3dc1b23a5f79f24b/Python/pythonrun.c#L318
通过方法名得知,这是一个判断是否为pyc文件的通用函数。
pyc运行环境分析
1、首先通过PyUnicode_Tailmatch后缀判断是否为pyc文件。
如果后缀不一致,随后通过PyImport_GetMagicNumber() &0xFFFF与文件开头的两个字节对比。
2、我们可以通过如下方法得知当前版本的Magic值是多少:
import importlib
importlib._bootstrap_external._RAW_MAGIC_NUMBER
3、找一个当前版本python的缓存pyc文件:(注意大小端)
4、随后查找哪里使用了这个PyUnicode_Tailmatch方法:
找到一个名为pyrun_simple_file的方法 位于pythonrun.c
https://github.com/python/cpython/blob/43a6e4fa4934fcc0cbd83f7f3dc1b23a5f79f24b/Python/pythonrun.c#L396
在他之前是对环境的一些初始化工作:
5、之后通过是否为pyc文件来执行不同的逻辑,我们只关注是pyc时候的情况。
6、进入run_pyc_file中,从这里开始就是真正开始分析并执行pyc的逻辑了。
7、开头通过magic == PyImport_GetMagicNumber()判断文件头格式,PyImport_GetMagicNumber在之前的maybe_pyc_file中已经分析过,它实际上执行的代码对应着的是:importlib._bootstrap_external._RAW_MAGIC_NUMBER。
随后跳过了三个Long,此处PyMarshal_ReadLongFromFile的Long指的是4个字节,这表示实际上执行pyc只会关注开头四个字节的头部,其余部分实际上是无效的。
之前用过pyinstxtractor的朋友可能经常去添加这个头部,网上大多数方法都是通过复制头部过去,实际上可以通过我们之前说的importlib._bootstrap_external._RAW_MAGIC_NUMBER加上没有意义的三个Long就可以运行了。当然在这之前有可能这些是有意义的,但在后面版本被废弃了,所以加上没有意义的三个Long可能不适用于所有版本。
8、在这之后便是两个十分重要的方法:
PyMarshal_ReadLastObjectFromFile和run_eval_code_obj
(1)先深入PyMarshal_ReadLastObjectFromFile:
此函数直接将整个文件读出来,然后传入了PyMarshal_ReadObjectFromString。
此函数初始化PyObject对象,将文件数据的引用一并存入,并将其传递给read_object方法进一步通过文件数据初始化PyObject对象。
(2)read_object方法:
此方法对PyObject的文件配置进行了检测,随后传入r_object。
9、随后我们来到了无比庞大的指令解析部分,通过方法内庞大的switch(type)能确定我们没有来错地方,这之中表示在python中的所有对象的底层元数据类型和解析方式。
10、但这还不够,我们需要知道不同类型之间的组合方式:
首先看看switch之前有哪些代码,开头的代码片段在多分支代码中是非常重要的一个部分。
11、首先通过r_byte从缓冲区中取出一个字节作为code。
开头一个小检查,确认当前没有EOF。随后给depth加一,然后和MAX_MARSHAL_STACK_DEPTH对比。由此得出depth是一个栈大小的计数。
12、随后通过取出的一个字节将它分为两个部分,分别为flag与type。
其中FLAG_REF被定义为 0x80。
13、然后通过type进行switch分支跳转,不同的分支会产生不同的返回值,并作为统一的对象PyObject*返回给调用者。
为了更好的展示它的结构,随机选择一个3.9的pyc文件用010Editor打开。
按照3.9的pyc文件格式我修正了原先010Editor库中的pyc解析模板。此模板将在文件末尾提供。
14、magic表示之前的pyc文件头。skip表示跳过的那三个头部数据,Data的type则是r_object第一个r_byte出来的那个code,被分为type和flag,此处值为E3抛弃掉flag位带入到switch:
15、找到case TYPE_CODE,代码中有各种数据的读取:
16、通过010Editor统计,内部有一些运行环境参数、代码段,静态变量段,名称段等等。表示这段代码使用到的任何东西。
可以看到代码段等类型都是PyObject并且再次使用r_object来调用,构成了一个递归。
17、除了TYPE_CODE以外还有很多基本数据结构,其中包含了python里面所有的数据结构。由于代码量太多了此处仅介绍部分常见结构。
(1)0TYPE_INT是直接读取一个long:
(2)TYPE_STRING是先读取一个long n,随后再通过r_string读取n个字节:
(3)TYPE_TUPLE 与TYPE_STRING的区别是读取n个PyObject:
18、我们回到TYPE_CODE:
由图可知,实际上他的成员code就是一个TYPE_STRING,它被当作字节序列来使用,一共是626个字节的长度。
这里面存放着很多的指令。
19、随后是TYPE_CODE中的consts段,在这个段里有许多代码环境中用到的常量。包括字符串、整数、浮点数、元组、列表等常量,同时也包含了子函数在内。
子函数也可以有code和consts,可以这样递归下去:
20、其次是names,其中包含了代码中会用到的库名,方法名,类名等。它们的类型通常是TYPE_ASCII系列或者TYPE_REF。(TYPE_REF用于引用其他ref_flag为1的变量,人工找的话不是很方便)
下面三个理论上是变量名的集合,但是全通过TYPE_REF引用了别处的值。
21、随后是文件名,与名称。文件名同样引用了别处的字符串。
就是这样一层一层的递归造就了整个pyc的运行环境。
结 束 语
至此pyc文件格式就结束了,整个pyc文件都围绕着一个递归构建的PyObject。它能表示python的基本数据结构,表示函数结构,表示代码等python的一切。而下一篇文章讲解python是如何使用PyObject来加载,来构建执行环境,来执行其中的代码的。
END
原文始发于微信公众号(锋刃科技):Python pyc文件格式解析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论