· 6 years ago · Jun 27, 2019, 07:08 PM
1"""
2PyInstaller Extractor v1.8 (Supports pyinstaller 3.2, 3.1, 3.0, 2.1, 2.0)
3Author : Extreme Coders
4E-mail : extremecoders(at)hotmail(dot)com
5Web : https://0xec.blogspot.com
6Date : 28-April-2017
7Url : https://sourceforge.net/projects/pyinstallerextractor/
8
9For any suggestions, leave a comment on
10https://forum.tuts4you.com/topic/34455-pyinstaller-extractor/
11
12This script extracts a pyinstaller generated executable file.
13Pyinstaller installation is not needed. The script has it all.
14
15For best results, it is recommended to run this script in the
16same version of python as was used to create the executable.
17This is just to prevent unmarshalling errors(if any) while
18extracting the PYZ archive.
19
20Usage : Just copy this script to the directory where your exe resides
21 and run the script with the exe file name as a parameter
22
23C:\path\to\exe\>python pyinstxtractor.py <filename>
24$ /path/to/exe/python pyinstxtractor.py <filename>
25
26Licensed under GNU General Public License (GPL) v3.
27You are free to modify this source.
28
29CHANGELOG
30================================================
31
32Version 1.1 (Jan 28, 2014)
33-------------------------------------------------
34- First Release
35- Supports only pyinstaller 2.0
36
37Version 1.2 (Sept 12, 2015)
38-------------------------------------------------
39- Added support for pyinstaller 2.1 and 3.0 dev
40- Cleaned up code
41- Script is now more verbose
42- Executable extracted within a dedicated sub-directory
43
44(Support for pyinstaller 3.0 dev is experimental)
45
46Version 1.3 (Dec 12, 2015)
47-------------------------------------------------
48- Added support for pyinstaller 3.0 final
49- Script is compatible with both python 2.x & 3.x (Thanks to Moritz Kroll @ Avira Operations GmbH & Co. KG)
50
51Version 1.4 (Jan 19, 2016)
52-------------------------------------------------
53- Fixed a bug when writing pyc files >= version 3.3 (Thanks to Daniello Alto: https://github.com/Djamana)
54
55Version 1.5 (March 1, 2016)
56-------------------------------------------------
57- Added support for pyinstaller 3.1 (Thanks to Berwyn Hoyt for reporting)
58
59Version 1.6 (Sept 5, 2016)
60-------------------------------------------------
61- Added support for pyinstaller 3.2
62- Extractor will use a random name while extracting unnamed files.
63- For encrypted pyz archives it will dump the contents as is. Previously, the tool would fail.
64
65Version 1.7 (March 13, 2017)
66-------------------------------------------------
67- Made the script compatible with python 2.6 (Thanks to Ross for reporting)
68
69Version 1.8 (April 28, 2017)
70-------------------------------------------------
71- Support for sub-directories in .pyz files (Thanks to Moritz Kroll @ Avira Operations GmbH & Co. KG)
72
73
74"""
75
76import os
77import struct
78import marshal
79import zlib
80import sys
81import imp
82import types
83from uuid import uuid4 as uniquename
84
85
86class CTOCEntry:
87 def __init__(self, position, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name):
88 self.position = position
89 self.cmprsdDataSize = cmprsdDataSize
90 self.uncmprsdDataSize = uncmprsdDataSize
91 self.cmprsFlag = cmprsFlag
92 self.typeCmprsData = typeCmprsData
93 self.name = name
94
95
96class PyInstArchive:
97 PYINST20_COOKIE_SIZE = 24 # For pyinstaller 2.0
98 PYINST21_COOKIE_SIZE = 24 + 64 # For pyinstaller 2.1+
99 MAGIC = b'MEI\014\013\012\013\016' # Magic number which identifies pyinstaller
100
101 def __init__(self, path):
102 self.filePath = path
103
104
105 def open(self):
106 try:
107 self.fPtr = open(self.filePath, 'rb')
108 self.fileSize = os.stat(self.filePath).st_size
109 except:
110 print('[*] Error: Could not open {0}'.format(self.filePath))
111 return False
112 return True
113
114
115 def close(self):
116 try:
117 self.fPtr.close()
118 except:
119 pass
120
121
122 def checkFile(self):
123 print('[*] Processing {0}'.format(self.filePath))
124 # Check if it is a 2.0 archive
125 self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET)
126 magicFromFile = self.fPtr.read(len(self.MAGIC))
127
128 if magicFromFile == self.MAGIC:
129 self.pyinstVer = 20 # pyinstaller 2.0
130 print('[*] Pyinstaller version: 2.0')
131 return True
132
133 # Check for pyinstaller 2.1+ before bailing out
134 self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET)
135 magicFromFile = self.fPtr.read(len(self.MAGIC))
136
137 if magicFromFile == self.MAGIC:
138 print('[*] Pyinstaller version: 2.1+')
139 self.pyinstVer = 21 # pyinstaller 2.1+
140 return True
141
142 print('[*] Error : Unsupported pyinstaller version or not a pyinstaller archive')
143 return True
144
145
146 def getCArchiveInfo(self):
147 try:
148 if self.pyinstVer == 20:
149 self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET)
150
151 # Read CArchive cookie
152 (magic, lengthofPackage, toc, tocLen, self.pyver) = \
153 struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE))
154
155 elif self.pyinstVer == 21:
156 self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET)
157
158 # Read CArchive cookie
159 (magic, lengthofPackage, toc, tocLen, self.pyver, pylibname) = \
160 struct.unpack('!8siiii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE))
161
162 except:
163 print('[*] Error : The file is not a pyinstaller archive')
164 return False
165
166 print('[*] Python version: {0}'.format(self.pyver))
167
168 # Overlay is the data appended at the end of the PE
169 self.overlaySize = lengthofPackage
170 self.overlayPos = self.fileSize - self.overlaySize
171 self.tableOfContentsPos = self.overlayPos + toc
172 self.tableOfContentsSize = tocLen
173
174 print('[*] Length of package: {0} bytes'.format(self.overlaySize))
175 return True
176
177
178 def parseTOC(self):
179 # Go to the table of contents
180 self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET)
181
182 self.tocList = []
183 parsedLen = 0
184
185 # Parse table of contents
186 while parsedLen < self.tableOfContentsSize:
187 (entrySize, ) = struct.unpack('!i', self.fPtr.read(4))
188 nameLen = struct.calcsize('!iiiiBc')
189
190 (entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = \
191 struct.unpack( \
192 '!iiiBc{0}s'.format(entrySize - nameLen), \
193 self.fPtr.read(entrySize - 4))
194
195 name = name.decode('utf-8').rstrip('\0')
196 if len(name) == 0:
197 name = str(uniquename())
198 print('[!] Warning: Found an unamed file in CArchive. Using random name {0}'.format(name))
199
200 self.tocList.append( \
201 CTOCEntry( \
202 self.overlayPos + entryPos, \
203 cmprsdDataSize, \
204 uncmprsdDataSize, \
205 cmprsFlag, \
206 typeCmprsData, \
207 name \
208 ))
209
210 parsedLen += entrySize
211 print('[*] Found {0} files in CArchive'.format(len(self.tocList)))
212
213
214
215 def extractFiles(self):
216 print('[*] Beginning extraction...please standby')
217 extractionDir = os.path.join(os.getcwd(), os.path.basename(self.filePath) + '_extracted')
218
219 if not os.path.exists(extractionDir):
220 os.mkdir(extractionDir)
221
222 os.chdir(extractionDir)
223
224 for entry in self.tocList:
225 basePath = os.path.dirname(entry.name)
226 if basePath != '':
227 # Check if path exists, create if not
228 if not os.path.exists(basePath):
229 os.makedirs(basePath)
230
231 self.fPtr.seek(entry.position, os.SEEK_SET)
232 data = self.fPtr.read(entry.cmprsdDataSize)
233
234 if entry.cmprsFlag == 1:
235 data = zlib.decompress(data)
236 # Malware may tamper with the uncompressed size
237 # Comment out the assertion in such a case
238 assert len(data) == entry.uncmprsdDataSize # Sanity Check
239
240 with open(entry.name, 'wb') as f:
241 f.write(data)
242
243 if entry.typeCmprsData == b'z':
244 self._extractPyz(entry.name)
245
246 @staticmethod
247 def _extractPyz(name):
248 dirName = name + '_extracted'
249 # Create a directory for the contents of the pyz
250 if not os.path.exists(dirName):
251 os.mkdir(dirName)
252
253 with open(name, 'rb') as f:
254 pyzMagic = f.read(4)
255 assert pyzMagic == b'PYZ\0' # Sanity Check
256
257 pycHeader = f.read(4) # Python magic value
258
259 if imp.get_magic() != pycHeader:
260 print('[!] Warning: The script is running in a different python version than the one used to build the executable')
261 print(' Run this script in Python{0} to prevent extraction errors(if any) during unmarshalling'.format(self.pyver))
262
263 (tocPosition, ) = struct.unpack('!i', f.read(4))
264 f.seek(tocPosition, os.SEEK_SET)
265
266 try:
267 toc = marshal.load(f)
268 except:
269 print('[!] Unmarshalling FAILED. Cannot extract {0}. Extracting remaining files.'.format(name))
270 return
271
272 print('[*] Found {0} files in PYZ archive'.format(len(toc)))
273
274 # From pyinstaller 3.1+ toc is a list of tuples
275 if type(toc) == list:
276 toc = dict(toc)
277
278 for key in toc.keys():
279 (ispkg, pos, length) = toc[key]
280 f.seek(pos, os.SEEK_SET)
281
282 fileName = key
283 try:
284 # for Python > 3.3 some keys are bytes object some are str object
285 fileName = key.decode('utf-8')
286 except:
287 pass
288
289 # Make sure destination directory exists, ensuring we keep inside dirName
290 destName = os.path.join(dirName, fileName.replace("..", "__"))
291 destDirName = os.path.dirname(destName)
292 if not os.path.exists(destDirName):
293 os.makedirs(destDirName)
294
295 try:
296 data = f.read(length)
297 data = zlib.decompress(data)
298 except:
299 print('[!] Error: Failed to decompress {0}, probably encrypted. Extracting as is.'.format(fileName))
300 open(destName + '.pyc.encrypted', 'wb').write(data)
301 continue
302
303 with open(destName + '.pyc', 'wb') as pycFile:
304 pycFile.write(pycHeader) # Write pyc magic
305 pycFile.write(b'\0' * 4) # Write timestamp
306 if 33 >= 33:
307 pycFile.write(b'\0' * 4) # Size parameter added in Python 3.3
308 pycFile.write(data)
309
310
311def main():
312 if len(sys.argv) < 2:
313 print('[*] Usage: pyinstxtractor.py <filename>')
314
315
316 PyInstArchive._extractPyz(sys.argv[1])
317
318 """
319 else:
320 arch = PyInstArchive(sys.argv[1])
321 if arch.open():
322 if arch.checkFile():
323 if arch.getCArchiveInfo():
324 arch.parseTOC()
325 arch.extractFiles()
326 arch.close()
327 print('[*] Successfully extracted pyinstaller archive: {0}'.format(sys.argv[1]))
328 print('')
329 print('You can now use a python decompiler on the pyc files within the extracted directory')
330 return
331
332 arch.close()"""
333
334
335if __name__ == '__main__':
336 main()