37 Commits

Author SHA1 Message Date
extremecoders-re
bb936f9b2c Mention support for 6.12.0 2025-02-22 15:28:28 +05:30
extremecoders-re
0e3ba85701 Mention support for 6.10.0, 6.11.0, 6.11.1 2024-11-12 22:40:46 +05:30
extremecoders-re
90974deaa6 Mention support for 6.7.0, 6.8.0, 6.9.0 2024-07-21 15:14:42 +05:30
extremecoders-re
7deea464e0 Mention support for pyinstaller 6.4.0, 6.5.0, 6.6.0 2024-04-14 11:48:14 +05:30
extremecoders-re
6c5040ec55 Mention support for pyinstaller 6.3.0 2024-01-20 18:06:02 +05:30
extremecoders-re
2457a39b67 Mention support for 6.1.0, 6.2.0 2023-12-03 23:49:07 +05:30
extremecoders-re
6be66895ba Fix Invalid escape sequence \p syntax warning 2023-12-03 23:47:41 +05:30
extremecoders-re
f1cff6b4cc Mention support for 5.13.1, 5.13.2, 6.0.0 2023-10-11 03:12:14 +05:30
extremecoders-re
9565295780 Backport non UTF-8 file name bugfix from pyinstxtractor-ng 2023-08-21 02:39:38 +05:30
extremecoders-re
4c89bd51db Mention support for 5.13.0 2023-08-19 14:00:49 +05:30
extremecoders-re
044812c1f0 Merge pull request #70 from maximevince/master
Do not abort after the first extraction error
2023-06-30 13:14:10 +05:30
Maxime Vincent
c19cdbb30f Do not abort after the first extraction error; continue with the next file instead 2023-06-30 08:51:02 +02:00
extremecoders-re
128cb962c0 Mention support for 5.11.0 and 5.12.0 2023-06-16 20:50:14 +05:30
extremecoders-re
5268087d25 Handle CArchive entries starting with an absolute file path, closes #67 2023-06-09 03:01:02 +05:30
extremecoders-re
cad8c74542 Mention support for 5.10.0, 5.10.1 2023-05-07 16:45:37 +05:30
extremecoders-re
389dbe53e5 Mention support for 5.9.0 2023-04-09 12:33:51 +05:30
extremecoders-re
59dcc98394 Mention support for 5.8.0 2023-02-26 10:36:27 +05:30
extremecoders-re
11de72754b Mention support for 5.7.0 2022-12-25 08:20:16 +00:00
extremecoders-re
87690463cb Support files larger than 2GiB, closes #61
PyInstaller 4.3 added support for CArchive sizes >2GiB & <4GiB

Details:
- https://pyinstaller.org/en/stable/CHANGES.html#id81
- https://github.com/pyinstaller/pyinstaller/commit/937d8a
2022-12-03 16:59:23 +05:30
extremecoders-re
158b3748f6 Mention support for 5.6.2 2022-12-03 16:53:48 +05:30
extremecoders-re
d368df5279 Update url's in Readme 2022-10-29 02:24:08 +05:30
extremecoders-re
236af2be70 Mention support for 5.5, 5.6, 5.6.1 + typo fixes 2022-10-29 02:20:43 +05:30
extremecoders-re
eaac3869c0 Mention support for encrypted exe's in -ng version 2022-09-23 15:18:26 +05:30
extremecoders-re
0651378023 Add link to pyinstxtractor-web 2022-09-22 03:52:33 +05:30
extremecoders-re
3f9ff3a7b9 Add note to pyinstxtractor-ng 2022-09-18 17:36:41 +05:30
extremecoders-re
dd3a4c4e89 Update README 2022-09-17 16:04:37 +05:30
extremecoders-re
129460a242 Correct major minor version check 2022-09-17 15:56:40 +05:30
extremecoders-re
45cfb9b592 Mention support for 5.4.1, 5.4 2022-09-17 15:38:01 +05:30
extremecoders-re
6c72177e58 Use correct pyc header magic value, Closes #53, #54 2022-09-17 15:21:29 +05:30
extremecoders-re
f0f5741ae4 Mention support for 5.2, 5.3 2022-08-21 14:04:44 +05:30
extremecoders-re
957f830443 Correctly calculate the python version, fixes #37
The python version is stored as major*100 + minor

