Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 35 additions & 36 deletions pyinstxtractor_ng.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import zlib
import struct
import argparse
from clc99 import *

from uuid import uuid4 as uniquename

Expand All @@ -27,8 +28,6 @@

from xdis.unmarshal import load_code

def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)

def pycHeader2Magic(header):
header = bytearray(header)
Expand Down Expand Up @@ -65,7 +64,7 @@ def open(self):
self.fPtr = open(self.filePath, "rb")
self.fileSize = os.stat(self.filePath).st_size
except:
eprint("[!] Error: Could not open {0}".format(self.filePath))
print_error("Could not open {0}".format(self.filePath),file=sys.stderr)
return False
return True

Expand All @@ -76,14 +75,14 @@ def close(self):
pass

def checkFile(self):
print("[+] Processing {0}".format(self.filePath))
print_status("Processing {0}".format(self.filePath))

searchChunkSize = 8192
endPos = self.fileSize
self.cookiePos = -1

if endPos < len(self.MAGIC):
eprint("[!] Error: File is too short or truncated")
print_error("File is too short or truncated",file=sys.stderr)
return False

while True:
Expand All @@ -108,19 +107,19 @@ def checkFile(self):
break

if self.cookiePos == -1:
eprint(
"[!] Error: Missing cookie, unsupported pyinstaller version or not a pyinstaller archive"
)
print_error(
"Missing cookie, unsupported pyinstaller version or not a pyinstaller archive",
file=sys.stderr)
return False

self.fPtr.seek(self.cookiePos + self.PYINST20_COOKIE_SIZE, os.SEEK_SET)

if b"python" in self.fPtr.read(64).lower():
print("[+] Pyinstaller version: 2.1+")
print_good("Pyinstaller version: 2.1+")
self.pyinstVer = 21 # pyinstaller 2.1+
else:
self.pyinstVer = 20 # pyinstaller 2.0
print("[+] Pyinstaller version: 2.0")
print_good("Pyinstaller version: 2.0")

return True

Expand All @@ -143,13 +142,13 @@ def getCArchiveInfo(self):
)

except:
eprint("[!] Error: The file is not a pyinstaller archive")
print_error("The file is not a pyinstaller archive",file=sys.stderr)
return False

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))
print_good("Python version: {0}.{1}".format(self.pymaj, self.pymin))

# Additional data after the cookie
tailBytes = (
Expand All @@ -168,7 +167,7 @@ def getCArchiveInfo(self):
self.tableOfContentsPos = self.overlayPos + toc
self.tableOfContentsSize = tocLen

print("[+] Length of package: {0} bytes".format(lengthofPackage))
print_status("Length of package: {0} bytes".format(lengthofPackage))
return True

def parseTOC(self):
Expand Down Expand Up @@ -198,7 +197,7 @@ def parseTOC(self):
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))
print_warning('Warning: File name {0} contains invalid bytes. Using random name {1}'.format(name, newName),file=sys.stderr)
name = newName

# Prevent writing outside the extraction directory
Expand All @@ -207,10 +206,10 @@ def parseTOC(self):

if len(name) == 0:
name = str(uniquename())
print(
"[!] Warning: Found an unamed file in CArchive. Using random name {0}".format(
print_warning(
"Warning: Found an unamed file in CArchive. Using random name {0}".format(
name
)
),file=sys.stderr
)

self.tocList.append(
Expand All @@ -225,7 +224,7 @@ def parseTOC(self):
)

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

def _writeRawData(self, filepath, data):
nm = (
Expand All @@ -243,7 +242,7 @@ def _writeRawData(self, filepath, data):
f.write(data)

def extractFiles(self, one_dir):
print("[+] Beginning extraction...please standby")
print_good("Beginning extraction...please standby")
extractionDir = os.path.join(
os.getcwd(), os.path.basename(self.filePath) + "_extracted"
)
Expand Down Expand Up @@ -278,7 +277,7 @@ def extractFiles(self, one_dir):
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))
print_status("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
Expand All @@ -299,8 +298,8 @@ def extractFiles(self, one_dir):
self._writeRawData(entry.name + ".pyc", data)

if entry.name.endswith("_crypto_key"):
print(
"[+] Detected _crypto_key file, saving key for automatic decryption"
print_good(
"Detected _crypto_key file, saving key for automatic decryption"
)
# This is a pyc file with a header (8,12, or 16 bytes)
# Extract the code object after the header
Expand All @@ -315,8 +314,8 @@ def extractFiles(self, one_dir):
self._writePyc(entry.name + ".pyc", data)

if entry.name.endswith("_crypto_key"):
print(
"[+] Detected _crypto_key file, saving key for automatic decryption"
print_good(
"Detected _crypto_key file, saving key for automatic decryption"
)
# This is a plain code object without a header
self.cryptoKeyFileData = data
Expand Down Expand Up @@ -413,8 +412,8 @@ def _extractPyz(self, name, one_dir):

elif self.pycMagic != pyzPycMagic:
self.pycMagic = pyzPycMagic
print(
"[!] Warning: pyc magic of files inside PYZ archive are different from those in CArchive"
print_warning(
"Warning: pyc magic of files inside PYZ archive are different from those in CArchive",file=sys.stderr
)

(tocPosition,) = struct.unpack("!i", f.read(4))
Expand All @@ -423,14 +422,14 @@ def _extractPyz(self, name, one_dir):
try:
toc = load_code(f, pycHeader2Magic(pyzPycMagic))
except:
print(
"[!] Unmarshalling FAILED. Cannot extract {0}. Extracting remaining files.".format(
print_warning(
"Unmarshalling FAILED. Cannot extract {0}. Extracting remaining files.".format(
name
)
),file=sys.stderr
)
return

print("[+] Found {0} files in PYZ archive".format(len(toc)))
print_good("Found {0} files in PYZ archive".format(len(toc)))

# From pyinstaller 3.1+ toc is a list of tuples
if type(toc) == list:
Expand Down Expand Up @@ -482,10 +481,10 @@ def _extractPyz(self, name, one_dir):
data = self._tryDecrypt(data, "cfb")
data = zlib.decompress(data)
except:
eprint(
"[!] Error: Failed to decrypt & decompress {0}. Extracting as is.".format(
print_error(
"Failed to decrypt & decompress {0}. Extracting as is.".format(
filePath
)
),file=sys.stderr
)
open(filePath + ".encrypted", "wb").write(data_copy)
continue
Expand All @@ -511,13 +510,13 @@ def main():
arch.parseTOC()
arch.extractFiles(args.one_dir)
arch.close()
print(
"[+] Successfully extracted pyinstaller archive: {0}".format(
print_good(
"Successfully extracted pyinstaller archive: {0}".format(
args.filename
)
)
print("")
print(
print_finish(
"You can now use a python decompiler on the pyc files within the extracted directory"
)
sys.exit(0)
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pyinstaller==5.10.1
xdis==6.1.3
PyCryptoDome==3.17
clc99
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
xdis==6.1.3
PyCryptoDome==3.17
clc99