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博客