Details: https://github.com/pyinstaller/pyinstaller/commit/9f7c92
2022-06-13 16:49:39 +05:30
extremecoders-re
fb7c13d7db Mention support for pyinstaller 5.1 2022-06-05 12:08:31 +03:00
extremecoders-re
eba6b6681c Do a case insensitive string search for "python" in cookie buffer, Fixes #47 2022-06-05 11:45:51 +03:00
extremecoders-re
4dc147a379 Correct handling order of runtime options, followup #44 2022-05-13 11:01:54 +03:00
extremecoders-re
d68ef84279 Ignore pyinstaller runtime options, Fixes #44 2022-05-11 12:36:50 +03:00
extremecoders-re
816d50f1e6 Mention support for pyinstaller 4.10, 5.0, 5.0.1 2022-04-29 00:05:18 +03:00
extremecoders-re
624bde27e1 Mention support for pyinstaller 4.9, 4.8 2022-02-25 16:52:11 +05:30
2 changed files with 99 additions and 42 deletions

View File

@@ -1,12 +1,12 @@
# PyInstaller Extractor
PyInstaller Extractor is a Python script to extract the contents of a PyInstaller generated Windows executable file. The contents of the pyz file (usually pyc files) present inside the executable are also extracted.
PyInstaller Extractor is a Python script to extract the contents of a PyInstaller generated executable file.
The header of the pyc files are automatically fixed so that a Python bytecode decompiler will recognize it. The script can run on both Python 2.x and 3.x. Pyinstaller versions 2.0, 2.1, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 4.0, 4.1, 4.2, 4.3, 4.4, 4.5, 4.5.1, 4.6, 4.7 are [tested](https://github.com/extremecoders-re/pyinstxtractor-test-binaries) & supported. Probably will work with other versions too.
The header of the pyc files are automatically fixed so that a Python bytecode decompiler will recognize it. The script can run on both Python 2.x and 3.x. PyInstaller versions 2.0, 2.1, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 4.0, 4.1, 4.2, 4.3, 4.4, 4.5, 4.5.1, 4.6, 4.7, 4.8, 4.9, 4.10, 5.0, 5.0.1, 5.1, 5.2, 5.3, 5.4, 5.4.1, 5.5, 5.6, 5.6.1, 5.6.2, 5.7.0, 5.8.0, 5.9.0, 5.10.0, 5.10.1, 5.11.0, 5.12.0, 5.13.0, 5.13.1, 5.13.2, 6.0.0, 6.1.0, 6.2.0, 6.3.0, 6.4.0, 6.5.0, 6.6.0, 6.7.0, 6.8.0, 6.9.0, 6.10.0, 6.11.0, 6.11.1, 6.12.0 are [tested](https://github.com/pyinstxtractor/pyinstxtractor-test-binaries) & supported. Probably will work with other versions too.
This project was originally hosted on [SourceForge](https://sourceforge.net/projects/pyinstallerextractor/).
## How to use
## Usage
The script can be run by passing the name of the exe as an argument.
@@ -35,7 +35,7 @@ X:\> python pyinstxtractor.py test.exe
You can now use a python decompiler on the pyc files within the extracted directory
```
After extracting the pyc's you can use a Python decompiler like [Uncompyle6](https://github.com/rocky/python-uncompyle6/).
After extracting the pyc's you can use a Python decompiler like [Uncompyle6](https://github.com/rocky/python-uncompyle6/) and [Decompyle++](https://github.com/zrax/pycdc).
```
X:\> uncompyle6.exe test.exe_extracted\test.pyc
@@ -43,10 +43,16 @@ X:\> uncompyle6.exe test.exe_extracted\PYZ-00.pyz_extracted\__future__.pyc
```
## Extracting Linux ELF binaries
Pyinstxtractor can now natively extract Linux ELF binaries without other additional tools.
Pyinstxtractor can natively extract Linux ELF binaries without requiring other tools.
For other questions and information, please see the [Wiki](https://github.com/extremecoders-re/pyinstxtractor/wiki/Extracting-Linux-ELF-binaries) and the [FAQ](https://github.com/extremecoders-re/pyinstxtractor/wiki/Frequently-Asked-Questions)
## See also
- [pyinstxtractor-ng](https://github.com/pyinstxtractor/pyinstxtractor-ng):
A standalone binary version of pyinstxtractor. This tool doesn't require Python to run and can extract all supported versions of PyInstaller. It also supports encrypted pyinstaller executables.
- [pyinstxtractor-web](https://github.com/pyinstxtractor/pyinstxtractor-go): pyinstxtractor running in the web browser, powered by Go & GopherJS.
## License
GNU General Public License v3.0
GNU General Public License v3.0

View File

@@ -1,5 +1,5 @@
"""
PyInstaller Extractor v2.0 (Supports pyinstaller 4.7, 4.6, 4.5.1, 4.5, 4.4, 4.3, 4.2, 4.1, 4.0, 3.6, 3.5, 3.4, 3.3, 3.2, 3.1, 3.0, 2.1, 2.0)
PyInstaller Extractor v2.0 (Supports pyinstaller 6.12.0, 6.11.1, 6.11.0, 6.10.0, 6.9.0, 6.8.0, 6.7.0, 6.6.0, 6.5.0, 6.4.0, 6.3.0, 6.2.0, 6.1.0, 6.0.0, 5.13.2, 5.13.1, 5.13.0, 5.12.0, 5.11.0, 5.10.1, 5.10.0, 5.9.0, 5.8.0, 5.7.0, 5.6.2, 5.6.1, 5.6, 5.5, 5.4.1, 5.4, 5.3, 5.2, 5.1, 5.0.1, 5.0, 4.10, 4.9, 4.8, 4.7, 4.6, 4.5.1, 4.5, 4.4, 4.3, 4.2, 4.1, 4.0, 3.6, 3.5, 3.4, 3.3, 3.2, 3.1, 3.0, 2.1, 2.0)
Author : Extreme Coders
E-mail : extremecoders(at)hotmail(dot)com
Web : https://0xec.blogspot.com
@@ -20,7 +20,7 @@ extracting the PYZ archive.
Usage : Just copy this script to the directory where your exe resides
and run the script with the exe file name as a parameter
C:\path\to\exe\>python pyinstxtractor.py <filename>
C:\\path\\to\\exe\\>python pyinstxtractor.py <filename>
$ /path/to/exe/python pyinstxtractor.py <filename>
Licensed under GNU General Public License (GPL) v3.
@@ -91,14 +91,6 @@ import zlib
import sys
from uuid import uuid4 as uniquename
# imp is deprecated in Python3 in favour of importlib
if sys.version_info.major == 3:
from importlib.util import MAGIC_NUMBER
pyc_magic = MAGIC_NUMBER
else:
import imp
pyc_magic = imp.get_magic()
class CTOCEntry:
def __init__(self, position, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name):
@@ -117,6 +109,8 @@ class PyInstArchive:
def __init__(self, path):
self.filePath = path
self.pycMagic = b'\0' * 4
self.barePycList = [] # List of pyc's whose headers have to be fixed
def open(self):
@@ -174,7 +168,7 @@ class PyInstArchive:
self.fPtr.seek(self.cookiePos + self.PYINST20_COOKIE_SIZE, os.SEEK_SET)
if b'python' in self.fPtr.read(64):
if b'python' in self.fPtr.read(64).lower():
print('[+] Pyinstaller version: 2.1+')
self.pyinstVer = 21 # pyinstaller 2.1+
else:
@@ -190,25 +184,26 @@ class PyInstArchive:
self.fPtr.seek(self.cookiePos, os.SEEK_SET)
# Read CArchive cookie
(magic, lengthofPackage, toc, tocLen, self.pyver) = \
(magic, lengthofPackage, toc, tocLen, pyver) = \
struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE))
elif self.pyinstVer == 21:
self.fPtr.seek(self.cookiePos, os.SEEK_SET)
# Read CArchive cookie
(magic, lengthofPackage, toc, tocLen, self.pyver, pylibname) = \
struct.unpack('!8siiii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE))
(magic, lengthofPackage, toc, tocLen, pyver, pylibname) = \
struct.unpack('!8sIIii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE))
except:
print('[!] Error : The file is not a pyinstaller archive')
return False
print('[+] Python version: {0}'.format(self.pyver))
self.pymaj, self.pymin = (pyver//100, pyver%100) if pyver >= 100 else (pyver//10, pyver%10)
print('[+] Python version: {0}.{1}'.format(self.pymaj, self.pymin))
# Additional data after the cookie
tailBytes = self.fileSize - self.cookiePos - (self.PYINST20_COOKIE_SIZE if self.pyinstVer == 20 else self.PYINST21_COOKIE_SIZE)
# Overlay is the data appended at the end of the PE
self.overlaySize = lengthofPackage + tailBytes
self.overlayPos = self.fileSize - self.overlaySize
@@ -229,14 +224,24 @@ class PyInstArchive:
# Parse table of contents
while parsedLen < self.tableOfContentsSize:
(entrySize, ) = struct.unpack('!i', self.fPtr.read(4))
nameLen = struct.calcsize('!iiiiBc')
nameLen = struct.calcsize('!iIIIBc')
(entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = \
struct.unpack( \
'!iiiBc{0}s'.format(entrySize - nameLen), \
'!IIIBc{0}s'.format(entrySize - nameLen), \
self.fPtr.read(entrySize - 4))
name = name.decode('utf-8').rstrip('\0')
try:
name = name.decode("utf-8").rstrip("\0")
except UnicodeDecodeError:
newName = str(uniquename())
print('[!] Warning: File name {0} contains invalid bytes. Using random name {1}'.format(name, newName))
name = newName
# Prevent writing outside the extraction directory
if name.startswith("/"):
name = name.lstrip("/")
if len(name) == 0:
name = str(uniquename())
print('[!] Warning: Found an unamed file in CArchive. Using random name {0}'.format(name))
@@ -275,32 +280,61 @@ class PyInstArchive:
os.chdir(extractionDir)
for entry in self.tocList:
self.fPtr.seek(entry.position, os.SEEK_SET)
data = self.fPtr.read(entry.cmprsdDataSize)
if entry.cmprsFlag == 1:
try:
data = zlib.decompress(data)
except zlib.error:
print('[!] Error : Failed to decompress {0}'.format(entry.name))
continue
# Malware may tamper with the uncompressed size
# Comment out the assertion in such a case
assert len(data) == entry.uncmprsdDataSize # Sanity Check
if entry.typeCmprsData == b'd' or entry.typeCmprsData == b'o':
# d -> ARCHIVE_ITEM_DEPENDENCY
# o -> ARCHIVE_ITEM_RUNTIME_OPTION
# These are runtime options, not files
continue
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
if entry.typeCmprsData == b's':
# s -> ARCHIVE_ITEM_PYSOURCE
# Entry point are expected to be python scripts
print('[+] Possible entry point: {0}.pyc'.format(entry.name))
if self.pycMagic == b'\0' * 4:
# if we don't have the pyc header yet, fix them in a later pass
self.barePycList.append(entry.name + '.pyc')
self._writePyc(entry.name + '.pyc', data)
elif entry.typeCmprsData == b'M' or entry.typeCmprsData == b'm':
# M -> ARCHIVE_ITEM_PYPACKAGE
# m -> ARCHIVE_ITEM_PYMODULE
# packages and modules are pyc files with their header's intact
self._writeRawData(entry.name + '.pyc', data)
# packages and modules are pyc files with their header intact
# From PyInstaller 5.3 and above pyc headers are no longer stored
# https://github.com/pyinstaller/pyinstaller/commit/a97fdf
if data[2:4] == b'\r\n':
# < pyinstaller 5.3
if self.pycMagic == b'\0' * 4:
self.pycMagic = data[0:4]
self._writeRawData(entry.name + '.pyc', data)
else:
# >= pyinstaller 5.3
if self.pycMagic == b'\0' * 4:
# if we don't have the pyc header yet, fix them in a later pass
self.barePycList.append(entry.name + '.pyc')
self._writePyc(entry.name + '.pyc', data)
else:
self._writeRawData(entry.name, data)
@@ -308,18 +342,28 @@ class PyInstArchive:
if entry.typeCmprsData == b'z' or entry.typeCmprsData == b'Z':
self._extractPyz(entry.name)
# Fix bare pyc's if any
self._fixBarePycs()
def _fixBarePycs(self):
for pycFile in self.barePycList:
with open(pycFile, 'r+b') as pycFile:
# Overwrite the first four bytes
pycFile.write(self.pycMagic)
def _writePyc(self, filename, data):
with open(filename, 'wb') as pycFile:
pycFile.write(pyc_magic) # pyc magic
pycFile.write(self.pycMagic) # pyc magic
if self.pyver >= 37: # PEP 552 -- Deterministic pycs
if self.pymaj >= 3 and self.pymin >= 7: # PEP 552 -- Deterministic pycs
pycFile.write(b'\0' * 4) # Bitfield
pycFile.write(b'\0' * 8) # (Timestamp + size) || hash
else:
pycFile.write(b'\0' * 4) # Timestamp
if self.pyver >= 33:
if self.pymaj >= 3 and self.pymin >= 3:
pycFile.write(b'\0' * 4) # Size parameter added in Python 3.3
pycFile.write(data)
@@ -335,12 +379,19 @@ class PyInstArchive:
pyzMagic = f.read(4)
assert pyzMagic == b'PYZ\0' # Sanity Check
pycHeader = f.read(4) # Python magic value
pyzPycMagic = f.read(4) # Python magic value
if self.pycMagic == b'\0' * 4:
self.pycMagic = pyzPycMagic
elif self.pycMagic != pyzPycMagic:
self.pycMagic = pyzPycMagic
print('[!] Warning: pyc magic of files inside PYZ archive are different from those in CArchive')
# Skip PYZ extraction if not running under the same python version
if pyc_magic != pycHeader:
if self.pymaj != sys.version_info.major or self.pymin != sys.version_info.minor:
print('[!] Warning: This script is running in a different Python version than the one used to build the executable.')
print('[!] Please run this script in Python{0} to prevent extraction errors during unmarshalling'.format(self.pyver))
print('[!] Please run this script in Python {0}.{1} to prevent extraction errors during unmarshalling'.format(self.pymaj, self.pymin))
print('[!] Skipping pyz extraction')
return