最近想着复现之前矩阵杯的一道分析 pyd 的题目,难度实在是对我来说有点大,看 Writeup 都费劲,所以想自己写源码再生成一个简单的 pyd 再分析一下,我们这篇文章来学习一下 Python 逆向当中的一些特殊文件
Pyc
pyc 文件的全称是 Python Compiled Bytecode File
关于 Pyc 的结构,我有在 marshal 模块和 pyc 的关系当中学习过了
pyc 与 marshal – Arnold’s Blog (arnold66.top)
当然,我觉得在 Python 逆向当中,学习到这个程度可能已经够了,这里再做一下简单介绍
pyc 文件是由用 Python 编程语言编写的源代码生成的编译输出文件。当使用 Python 解释器运行 py 文件时,它会被转换为字节码以供执行。同时,编译后的字节码也会保存为 pyc 文件,以便以后在适用的情况下从缓存中重用。而无需在每次运行脚本时重新编译源代码。这可能会导致更快的脚本执行时间,尤其是对于大型脚本或模块。
pyc 文件有保护源码的作用,我们看不到它的源码,但可以通过命令运行它。
Pyo
pyo 文件的全称是 Python Optimized Bytecode File
pyc 文件,是 python 编译后的字节码(bytecode)文件。
pyo 文件,是 python 编译优化后的字节码文件。pyo 文件在大小上,一般小于等于 pyc 文件。如果想得到某个 py 文件的 pyo 文件,可以这样
这里注意 O 要大写,没想到生成后还是 pyc 后缀的文件
优化的大小和不优化大小也是一样的,这种优化可能只优化一些特定的代码
那就没啥好说的了,跟 pyc 一样的逆向分析方法。
Pyd
pyd 文件的全称是 Python Dynamic Module
pyd 文件是 Python 中一种特殊的二进制文件,主要用于在 Windows 操作系统上存放 C 或 C++ 编写的 Python 扩展模块。这些扩展模块通过编译成.pyd 文件,可以在 Python 环境中被导入和使用,从而提供比纯 Python 代码更高的执行效率。这种文件类似于 Unix 系统中的.so(共享对象)文件,它们的功能和用法都十分相似。
当然,python 代码也可以打包为 pyd。但是我猜测可能利用 C 或 C++ 编写更多,因为可以达到提高代码执行效率的目的,python 代码打包肯定是没有 C 和 C++ 那么显著的。
我们先看 Python 代码打包成 pyd 的情况
创建一个 setup.py 文件,内容如下
然后我 sample.py 里面为了方便逆向分析,内容如下
然后再用以下命令即可生成 pyd
就可以成功生成 pyd 文件了
运行命令后会生成一个 bulid 文件夹,里面有两个文件夹,第一个内容就是 pyd,第二个应该是关于 C 语言的,我们不关注这部分,我们看 pyd。
可以看到是可以正常作为 Python 的模块导入并且使用的,我们可以通过 help 来查看模块当中都有哪些函数。
接下来再在 IDA 当中逆向分析 pyd,我们可以在 String 窗口追踪到相应的函数,以 ADD 函数为例
函数有很多的混淆代码,根本难以发现逻辑点,这也是 pyd 为啥难以分析的原因,仔细找,发现了一个相关的 API 函数,很明显是执行加法的运算
再通过这个 API 函数,我们定位 sub_180003610 函数,点进去发现确实是 ADD 函数的大概逻辑,但是这是在我们知道源代码的情况下分析的,倘若不知道源代码,其逆向分析的难度你看一下这些代码就知道并不简单。
SUB 函数的分析过程和 ADD 函数一样
那么我就想修改一下 SUB 或者 ADD 函数的代码,去观察它们的区别来看往后我们逆向分析的时候关键看哪里,我把 SUB 函数和 ADD 函数改成了这样
SUB 函数的减去的数不同之后,传入关键函数的参数就多了一个
ADD 函数也一样,并且多用了一个 API 函数来实现三数的相加,这些简单逻辑视乎都只用关注关键函数的 return 返回值的部分。
在修改了代码之后,还发现下面这个 API 函数 PyErr_Format 传入的这个位置的参数,就是我们分析的函数实际需要接收的参数的个数。
我们在 Python 中调用,并且只传入 2 个函数给 ADD 看看,应证我们的猜想是正确的
那么这两个简单函数在 pyd 中的呈现我们就看完了,接下来就看 JUGE 函数
很容易就找到了 JUGE 函数的主要逻辑,第三部分的代码一般只要 Python 中出现 If 的判断语句都会有
这里的 IsTrue 变量就是一个标志,还有 PyObject_RichCompare、PyObject_IsTrue 等 API 函数可能也都是。
但是我有一个疑惑的点在于倘若判断成立,应该会输出”Arnold!!!” 的字符串,但是函数里面找不到这样的操作,我认为打印函数可能是下面这个
因为 PyObject_Call 这个 API 函数全局只调用了一次,但是我 patch 了一下,发现并没有什么影响,然后前面的跳转也试了 patch 一下,还是找不到哪里是打印函数的地方,算了在这里就打个问号先吧。
我还发现引用 PyUnicode_InternFromString 的函数,一般有一些关键字符串变量在里面,如图
这里的字符串比如”Arnold!!!” 如果被修改的话,是会有影响的。
在 pyd 逆向当中,因为 CTF 题目通常要得出 flag,所以一般密文会以数组的形式存在,接下来我们探讨一下数组的形式在 pyd 文件当中的呈现,源代码如下
这函数里面有一个长度为 19 的列表,再放到 IDA 中看看,很容易找到关键逻辑
用一个 API 创建了一个列表,后面 v5 偏移每次加 8,加到 144,刚好是 19 个元素,但是这里没有显示列表当中的数据,找到 PyUnicode_InternFromString 函数的地方,也没有列表当中的数据
通过交叉引用可以发现列表当中的数据
这里的数据从小到大排列,有很密集的 PyLong_FromLong 函数,传入函数的参数就有我们列表当中的数据,我们看看如何确定列表数据的实际顺序
如图,左边第一个列表元素的索引为 23,再看右边的索引,就能得到列表的第一个元素是 11,这样我们就能得到列表的全部元素了。
这里还发现 PyObject_SetItem 函数一般用于设置列表索引的值,如下图
用了两次函数其实就相当于上面的 Python 代码
目前为止就是我关于 pyd 的所有发现了,等学到了新知识再来补充。