py编译逆向学习

PYC 文件

pyc 文件是 python 在编译过程中出现的主要中间过程文件。pyc 文件是二进制的,类似 java 的字节码,可以由 python 虚拟机直接执行的。

PyCodeObject

实际上,pyc 文件就是 PyCodeObject 对象在硬盘上的保存形式。

而 PyCodeObject 的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct {
PyObject_HEAD
int co_argcount; /* 位置参数个数 */
int co_nlocals; /* 局部变量个数 */
int co_stacksize; /* 栈大小 */
int co_flags;
PyObject *co_code; /* 字节码指令序列 */
PyObject *co_consts; /* 所有常量集合 */
PyObject *co_names; /* 所有符号名称集合 */
PyObject *co_varnames; /* 局部变量名称集合 */
PyObject *co_freevars; /* 闭包用的的变量名集合 */
PyObject *co_cellvars; /* 内部嵌套函数引用的变量名集合 */
/* The rest doesn’t count for hash/cmp */
PyObject *co_filename; /* 代码所在文件名 */
PyObject *co_name; /* 模块名|函数名|类名 */
int co_firstlineno; /* 代码块在文件中的起始行号 */
PyObject *co_lnotab; /* 字节码指令和行号的对应关系 */
void *co_zombieframe; /* for optimization only (see frameobject.c) */
} PyCodeObject;

PyObject_HEAD

不同的 Python 版本会有不同的 PyObject_HEAD,以下是各版本的文件头:

Python 版本 十六进制文件头
Python 2.7 03f30d0a00000000
Python 3.0 3b0c0d0a00000000
Python 3.1 4f0c0d0a00000000
Python 3.2 6c0c0d0a00000000
Python 3.3 9e0c0d0a0000000000000000
Python 3.4 ee0c0d0a0000000000000000
Python 3.5 170d0d0a0000000000000000
Python 3.6 330d0d0a0000000000000000
Python 3.7 420d0d0a000000000000000000000000
Python 3.8 55 0d 0d 0a 00 00 00 00 00 00 00 00 00 00 00 00
Python 3.9 610d0d0a000000000000000000000000
Python 3.10 6f0d0d0a000000000000000000000000
Python 3.11 a70d0d0a000000000000000000000000

反反编译

更改魔术头

当想要保护我们的 pyc 文件不被反编译,最简单的做法就是更改魔术头,即 PyObject_HEAD。

可能是完全删除魔术头,也可能是修改为不是原生版本的魔术头,我们只需要根据情况添加或修改即可。

PYD 文件

pyd 文件相当于 python 的运行时 dll,在 python 代码中可以直接使用 import 将 pyd 文件当作模块导入。

对于 pyd 的逆向,我们需要借助 ida 的 attach 动态调试跟静态分析。

pyexe的逆向

PyInstaller打包后,pyc文件的前8个字节会被抹掉,所以最后要自己添加回去

逆向:

用pyinstxtractor来解pyc包

安装

1
pip install pyinstxtractor

执行脚本

1
python pyinstxtractor.py **.exe

就会解包得到一个文件夹(一般命名为**.exe_extracted)

里面只用注意两个文件,其他的一般是库脚本

一个是1.pyc,另一个是struct.pyc,

同时会有提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
PE:
[+] Processing pyre.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 37
[+] Length of package: 5796250 bytes
[+] Found 61 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: 1.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python37 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: pyre.exe

如果解包出来发现原来的pyc脚本是用与自己的python的版本不一样的话,会有警告:

1
[!] Warning: This script is running in a different Python version than the one used to build the executable.

这个是告诉你版本号不同,魔术头被修改了;

只需要自己将魔术头给改正,一般struct.pyc的前16字节就是所要的魔术头;

魔术头依照版本号来确定:

在刚刚的提示里面就含有这个文件对应的版本号

1
[+] Python version: 37

E.P.:

原:

image-20220919203628458

改:

image-20220919203711306

​ E3这个是个可以作为本文件的标记,所以可见多了4个字节,复制粘贴struct.pyc的头时顺便把那4个给覆盖了。

修改完之后就简单了,对1.pyc使用 uncompyle 可以将 pyc 文件完美反编译。uncompyle6 Github主页

安装:

1
pip3 install uncompyle

而 uncompyle 用法如下:

1
uncompyle6 *.pyc

uncomyle6 会直接将反编译后的源码输出在标准输出中,推荐用法:

1
uncompyle6 *.pyc > filename

将源码输出到文件里面,比如说:

1
uncompyle6 1.pyc > 1.py

输出后就直接看1.py即可。

直接输出pyc

1
2
3
4
5
6
7
8
9
10
11
12
import dis
import marshal
import os

from Crypto.Util.number import long_to_bytes

os.chdir(r'C:\Users\74592\Desktop\hgame2023\week2\stream')

with open('stream.pyc', 'rb') as file:
code = marshal.load(file)

print(dis.disassemble(code))