有点问题,懒得改了。。。
前言
刚打的 XCTF Final 里虽然 re 十分凄惨,只出了一道,而且由于 大家在做这道题的时候我还在和 frida 激情 battle 所以没看,不过看了一下 做题记录好像用的是 pyinstaller 的特性才解密的包(☁神 tql)
不过这里介绍一种比较通用的 pyc 逆向方法 --dump
从总思路说起
对于一个 python 逆向题,无论是使用什么 pyinstaller 打包也好,除了使用 cython 直接编译为 pyd,最终很多都是要归为 pyc 逆向的(当然有些会直接在 py 源码里面)。然后对于我们来说能反编译回 python 代码的,一般会 使用 pycdc 或者在线的那个反编译器,如果编译不回去的话我们会使用 marshal 来查看字节码,然后根据字节码来逆向。
我们知道 pyinstextractor 这玩意是通过识别 pyinstaller 的 zlib 数据进行解包的。那如果压根没有这 zlib 数据,例如通过加载器 将原 pe 异或一下,将无法进行解包。也就是无法得到 pyc,自然也没有办法 进行下面的分析了。但是 pyc 有个问题,就是最终由于还是字节码,还是得 通过 python 虚拟机来执行,这就给了我们操作的空间。
CPython 与 GIL
其实 cpython 确实是有一些执行的接口的,不过在提到这些之前要先提一下 GIL
GIL:又叫全局解释器锁,每个线程在执行的过程中都需要先获取 GIL,保证同一时刻只有一个线程在运行,目的是解决多线程同时竞争程序中的全局变量而出现的线程安全问题。
举个例子,可以包含 cpython 的库来简单运行一个 py 程序
Py_SetProgramName(L"当前 py 程序名");
PyEval_InitThreads();
PyGILState_STATE s = PyGILState_Ensure();
PyRun_SimpleString("print('hello!!!')"); // 可以执行 python 代码
PyGILState_Release(s);
从这个简单的代码可以看出,取得 GIL 后再创建线程就可以简单的运行任意的 py 脚本了!只要在你想要的时候创建一个线程执行这段代码可以了!
这里提一句,看起来在最新版本不需要这个 PyEval_InitThreads() 了,去掉也没事(好像 3.9 就废弃了),但是之前的 py 版本需要
Dump 与实战
说得很好,但是,有什么用呢?
说到主题,先要知道一个概念,code 是什么?
这个热知识让我们的 cha 老师来讲一下
在 Python 中,f_frame 和 f_code 都与函数调用和执行的上下文有关。
f_frame(frame object):帧对象是 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, 我们就能得到我们 pyc 中 最需要的部分 -- 代码逻辑
那么,对于一个被打包的程序,我们该怎么写出 f_code 呢?没错,当然是调用 python 虚拟机运行代码!
看一个 pyinstaller 的例子:XCTF FINAL 23, 我不是病毒 看到了吗,最下面的这 python 虚拟机依赖已经被加载到内存了
也就是我们可以通过 Hook 来直接调用这个内存里的 python 虚拟机来 调用函数了
Py_SetProgramName(L"InjectedByDr3");
PyEval_InitThreads();
PyGILState_STATE s = PyGILState_Ensure();
PyRun_SimpleString("import os\nwith open(\"code.py\",\"r\") as file:\n data = file.read()\nexec(data)");
PyGILState_Release(s);
由于是同一个 python 程序,所以可以直接写出自(dui)己(fang)的 code 最后通过外部的 code.py 写出
import sys, marshal
i =0
for frame in sys._current_frames().values():
code = frame.f_code
open("dumped"+str(i)+".marshal", "wb").write(marshal.dumps(code))# Loop all the threads running in the process
i+=1
print("Dump finished!")
看看效果吧
读取 marshal 可以用 py 自带的库
import marshal
import dis
with open('dumped1.marshal', 'rb') as f:
code = marshal.load(f)
dis.dis(code)
完美 dump
附加一个转换为 pyc 的代码
import marshal
import importlib
with open('dumped1.marshal', 'rb') as f:
code = marshal.load(f)
pyc_data = importlib._bootstrap_external._code_to_timestamp_pyc(code)
print(pyc_data)
with open('file.pyc', 'wb') as f:
f.write(pyc_data)