Python 打包后 DUMP 的那些事
前言
随着CTF的发展,Python逆向题目在出题人手下越来越不走常规路,常常搞一些魔改或是加密,对于打包完后的结果我们并不一定每次都可以成功的对其进行解包。我们常见的 exe 或是 elf 的 DUMP 目的是提取对应的字节码,而Python打包后的东西与前两者不太一样,对此我们考虑使用 Python 内置的虚拟机来进行完成我们的DUMP。
要想完成相关代码的提取我们首先需要知道要提取什么,在此过程我们主要的目标是f_code
主要工作
帧对象
帧对象是 Python 中用于表示函数调用栈帧的数据结构。
每当一个函数被调用时,Python 会为该函数创建一个帧对象。帧对象包含了函数调用过程中的所有上下文信息,如局部变量、全局变量、调用者的信息以及代码对象等。
帧对象在函数调用期间会被分配到栈上,从而形成一个函数调用栈。
帧对象(frame objects)具有以下一些属性:
f_back:指向调用当前函数的帧对象的引用。
f_code:当前帧对象所执行的代码对象。
f_globals:全局命名空间的字典。
f_locals:局部命名空间的字典。
f_lineno:当前正在执行的行号。
f_code(code object):代码对象是 Python 中用于表示编译后的字节码的数据结构。
当 Python 解释器解析并编译源代码时,它会生成代码对象。代码对象包含了编译后的字节码以及与源代码相关的其他元数据,如变量名、行号、常量等。
代码对象(code objects)具有以下一些属性:
co_argcount:普通参数的数量。
co_code:包含编译后字节码的字符串。
co_consts:常量元组,包含了代码对象中所有的常量。
co_filename:源代码文件的名称。
co_firstlineno:源代码中第一个行号。
co_flags:解释器内部使用的标志。
co_lnotab:编码字节码偏移量到源代码行号的映射。
co_name:代码对象的名称。
co_names:包含了代码对象中所有变量名的元组。
co_varnames:包含了局部变量名的元组。
代码抽取
所以对于我们来说当我们能拿到f_code时我们便可以写出对应的代码逻辑,那么重点便落到了我们如何进行拿到相应的代码,目前有两种方式来进行获取:
- 调试对应虚拟机,从中找核心函数
- 调用内置的Python虚拟机
一般来说我们并不想弄的那么复杂,我们采用第二种方式进行实现
一般我们打包的Python程序中会带有虚拟机,我们是如何知道的呢?此处采用一个简单打包文件进行运行,使用Process Hacker观察对应程序
我们可以看到其创建了一个进程,通过分析PyInstaller源码可以知道我们打包的程序会将真正的PYC文件进行释放(通过CreateProcess),之后再运行,我们查看对应子进程所加载的动态链接库:

我们可以看到其加载了Python 3.10的一个dll文件,这个其实就是我们PyInstaller打包进去的虚拟机,其便是我们执行相关的代码所需要的文件,我们可以看到其中存在许多导出函数

这些函数便是我们之后想要利用的代码,通过调用其中的代码来进行调用对应的代码。
那么我们如何进行调用对应导出空间的函数呢?我们可以使用dll进行注入,这样我们就可以使用对应空间的代码,我们采用以下项目的代码来进行生成我们需要的dll文件
其创建了一个线程,当我们将生成的dll注入到对应的打包程序中后,其会执行对应目录下的code.py代码中的内容,对此我们可以在该文件中写入我们想要获取的信息,便可以从中拿到对应的代码
当我们注入dll后,便是编写相关的code.py文件来达到我们提取代码的目的,代码如下:
1 | |
我们通过遍历当前的frames将其中的f_code进行提取并转换为marshal,但是marshal与我们通常的pyc不太一样,我们还需要将其进行转换操作
1 | |
之后我们便可以拿到对应的pyc文件,之后就可以丢进uncompyle6或是pycdc中对pyc进行反编译
一些问题
上述代码中我们采用的是_current_frames()来获取当前的frame,意味着我们如果中间采用一个加密是在其他的文件中的,那么我们可能只能够提取出对应文件中的加密信息,而调用者的相关代码则无法进行提取,但是我们可以使用_getframe来获取对应的调用,但是其功能也比较有限,需要我们手动输入对应的层来进行dump相应的信息
1 | |
函数原型为:
sys._getframe([depth])depth为对应的调用深度,当前函数深度为 0,调用该函数则深度为 1,并以此类推
与此同时我们也可能会遇到多个marshal文件,当我们dump某些包含代码库的打包程序时便会产生上述情况,此时因为marshal文件过多,我们比较难以定位到相应的核心代码,因此该方法也不一定十分的有效
总结
整体来说上述方法有一定的可行性,但是对于一些大型的软件逆向可能仍然存在有一些不足之处,其主要的核心依然是获取CPython中的相关函数来为我们创建一个线程进而来获取对应的代码信息,进而达到我们的目的
参考
inspect — Inspect live objects — Python 3.7.17 documentation
What is a code object in Python? - Quora
Python3 获取调用栈信息 sys._getframe_118路司机的博客-CSDN博客