Advertisement
Guest User

PyInstaller Extractor v1.8

a guest
Jun 22nd, 2017
1,604
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.94 KB | None | 0 0
  1. """
  2. PyInstaller Extractor v1.8 (Supports pyinstaller 3.2, 3.1, 3.0, 2.1, 2.0)
  3. Author : Extreme Coders
  4. E-mail : extremecoders(at)hotmail(dot)com
  5. Web    : https://0xec.blogspot.com
  6. Date   : 28-April-2017
  7. Url    : https://sourceforge.net/projects/pyinstallerextractor/
  8.  
  9. For any suggestions, leave a comment on
  10. https://forum.tuts4you.com/topic/34455-pyinstaller-extractor/
  11.  
  12. This script extracts a pyinstaller generated executable file.
  13. Pyinstaller installation is not needed. The script has it all.
  14.  
  15. For best results, it is recommended to run this script in the
  16. same version of python as was used to create the executable.
  17. This is just to prevent unmarshalling errors(if any) while
  18. extracting the PYZ archive.
  19.  
  20. Usage : 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.  
  23. C:\path\to\exe\>python pyinstxtractor.py <filename>
  24. $ /path/to/exe/python pyinstxtractor.py <filename>
  25.  
  26. Licensed under GNU General Public License (GPL) v3.
  27. You are free to modify this source.
  28.  
  29. CHANGELOG
  30. ================================================
  31.  
  32. Version 1.1 (Jan 28, 2014)
  33. -------------------------------------------------
  34. - First Release
  35. - Supports only pyinstaller 2.0
  36.  
  37. Version 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.  
  46. Version 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.  
  51. Version 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.  
  55. Version 1.5 (March 1, 2016)
  56. -------------------------------------------------
  57. - Added support for pyinstaller 3.1 (Thanks to Berwyn Hoyt for reporting)
  58.  
  59. Version 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.  
  65. Version 1.7 (March 13, 2017)
  66. -------------------------------------------------
  67. - Made the script compatible with python 2.6 (Thanks to Ross for reporting)
  68.  
  69. Version 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.  
  76. import os
  77. import struct
  78. import marshal
  79. import zlib
  80. import sys
  81. import imp
  82. import types
  83. from uuid import uuid4 as uniquename
  84.  
  85.  
  86. class 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.  
  96. class 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 False
  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.  
  247.     def _extractPyz(self, 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 self.pyver >= 33:
  307.                         pycFile.write(b'\0' * 4)  # Size parameter added in Python 3.3
  308.                     pycFile.write(data)
  309.  
  310.  
  311. def main():
  312.     if len(sys.argv) < 2:
  313.         print('[*] Usage: pyinstxtractor.py <filename>')
  314.  
  315.     else:
  316.         arch = PyInstArchive(sys.argv[1])
  317.         if arch.open():
  318.             if arch.checkFile():
  319.                 if arch.getCArchiveInfo():
  320.                     arch.parseTOC()
  321.                     arch.extractFiles()
  322.                     arch.close()
  323.                     print('[*] Successfully extracted pyinstaller archive: {0}'.format(sys.argv[1]))
  324.                     print('')
  325.                     print('You can now use a python decompiler on the pyc files within the extracted directory')
  326.                     return
  327.  
  328.             arch.close()
  329.  
  330.  
  331. if __name__ == '__main__':
  332.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement