Python EXE Unpacker 是一个用于解包和分析 Python 编译的 Windows 可执行文件(.exe)的工具,下面是几种Python EXE Unpacker的安装方式。

方法一:通过 pip 直接安装

直接在cmd中运行以下命令

1
pip install python-exe-unpacker

验证安装

1
python -m python_exe_unpacker --help

方法二:从GitHub下载

获取源码

直接从GitHub获取源码并进行解压,项目地址WithSecureLabs/python-exe-unpacker: A helper script for unpacking and decompiling EXEs compiled from python code.

修改依赖库

直接解压后你会发现运行出错了,原因是No module named 'imp',尝试pip下载imp库,但是全都是报错了,各种原因都有。image-20260408143630008

上网查了一下是因为从 Python 3.4 开始,imp模块已被弃用,在较新的 Python 版本中已被移除。pyinstxtractor.py脚本使用了旧的 imp模块,所以我们准备用importlib模块来代替imp模块,直接把pyinstxtractor.py里面的代码全部替换为如下代码

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 os
import struct
import marshal
import zlib
import sys
import types
from uuid import uuid4 as uniquename
import importlib
import importlib.util
import importlib.machinery


# 兼容性处理:为旧代码创建imp模块的模拟
if sys.version_info >= (3, 4):
class ImpCompat:
@staticmethod
def get_magic():
"""返回当前Python的魔数"""
import importlib.util
return importlib.util.MAGIC_NUMBER

@staticmethod
def find_module(name, path=None):
"""模拟imp.find_module"""
try:
spec = importlib.util.find_spec(name, path)
if spec is not None and spec.loader is not None:
return spec.loader, spec.origin, None
except Exception:
pass
raise ImportError(f"No module named {name}")

@staticmethod
def load_module(name, file, filename, details):
"""模拟imp.load_module"""
spec = importlib.util.spec_from_file_location(name, filename, loader=file)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module

# 将模拟的imp模块添加到sys.modules
sys.modules['imp'] = ImpCompat()
else:
import imp


class CTOCEntry:
def __init__(self, position, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name):
self.position = position
self.cmprsdDataSize = cmprsdDataSize
self.uncmprsdDataSize = uncmprsdDataSize
self.cmprsFlag = cmprsFlag
self.typeCmprsData = typeCmprsData
self.name = name


class PyInstArchive:
PYINST20_COOKIE_SIZE = 24 # For pyinstaller 2.0
PYINST21_COOKIE_SIZE = 24 + 64 # For pyinstaller 2.1+
MAGIC = b'MEI\014\013\012\013\016' # Magic number which identifies pyinstaller

def __init__(self, path):
self.filePath = path


def open(self):
try:
self.fPtr = open(self.filePath, 'rb')
self.fileSize = os.stat(self.filePath).st_size
except Exception as e:
print(f'[*] Error: Could not open {self.filePath}: {e}')
return False
return True


def close(self):
try:
self.fPtr.close()
except:
pass


def checkFile(self):
print('[*] Processing {0}'.format(self.filePath))
# Check if it is a 2.0 archive
self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET)
magicFromFile = self.fPtr.read(len(self.MAGIC))

if magicFromFile == self.MAGIC:
self.pyinstVer = 20 # pyinstaller 2.0
print('[*] Pyinstaller version: 2.0')
return True

# Check for pyinstaller 2.1+ before bailing out
self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET)
magicFromFile = self.fPtr.read(len(self.MAGIC))

if magicFromFile == self.MAGIC:
print('[*] Pyinstaller version: 2.1+')
self.pyinstVer = 21 # pyinstaller 2.1+
return True

print('[*] Error : Unsupported pyinstaller version or not a pyinstaller archive')
return False


def getCArchiveInfo(self):
try:
if self.pyinstVer == 20:
self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET)

# Read CArchive cookie
(magic, lengthofPackage, toc, tocLen, self.pyver) = \
struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE))

elif self.pyinstVer == 21:
self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET)

# Read CArchive cookie
(magic, lengthofPackage, toc, tocLen, self.pyver, pylibname) = \
struct.unpack('!8siiii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE))

except Exception as e:
print(f'[*] Error : The file is not a pyinstaller archive: {e}')
return False

print('[*] Python version: {0}'.format(self.pyver))

# Overlay is the data appended at the end of the PE
self.overlaySize = lengthofPackage
self.overlayPos = self.fileSize - self.overlaySize
self.tableOfContentsPos = self.overlayPos + toc
self.tableOfContentsSize = tocLen

print('[*] Length of package: {0} bytes'.format(self.overlaySize))
return True


def parseTOC(self):
# Go to the table of contents
self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET)

self.tocList = []
parsedLen = 0

# Parse table of contents
while parsedLen < self.tableOfContentsSize:
(entrySize, ) = struct.unpack('!i', self.fPtr.read(4))
nameLen = struct.calcsize('!iiiiBc')

(entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = \
struct.unpack( \
'!iiiBc{0}s'.format(entrySize - nameLen), \
self.fPtr.read(entrySize - 4))

name = name.decode('utf-8').rstrip('\0')
if len(name) == 0:
name = str(uniquename())
print('[!] Warning: Found an unamed file in CArchive. Using random name {0}'.format(name))

self.tocList.append( \
CTOCEntry( \
self.overlayPos + entryPos, \
cmprsdDataSize, \
uncmprsdDataSize, \
cmprsFlag, \
typeCmprsData, \
name \
))

parsedLen += entrySize
print('[*] Found {0} files in CArchive'.format(len(self.tocList)))


def extractFiles(self, custom_dir=None):
print('[*] Beginning extraction...please standby')
if custom_dir is None:
extractionDir = os.path.join(os.getcwd(), os.path.basename(self.filePath) + '_extracted')

if not os.path.exists(extractionDir):
os.mkdir(extractionDir)

os.chdir(extractionDir)
else:
if not os.path.exists(custom_dir):
os.makedirs(custom_dir)
os.chdir(custom_dir)

for entry in self.tocList:
basePath = os.path.dirname(entry.name)
if basePath != '':
# Check if path exists, create if not
if not os.path.exists(basePath):
os.makedirs(basePath)

self.fPtr.seek(entry.position, os.SEEK_SET)
data = self.fPtr.read(entry.cmprsdDataSize)

if entry.cmprsFlag == 1:
data = zlib.decompress(data)
# Malware may tamper with the uncompressed size
# Comment out the assertion in such a case
assert len(data) == entry.uncmprsdDataSize # Sanity Check

with open(entry.name, 'wb') as f:
f.write(data)

if entry.typeCmprsData == b'z':
self._extractPyz(entry.name)


def _extractPyz(self, name):
dirName = name + '_extracted'
# Create a directory for the contents of the pyz
if not os.path.exists(dirName):
os.mkdir(dirName)

with open(name, 'rb') as f:
pyzMagic = f.read(4)
assert pyzMagic == b'PYZ\0' # Sanity Check

pycHeader = f.read(4) # Python magic value

# 获取当前Python的魔数进行比较
if sys.version_info >= (3, 4):
import importlib.util
current_magic = importlib.util.MAGIC_NUMBER
else:
import imp
current_magic = imp.get_magic()

if current_magic != pycHeader:
print('[!] Warning: The script is running in a different python version than the one used to build the executable')
print(' Run this script in Python{0} to prevent extraction errors(if any) during unmarshalling'.format(self.pyver))

(tocPosition, ) = struct.unpack('!i', f.read(4))
f.seek(tocPosition, os.SEEK_SET)

try:
toc = marshal.load(f)
except Exception as e:
print(f'[!] Unmarshalling FAILED. Cannot extract {name}: {e}')
print('[!] Extracting remaining files.')
return

print('[*] Found {0} files in PYZ archive'.format(len(toc)))

# From pyinstaller 3.1+ toc is a list of tuples
if type(toc) == list:
toc = dict(toc)

for key in toc.keys():
(ispkg, pos, length) = toc[key]
f.seek(pos, os.SEEK_SET)

fileName = key
try:
# for Python > 3.3 some keys are bytes object some are str object
if isinstance(key, bytes):
fileName = key.decode('utf-8')
except Exception as e:
print(f'[!] Warning: Could not decode filename {key}: {e}')

# Make sure destination directory exists, ensuring we keep inside dirName
destName = os.path.join(dirName, fileName.replace("..", "__"))
destDirName = os.path.dirname(destName)
if not os.path.exists(destDirName):
os.makedirs(destDirName)

try:
data = f.read(length)
data = zlib.decompress(data)
except Exception as e:
print(f'[!] Error: Failed to decompress {fileName}, probably encrypted. Extracting as is: {e}')
open(destName + '.pyc.encrypted', 'wb').write(data)
continue

with open(destName + '.pyc', 'wb') as pycFile:
pycFile.write(pycHeader) # Write pyc magic
pycFile.write(b'\0' * 4) # Write timestamp
if self.pyver >= 33:
pycFile.write(b'\0' * 4) # Size parameter added in Python 3.3
pycFile.write(data)


def main():
if len(sys.argv) < 2:
print('[*] Usage: pyinstxtractor.py <filename>')
sys.exit(1)

else:
arch = PyInstArchive(sys.argv[1])
if arch.open():
if arch.checkFile():
if arch.getCArchiveInfo():
arch.parseTOC()
arch.extractFiles()
arch.close()
print('[*] Successfully extracted pyinstaller archive: {0}'.format(sys.argv[1]))
print('')
print('You can now use a python decompiler on the pyc files within the extracted directory')
return
arch.close()


if __name__ == '__main__':
main()

运行命令安装importlib

1
pip install importlib

这时候你的Python EXE Unpacker就已经安装成功了!

基本使用方法

安装完成后,可以使用以下命令解包 EXE 文件:

1
python pyinstxtractor.py ...exe

解包成功后,会在当前目录生成一个以 _extracted结尾的文件夹(如 example.exe_extracted),其中包含提取的文件和反编译的源代码。