记录一下对python逆向的了解,md这两天和py杠上了 ## 什么是python

Python 是一种解释型、面向对象、动态数据类型的高级程序设计语言。

解释型语言没有严格编译汇编过程,存在运行效率低,重复解释的问题。

Python作为一个解释性语言,没有严格意义上的编译和汇编过程。一般认为编写好的Python源文件,经由Python解释器翻译成以.pyc为结尾的字节码文件,然后由Python虚拟机直接运行。

csdn上找了一张图:

image-20251031143917955
image-20251031141427467

pyc

pyc文件

pyc 文件: Python 在解释执行源代码时生成的一种字节码文件,包含了源代码的编译结果和相关的元数据信息,便于 Python 可以更快地加载和执行代码。

​ 如果源文件的修改之后被重新加载,那么解释器也会重新生成新的.pyc文件来更新

pyc文件的布局

image-20251031143440461

前4个字节,魔数

由两部分组成: 2 字节的整数和另外两个字符回车换行(“”)

参考一下不同python版本的魔数:

(pycdc的源码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
enum PycMagic {
MAGIC_1_0 = 0x00999902,
MAGIC_1_1 = 0x00999903, /* Also covers 1.2 */
MAGIC_1_3 = 0x0A0D2E89,
MAGIC_1_4 = 0x0A0D1704,
MAGIC_1_5 = 0x0A0D4E99,
MAGIC_1_6 = 0x0A0DC4FC,

MAGIC_2_0 = 0x0A0DC687,
MAGIC_2_1 = 0x0A0DEB2A,
MAGIC_2_2 = 0x0A0DED2D,
MAGIC_2_3 = 0x0A0DF23B,
MAGIC_2_4 = 0x0A0DF26D,
MAGIC_2_5 = 0x0A0DF2B3,
MAGIC_2_6 = 0x0A0DF2D1,
MAGIC_2_7 = 0x0A0DF303,

MAGIC_3_0 = 0x0A0D0C3A,
MAGIC_3_1 = 0x0A0D0C4E,
MAGIC_3_2 = 0x0A0D0C6C,
MAGIC_3_3 = 0x0A0D0C9E,
MAGIC_3_4 = 0x0A0D0CEE,
MAGIC_3_5 = 0x0A0D0D16,
MAGIC_3_5_3 = 0x0A0D0D17,
MAGIC_3_6 = 0x0A0D0D33,
MAGIC_3_7 = 0x0A0D0D42,
MAGIC_3_8 = 0x0A0D0D55,
MAGIC_3_9 = 0x0A0D0D61,
MAGIC_3_10 = 0x0A0D0D6F,
MAGIC_3_11 = 0x0A0D0DA7,
MAGIC_3_12 = 0x0A0D0DCB,
MAGIC_3_13 = 0x0A0D0DF3,

INVALID = 0,
};

对于任何一个 pyc 文件来说,前 16 字节是固定的(如果 Python 低于 3.7,那么前 12 个字节是固定的)

16字节后面就是Code Object对象序列化之后的数据

当 Python 源代码 (如 .py 文件) 被编译时,它会被转换成字节码 (bytecode)。这个 PyCodeObject 结构就是存储这些字节码以及执行它们所需的所有元数据(metadata)的容器

Code Object结构在Include\cpython\code.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Python 2.7 (Include/code.h) */
typedef struct {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */
int co_nlocals; /* #local variables */
int co_stacksize; /* #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
PyObject *co_code; /* instruction opcodes (bytes) */
PyObject *co_consts; /* list (constants used) */
PyObject *co_names; /* list of strings (names used) */
PyObject *co_varnames; /* tuple of strings (local variable names) */
PyObject *co_freevars; /* tuple of strings (free variable names) */
PyObject *co_cellvars; /* tuple of strings (cell variable names) */

/* 以下字段用于调试和回溯 */
PyObject *co_filename; /* string (where it was loaded from) */
PyObject *co_name; /* string (name, for reference) */
int co_firstlineno; /* first source line number */
PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */

/* 内部优化字段 */
void *co_zombieframe; /* for optimization only (see frameobject.c) */
PyObject *co_weakreflist; /* to support weakrefs to code objects */
} PyCodeObject;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Python 3.0 - 3.7 (Include/code.h) */
typedef struct {
PyObject_HEAD
int co_argcount;
int co_kwonlyargcount; /* 新增于 3.0 (PEP 3102) */
int co_nlocals;
int co_stacksize;
int co_flags;
int co_firstlineno;
PyObject *co_code;
PyObject *co_consts;
PyObject *co_names;
PyObject *co_varnames;
PyObject *co_freevars;
PyObject *co_cellvars;

Py_ssize_t *co_cell2arg; /* 内部映射 */
PyObject *co_filename;
PyObject *co_name;
PyObject *co_lnotab;
void *co_zombieframe;
PyObject *co_weakreflist;
void *co_extra; /* 新增于 3.6 (PEP 523) */
} PyCodeObject;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* Python 3.8 - 3.10 (Include/cpython/code.h) */
struct PyCodeObject {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */
int co_posonlyargcount; /* 新增于 3.8 (PEP 570) */
int co_kwonlyargcount; /* #keyword only arguments */
int co_nlocals; /* #local variables */
int co_stacksize; /* #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
int co_firstlineno; /* first source line number */
PyObject *co_code; /* instruction opcodes */
PyObject *co_consts; /* list (constants used) */
PyObject *co_names; /* list of strings (names used) */
PyObject *co_varnames; /* tuple of strings (local variable names) */
PyObject *co_freevars; /* tuple of strings (free variable names) */
PyObject *co_cellvars; /* tuple of strings (cell variable names) */

/* The rest aren't used in either hash or comparisons... */
Py_ssize_t *co_cell2arg;
PyObject *co_filename;
PyObject *co_name;
PyObject *co_lnotab;
void *co_zombieframe;
PyObject *co_weakreflist;
void *co_extra;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* Python 3.11+ (Include/cpython/code.h) - 概念性结构 */
struct PyCodeObject {
PyObject_HEAD
/*... 核心元数据... */
int co_argcount;
int co_posonlyargcount;
int co_kwonlyargcount;
int co_stacksize;
int co_flags;
int co_firstlineno;

/* 新的元数据对象 */
PyObject *co_filename;
PyObject *co_name;
PyObject *co_qualname; /* 新增于 3.11 [1, 6] */
PyObject *co_linetable; /* 新增于 3.11 (替换 co_lnotab) */
PyObject *co_exceptiontable; /* 新增于 3.11 [1, 6] */

/* 新的合并变量布局 */
int co_nlocalsplus; /* locals + cells + free 的总空间 */
int co_nlocals;
int co_nplaincellvars; /* */
int co_ncellvars;
int co_nfreevars;
PyObject *co_localsplusnames; /* 包含所有名称的单一元组 */
PyObject *co_localspluskinds; /* 描述名称类型的字节对象 */

PyObject *co_consts;
PyObject *co_names;
PyObject *co_weakreflist;

/* PEP 659 和内部字段 */
int co_warmup; /* 自适应解释器的预热计数器 [6, 18] */
char *co_code_adaptive; /* 指向可修改的、特化字节码的指针 */

/*
* co_code (PyObject*) 已被移除。
* 原始字节码现在是灵活数组成员(Flexible Array Member),
* 紧跟在C结构体之后分配,以实现最大数据局部性。
*/
char _co_code;
};

一般用不到,不看了

marshal类型代码0xE3 (或 TYPE_CODE) 明确表示“这是一个 PyCodeObject”

marshal 的另一个类型代码0x73 (ASCII ‘s’) 在 Python 3.x 的 marshal 中表示一个 bytes 对象。这标志着 co_code(即我们之前讨论的 C 结构中的 co_code_adaptive 字段)的开始。

Pyc加载过程

Python在命令行运行文件的时候,会进入到pymain_run_file函数中,然后调用pymain_run_file_obj函数。

pymain_run_file_obj函数主要调用_PyRun_AnyFileObject函数。

_PyRun_AnyFileObject主要调用_PyRun_SimpleFileObject函数。

_PyRun_SimpleFileObject函数会通过maybe_pyc_file函数来判断该文件是否是Pyc文件。

判断成功后会去通过run_pyc_file函数来执行该Pyc文件。

之后就是判断文件的头部,magic是否符合版本要求。

通过PyMarshal_ReadLastObjectFromFile函数读取Pyc文件,生成PyCodeObject对象。

随后run_eval_code_obj执行此PyCodeObject对象。

Python的Opcode在Include\opcode.h

关于Opcode的含义,可在https://docs.python.org/3.11/library/dis.html中找到。

Opcode的处理在Include\ceval.c中的_PyEval_EvalFrame。可以看到switch分支处理各个Opcode。

exe解包

这里主要看.py文件使用PyInstaller打包后生成的文件

我们可以使用pyinstxtractor进行解包,想看怎么解包的可以深入代码了解一下

AI对代码的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
以下是其核心运行流程的详细解析:

1. 启动与初始化 (main 和 __init__)
入口点:脚本从 if __name__ == '__main__': 开始执行,调用 main() 函数。

参数检查:main() 函数首先检查 sys.argv,确保用户提供了一个文件名作为参数。

实例化:创建一个 PyInstArchive 类的实例 arch,并将可执行文件的路径传递给它。

初始化 (__init__):PyInstArchive 的构造函数 (__init__) 会初始化一些变量:

self.filePath:存储目标文件路径。

self.pycMagic:一个4字节的占位符 b'\0' * 4,用于稍后存储 .pyc 文件的 "magic number"。

self.barePycList:一个空列表,用于存储那些写入时缺少 .pyc 头部的文件,以便后续修复。

2. 打开和检查文件 (open 和 checkFile)
这是找到 Magic Number 的关键步骤。

打开文件 (open):以二进制只读模式 ('rb') 打开目标可执行文件,并将文件句柄存储在 self.fPtr 中,同时获取文件总大小 self.fileSize。

定位 Magic (checkFile):

PyInstaller 会将所有数据(称为 "overlay")附加在可执行文件(如 PE 或 ELF)的末尾。

checkFile 函数定义了 PyInstaller 的 "cookie"(即 Magic Number):MAGIC = b'MEI\014\013\012\013\016'。

它不会从头开始搜索。相反,它从文件的末尾开始向前反向搜索。

它以 8192 字节为块(searchChunkSize)从后往前读取文件块。

在每个块中,它使用 data.rfind(self.MAGIC) 来查找这个 MAGIC 字符串的最后一次出现。

一旦找到,它会记录 MAGIC 字符串的起始位置(绝对文件偏移量)在 self.cookiePos 中,并停止搜索。

如果整个文件搜索完都找不到,脚本会报错并退出。

判断 PyInstaller 版本:

找到 MAGIC 后,脚本会紧接着读取 MAGIC 后面的一小块数据来判断 PyInstaller 的版本。

它读取 self.cookiePos + 24(即 PYINST20_COOKIE_SIZE)之后的 64 字节。

如果这 64 字节中包含 b'python' 字符串,则判定为 PyInstaller 2.1+ ( self.pyinstVer = 21)。

否则,判定为 PyInstaller 2.0 ( self.pyinstVer = 20)。

3. 获取 CArchive 存档信息 (getCArchiveInfo)
在知道 MAGIC 位置和 PyInstaller 版本后,脚本开始读取核心元数据。

定位:脚本跳转到 self.cookiePos。

解包 (Unpack):它使用 struct.unpack 来读取 "CArchive cookie" 结构体。这个结构体的长度根据 self.pyinstVer (2.0 或 2.1+) 而不同。

获取关键信息:从这个结构体中,它解析出:

lengthofPackage:整个 PyInstaller 存档的总长度。

toc:Table of Contents(目录表)的相对偏移量。

tocLen:TOC 的总长度。

pyver:打包时使用的 Python 版本(例如 308, 309, 310...)。

计算:

脚本解析 pyver 得到主版本号 self.pymaj 和次版本号 self.pymin。

它计算出整个数据包("overlay")在文件中的起始位置 self.overlayPos。

它计算出 TOC 的绝对文件位置 self.tableOfContentsPos = self.overlayPos + toc。

4. 解析TOC (Table of Contents) (parseTOC)
TOC 就像一个文件系统的索引,列出了存档中所有文件的信息。

定位:脚本跳转到 self.tableOfContentsPos。

循环读取:它在一个 while 循环中逐一读取TOC条目,直到读取的总字节数达到 self.tableOfContentsSize。

解析条目:每个 TOC 条目都包含:

entryPos:文件数据的相对偏移量(相对于 overlayPos)。

cmprsdDataSize:压缩后的数据大小。

uncmprsdDataSize:解压后的大小。

cmprsFlag:压缩标志(1 表示 zlib 压缩)。

typeCmprsData:一个字符,表示文件类型(如 's'脚本, 'm'/'M'模块, 'z'PYZ存档, 'b'二进制文件等)。

name:文件名。

存储:脚本将每个解析出的条目包装成一个 CTOCEntry 对象,并追加到 self.tocList 列表中。

5. 提取文件 (extractFiles)
这是实际的解包过程。

创建目录:在当前目录下创建一个以可执行文件名命名的新目录(例如 my_app.exe_extracted)。

进入目录:os.chdir() 进入这个新创建的目录。

遍历TOC:脚本遍历 self.tocList 中的每一个 CTOCEntry 对象。

提取数据:对于每个条目:

跳转到文件数据位置 (entry.position)。

读取 entry.cmprsdDataSize 字节的数据。

如果 entry.cmprsFlag == 1,则使用 zlib.decompress() 解压数据。

根据 entry.typeCmprsData(文件类型)决定如何处理:

'd' 或 'o':忽略(运行时选项)。

's' (脚本):这是 Python 脚本(通常是入口点),调用 _writePyc 将其保存为 .pyc 文件。

'm' 或 'M' (模块):这是 Python 模块。

PYC Magic 探测:脚本会检查数据的前几个字节。对于 PyInstaller 5.3 之前的版本,.pyc 文件会自带头部。如果脚本在这里发现了一个完整的 .pyc 头部,它会立即提取 self.pycMagic (例如 b'U\r\r\n' 或其他值),并使用 _writeRawData 完整写入。

对于 PyInstaller 5.3 及之后的版本,这里是没有头部的 "bare" pyc,脚本会调用 _writePyc 写入。

'z' 或 'Z' (PYZ 存档):这是一个“存档中的存档”,包含了大量的 Python 模块。脚本首先使用 _writeRawData 将其原样提取出来。

其他类型(如二进制 .dll, .so 或数据文件):使用 _writeRawData 原样写入磁盘。

处理 PYZ:如果提取了 'z' 或 'Z' 类型的文件,脚本会立即调用 _extractPyz 函数来处理这个刚提取的 PYZ 文件。

6. PYZ 存档提取 (_extractPyz)
这是提取的核心之一,因为大多数 Python 模块都在 PYZ 存档里。

打开 PYZ:打开刚提取的 PYZ 文件(例如 PYZ-00.pyz)。

获取 PYC Magic:PYZ 文件的开头(在 b'PYZ\0' 标记之后)就存储了正确的 .pyc magic number。

这是最可靠的获取 self.pycMagic 的地方。如果 self.pycMagic 仍然是 b'\0' * 4,脚本会在这里设置它。

版本检查:非常关键的一步。脚本会比较 self.pymaj/self.pymin(从可执行文件中读取的)和 sys.version_info(当前运行 pyinstxtractor.py 脚本的 Python 版本)。

如果两个版本不匹配,marshal.load() 会失败。脚本会打印警告,并跳过 PYZ 的提取,只保留那个 .pyz 文件。

解组 (Unmarshal):如果版本匹配,脚本会读取 PYZ 内的 TOC(它本身是用 marshal 序列化的),使用 marshal.load() 将其加载为一个字典(或列表)。

提取 PYZ 内容:遍历这个字典,它包含了 PYZ 内部所有模块的 (ispkg, pos, length)。脚本会跳转到 pos,读取 length 字节,解压数据,并调用 _writePyc 将其保存为对应的 .pyc 文件(例如 requests/api.pyc)。

7. PYC 头部修复 (_writePyc 和 _fixBarePycs)
这是一个巧妙的“两遍”系统,用于处理 PyInstaller 5.3+ 带来的 "bare" pyc(即没有 .pyc 头部的原始 marshal 数据)。

_writePyc (写入):当 extractFiles 或 _extractPyz 需要写入一个 .pyc 文件时:

它首先写入 self.pycMagic。注意:此时 self.pycMagic 可能是 b'\0' * 4(如果 PYZ 还没被处理)。

然后它根据 Python 版本写入时间戳或哈希值的占位符(通常也是全零)。

最后写入解压后的 Python 字节码数据。

如果 self.pycMagic 当时是 b'\0' * 4,这个文件就会被添加到 self.barePycList 列表中。

_fixBarePycs (修复):

在 extractFiles 的最后,它会调用 _fixBarePycs()。

此时,PYZ 存档(如果有的话)已经被处理过了,self.pycMagic 一定已经被设置成了正确的值。

脚本会遍历 self.barePycList 中的所有文件名。

它以 r+b (读写二进制) 模式重新打开这些 "bare" .pyc 文件。

它跳转到文件开头,并用正确的 self.pycMagic 覆盖掉开头的4个 \0 字节。

8. 结束 (close)
所有文件提取并修复完毕后,main() 函数中的 arch.close() 被调用,关闭可执行文件的文件句柄,脚本执行完毕。

那么这个时候就有人问了,有坏蛋改了magic number怎么办,???

我们来看一下pyinstxtractor ,他是怎么识别的呢?:

pyinstxtractor 的分析显示,MAGIC 只是 “cookie” 结构体的第一部分。我们来看看 PyInstaller 2.1+ 的结构:

1
2
3
4
5
6
7
8
9

struct CArchiveCookie {
char magic[8]; // 8 字节 (这就是我们要找的)
unsigned int lengthofPackage; // 4 字节
unsigned int toc; // 4 字节
unsigned int tocLen; // 4 字节
int pyver; // 4 字节
char pylibname[64]; // 64 字节 (这里面包含 'pythonXX.dll' 或 'libpythonX.X')
};

攻击者很可能只修改了 magic[8],而保留了后面部分的结构。

而且,这个 libpythonX.X 文件(在 Windows 上通常是 pythonXX.dll,例如 python310.dll;在 Linux 上是 libpythonX.X.so,例如 libpython3.10.so)是 Python 解释器本身, 是PyInstaller 应用的“引擎”

pylibname 字段就是 Bootloader 用来识别哪个文件是“引擎”的标签。如果这个字段被篡改,或者对应的文件在解压后丢失,Bootloader 就无法启动 Python 运行时,程序会立刻崩溃。

反序列化 pickle marshal

pickle:

pickle是Python的一个库,可以对一个对象进行序列化和反序列化操作。

“pickling” 是将 Python 对象及其所拥有的层次结构转化为一个字节流的过程,而 “unpickling” 是相反的操作,会将(来自一个 binary file 或者 bytes-like object 的)字节流转化回一个对象层次结构。 pickling(和 unpickling)也被称为“序列化”, “编组” [1] 或者 “平面化”。而为了避免混乱,此处采用术语 “封存 (pickling)” 和 “解封 (unpickling)”。

  • 与json相比,pickle以二进制储存,不易人工阅读;json可以跨语言,而pickle是Python专用的;pickle能表示python几乎所有的类型(包括自定义类型),json只能表示一部分内置类型且不能表示自定义类型。

pickle 具有两个重要的函数:

  • 一个是dump(),作用是接受一个文件句柄和一个数据对象作为参数,把数据对象以特定的格式保存到给定的文件中;

  • 另一个是load(),作用是从文件中取出已保存的对象,pickle 知道如何恢复这些对象到他们本来的格式。

使用Fickling工具工具进行反序列化即可:

trailofbits/fickling: A Python pickling decompiler and static analyzer

marshal:

提供 marshal 模块主要是为了支持读写 .pyc 形式“伪编译”代码的 Python 模块。 因此,Python 维护者保留在必要时以不向下兼容的方式修改 marshal 格式的权利。 代码对象的格式在 Python 版本之间不保证兼容,即使格式的版本是相同的。 在不正确的 Python 版本中反序列化代码对象是未定义的行为。 如果你要序列化和反序列化 Python 对象,请改用 pickle 模块 —— 具有类似的性能,保证版本独立性,并且 pickle 还支持比 marshal 更丰富种类的对象。

Cython逆向

不写了

对于cython的基础逆向分析(1)-先知社区

pyd_hook | fineのblog

Number Protocol — Python 3.14.0 documentation

源码混淆

pyminifier

pyminifier是一个对Python文件进行压缩、混淆的工具,项目地址 https://github.com/liftoff/pyminifier

Oxyry Python Obfuscator

Oxyry Python Obfuscator是一个在线混淆代码的工具,地址是 http://pyob.oxyry.com/

注意目前Oxyry也只能混淆单个Python文件,测试过混淆后代码可用。

Opy

Opy也是一个代码混淆工具,可以对整个目录的Python文件进行混淆处理,并且支持定义混淆格式,项目地址 https://github.com/QQuick/Opy

经过测试,混淆后的Python项目不可直接执行,不建议使用。

Lambda

邱奇编码(Church Encoding)是把数据和运算符嵌入到lambda演算内的一种方式,最常见的形式是邱奇数,它是使用lambda符号的自然数的表示法。这是一种图灵完备的编码。

反混淆神秘小工具: https://github.com/owieczka/lambda-calculus-python

pyarmor

PyArmor 的基本概念包括加密、混淆和授权管理:

加密:加密是指将 Python 脚本转换为加密格式,使其不可读,从而防止源代码泄露。 混淆:混淆是指对 Python 脚本进行变换和重构,使其难以理解和分析,从而增加破解的难度。 授权管理:授权管理是指对加密后的 Python 脚本进行授权管理,限制脚本的运行权限和有效期限。

https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot/releases

pyc混淆

pyc字节码混淆

个混淆的手段就是修改 co_code 字段中的 opcode 序列,可以添加一些加载超出范围的变量的指令,再用一些指令去跳过这些会出错的指令,这样执行的时候就不会出错了,但是反编译工具就不能正常工作了。

1
2
3
4
0 LOAD_NAME                0 (print)
2 LOAD_CONST 0 ('Hello World! --idiot.')
4 CALL_FUNCTION 1
6 POP_TOP

以上是由 Python 3.7 生成的 pyc 的一段 opcode 序列,这时考虑在它的前面加两条指令来进行混淆。

1
2
3
4
5
6
0 JUMP_ABSOLUTE            4
2 LOAD_CONST 255
4 LOAD_NAME 0 (print)
6 LOAD_CONST 0 ('Hello, world')
8 CALL_FUNCTION 1
10 POP_TOP

其中 JUMP_ABSOLUTE 4 表示直接跳转到 offset 为4的位置去执行指令,也就是插入的第二条指令 LOAD_CONST 255 并不会被执行,所以所以也并不会报错。但是对于反编译工具来说,这就是一个错误了,直接导致了反编译的失败。

实现:

根据上面的那个思路,我们可以插入许多这样类似的指令,任意的不合法指令(其实随机数据都可以),然后用一些 JUMP 指令去跳过这样的不合法指令,上面的 JUMP_ABSOLUTE 只是一个简单的例子。甚至我们可以跳转到一些自行添加的虚假分支再跳转到到真实的分支(参考 ROP 的思路)。

Python 的 opcode 中与 JUMP 相关的有:

1
2
3
4
5
6
'JUMP_FORWARD',
'JUMP_IF_FALSE_OR_POP',
'JUMP_IF_TRUE_OR_POP',
'JUMP_ABSOLUTE',
'POP_JUMP_IF_FALSE',
'POP_JUMP_IF_TRUE',

原则上这六个都可以使用,但是实际上为了方便的话,其实还是 JUMP_FORWARDJUMP_ABSOLUTE 比较好用,因为其他的 JUMP 指令存在一些当前栈顶元素判断的问题(要做也可以,只不过实现同样的功能可能需要写更多的指令)。

在添加混淆指令的时候可能会遇到的问题:

  • 首先是 Python 版本的问题,Python3.6 之前使用的是变长指令,3.6及之后都是用的是定长指令了,这样对于不同的版本需要有不用的处理;
  • 由于添加了指令,一些原本存在的绝对跳转指令就会失效,所以需要对原本存在的绝对跳转指令计算偏;
  • 不定长指令的参数长度是两个字节,而定长指令的参数只有一个字节,可能存在参数长度不够用的时候,这个时候可以使用 EXTENDED_ARG 指令去扩展参数的长度,最多可以有四个字节。
  • 跳转的混淆最好还是不要从循环内到循环外或者循环外到循环内。其实最好是根据 co_lnotab 字段中的指令偏移和行号来插入混淆指令,不在属于同一行的指令中间插入,这样可以避免一些可能存在的问题。

解决方法:

先使用pycdas查看汇编,然后根据pyc文件布局(不同版本的pyc文件布局不一样)修改数据:

image-20251031170702100

opcode换表

libpythonX.X 文件(在 Windows 上通常是 pythonXX.dll,例如 python310.dll;在 Linux 上是 libpythonX.X.so,例如 libpython3.10.so)放到ida里,去这个函数_PyEval_EvalFrame找switch_case,得到变换之后的opcode

2025 华为杯研究生赛oooops:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308

import sys
import marshal
import opcode
import types

#
# 这是您提供的置换表 (Permuted Map) - 保持不变
#
permuted_map_str = {
"0x1": "INPLACE_TRUE_DIVIDE",
"0x2": "INPLACE_ADD",
"0x3": "WITH_CLEANUP_FINISH",
"0x4": "STORE_SUBSCR",
"0x5": "ROT_THREE",
"0x9": "INPLACE_LSHIFT",
"0xA": "BINARY_LSHIFT",
"0xB": "BINARY_TRUE_DIVIDE",
"0xC": "BINARY_XOR",
"0xF": "BEFORE_ASYNC_WITH",
"0x10": "PRINT_EXPR",
"0x11": "INPLACE_MATRIX_MULTIPLY",
"0x13": "BINARY_SUBSCR",
"0x14": "BINARY_SUBTRACT",
"0x16": "INPLACE_AND",
"0x17": "NOP",
"0x18": "GET_ITER",
"0x19": "BINARY_ADD",
"0x1A": "GET_AWAITABLE",
"0x1B": "UNARY_INVERT",
"0x1C": "BINARY_MODULO",
"0x1D": "BREAK_LOOP",
"0x32": "BINARY_MATRIX_MULTIPLY",
"0x33": "BINARY_AND",
"0x34": "INPLACE_FLOOR_DIVIDE",
"0x37": "INPLACE_MODULO",
"0x38": "UNARY_NOT",
"0x39": "UNARY_POSITIVE",
"0x3B": "DUP_TOP_TWO",
"0x3C": "END_FINALLY",
"0x3D": "YIELD_FROM",
"0x3E": "DUP_TOP",
"0x3F": "UNARY_NEGATIVE",
"0x40": "POP_TOP",
"0x41": "BINARY_MULTIPLY",
"0x42": "GET_AITER",
"0x43": "INPLACE_OR",
"0x44": "IMPORT_STAR",
"0x45": "ROT_TWO",
# "0x46": "LABEL_635",
"0x46": "YIELD_VALUE",
"0x47": "INPLACE_RSHIFT",
"0x48": "GET_ANEXT",
"0x49": "BINARY_POWER",
"0x4B": "INPLACE_POWER",
# "0x4C": "LABEL_866",
"0x4C": "RETURN_VALUE",
"0x4D": "POP_BLOCK",
"0x4E": "DELETE_SUBSCR",
"0x4F": "INPLACE_MULTIPLY",
"0x50": "POP_EXCEPT",
"0x51": "BINARY_OR",
"0x52": "INPLACE_SUBTRACT",
"0x53": "BINARY_FLOOR_DIVIDE",
"0x54": "INPLACE_XOR",
"0x56": "BINARY_RSHIFT",
"0x57": "LOAD_BUILD_CLASS",
"0x58": "GET_YIELD_FROM_ITER",
"0x59": "WITH_CLEANUP_START",
"0x5A": "UNPACK_SEQUENCE",
"0x5B": "STORE_NAME",
"0x5C": "JUMP_IF_TRUE_OR_POP",
"0x5D": "POP_JUMP_IF_FALSE",
"0x5E": "SETUP_FINALLY",
"0x5F": "FOR_ITER",
"0x60": "BUILD_TUPLE_UNPACK",
"0x61": "POP_JUMP_IF_TRUE",
"0x62": "BUILD_LIST",
"0x64": "LOAD_FAST",
"0x65": "LOAD_CLOSURE",
"0x66": "IMPORT_FROM",
"0x67": "BUILD_SLICE",
"0x68": "COMPARE_OP",
"0x69": "LOAD_CLASSDEREF",
"0x6A": "BUILD_TUPLE",
"0x6B": "SETUP_LOOP",
"0x6C": "LOAD_DEREF",
"0x6D": "BUILD_LIST_UNPACK",
"0x6E": "JUMP_IF_FALSE_OR_POP",
"0x6F": "SETUP_EXCEPT",
"0x70": "DELETE_ATTR",
"0x71": "MAKE_FUNCTION",
"0x72": "SET_ADD",
"0x73": "LIST_APPEND",
"0x74": "JUMP_FORWARD",
"0x77": "LOAD_CONST",
"0x78": "LOAD_GLOBAL",
"0x79": "STORE_FAST",
"0x7A": "BUILD_SET",
"0x7C": "LOAD_ATTR",
"0x7D": "BUILD_MAP",
"0x7E": "DELETE_FAST",
"0x82": "SETUP_WITH",
"0x83": "CALL_FUNCTION",
"0x84": "BUILD_SET_UNPACK",
"0x85": "MAP_ADD",
"0x86": "UNPACK_EX",
"0x87": "EXTENDED_ARG",
"0x88": "STORE_DEREF",
"0x89": "CONTINUE_LOOP",
"0x8A": "SETUP_ASYNC_WITH",
"0x8C": "CALL_FUNCTION_VAR",
"0x8D": "CALL_FUNCTION_KW",
"0x8E": "CALL_FUNCTION_VAR_KW",
"0x8F": "RAISE_VARARGS",
"0x90": "IMPORT_NAME",
"0x91": "MAKE_CLOSURE",
"0x92": "DELETE_GLOBAL",
"0x93": "BUILD_MAP_UNPACK",
"0x94": "DELETE_NAME",
"0x95": "DELETE_DEREF",
"0x96": "STORE_ATTR",
"0x97": "JUMP_ABSOLUTE",
"0x98": "STORE_GLOBAL",
"0x99": "BUILD_MAP_UNPACK_WITH_CALL",
"0x9A": "LOAD_NAME"
}


# --- 函数1:已替换 ---
def build_reverse_map():
"""
构建一个字典映射:
permuted_opcode_num -> standard_opcode_num
"""
standard_opmap = opcode.opmap
reverse_map = {}

for hex_str, name in permuted_map_str.items():
if name in standard_opmap:
permuted_num = int(hex_str, 16)
standard_num = standard_opmap[name]
reverse_map[permuted_num] = standard_num

return reverse_map


def patch_code_object(code_obj, reverse_map):
"""
递归地修复一个 Code 对象,方法是手动解析和重写其字节码 (co_code)
(*** Python 3.5 修复版 ***)
"""

# 1. 修复所有嵌套在 co_consts 里的 Code 对象
new_consts = []
for const in code_obj.co_consts:
if isinstance(const, types.CodeType):
# 递归调用
new_consts.append(patch_code_object(const, reverse_map))
else:
new_consts.append(const)

# 2. 翻译 (patch) 当前 Code 对象的字节码
new_bytecode = bytearray()
original_bytecode = code_obj.co_code

# Python 3.5 的常量
HAVE_ARGUMENT = 90

i = 0
while i < len(original_bytecode):
# 1. 读取混淆的操作码
permuted_op = original_bytecode[i]
i += 1

# 2. 查找它对应的标准操作码
if permuted_op not in reverse_map:
print("[!] 警告: 混淆的操作码 {} (0x{:x}) 不在置换表中。".format(permuted_op, permuted_op))
standard_op = permuted_op
else:
standard_op = reverse_map[permuted_op]

# 3. 将 *标准* 操作码写入新字节码
new_bytecode.append(standard_op)

# 4. 检查 *标准* 操作码是否需要参数
if standard_op >= HAVE_ARGUMENT:

# --- 这是关键的修复 ---
# Python 3.5 的参数是 2 字节长的
# --------------------------

if i + 1 >= len(original_bytecode): # 检查是否有足够的字节
print("[!] 错误: 字节码在 {} 处意外结束 (需要 2 个参数字节)".format(i))
break

# 5. 读取 2 个参数字节
arg_byte_1 = original_bytecode[i]
i += 1
arg_byte_2 = original_bytecode[i]
i += 1

# 6. 将 2 个参数字节 *原封不动* 地写入新字节码
new_bytecode.append(arg_byte_1)
new_bytecode.append(arg_byte_2)

# 3. 创建并返回一个新的、已修复的 Code 对象
# (这部分与您原始代码相同)
try:
# Python 3.5, 3.6, 3.7 的 CodeType 构造函数
return types.CodeType(
code_obj.co_argcount,
code_obj.co_kwonlyargcount,
code_obj.co_nlocals,
code_obj.co_stacksize,
code_obj.co_flags,
bytes(new_bytecode), # <-- 注入修复后的字节码
tuple(new_consts), # <-- 注入修复后的常量
code_obj.co_names,
code_obj.co_varnames,
code_obj.co_filename,
code_obj.co_name,
code_obj.co_firstlineno,
code_obj.co_lnotab,
code_obj.co_freevars,
code_obj.co_cellvars
)
except TypeError:
# 尝试 Python 3.8+ 的构造函数 (以防万一)
try:
return types.CodeType(
code_obj.co_argcount,
code_obj.co_posonlyargcount, # 3.8+ 新增
code_obj.co_kwonlyargcount,
code_obj.co_nlocals,
code_obj.co_stacksize,
code_obj.co_flags,
bytes(new_bytecode),
tuple(new_consts),
code_obj.co_names,
code_obj.co_varnames,
code_obj.co_filename,
code_obj.co_name,
code_obj.co_firstlineno,
code_obj.co_lnotab,
code_obj.co_freevars,
code_obj.co_cellvars
)
except Exception as e:
print("[!] 创建 Code 对象时出错: {}".format(e))
print("[!] 请确保您用于运行此脚本的 Python 版本与 .pyc 文件的版本(或结构)兼容。")
sys.exit(1)
except Exception as e:
print("[!] 创建 Code 对象时出错: {}".format(e))
print("[!] 请确保您用于运行此脚本的 Python 版本与 .pyc 文件的版本(或结构)兼容。")
sys.exit(1)

def main():
if len(sys.argv) != 3:
print("用法: python {} <输入的.pyc> <输出的.pyc>".format(sys.argv[0]))
print("示例: python {} ps.pyc ooops_patched.pyc".format(sys.argv[0]))
sys.exit(1)

input_file = sys.argv[1]
output_file = sys.argv[2]

print("[+] 正在构建 Opcode 反置换字典...")
# --- 修改点 ---
reverse_map = build_reverse_map()

try:
with open(input_file, 'rb') as f:
print("[+] 正在读取: {}".format(input_file))

# 假设 Py 3.5,头部为 12 字节
pyc_header = f.read(12)

# 检查 magic number 是否匹配 3.5 (可选但推荐)
# 3.5 的 magic number 是 0x330d0d0a
# if pyc_header[:4] != b'\x33\x0d\x0d\x0a':
# print("[!] 警告: 这似乎不是 Python 3.5 的 .pyc 文件。")

original_code_obj = marshal.load(f)

print("[+] 正在递归 patch Code 对象 (使用手动解析)...")
# --- 修改点 ---
patched_code_obj = patch_code_object(original_code_obj, reverse_map)

with open(output_file, 'wb') as f:
print("[+] 正在写入: {}".format(output_file))
f.write(pyc_header)
marshal.dump(patched_code_obj, f)

# --- 修改点:移除了导致GBK错误的 emoji ---
print("\n[+] 修复完成!")
print("您现在可以尝试使用标准工具 (如 uncompyle6) 来反编译 '{}'".format(output_file))

except Exception as e:
print("[!] 发生严重错误: {}".format(e))
if isinstance(e, FileNotFoundError):
print("[!] 找不到文件: {}".format(input_file))
elif isinstance(e, (ImportError, EOFError, ValueError)):
print("[!] 'marshal' 加载失败。这通常意味着 .pyc 文件已损坏,或者 Python 版本不兼容。")
print("[!] 请确保您使用的是 Python 3.5 来运行此脚本。")


if __name__ == "__main__":
main()