Python反编译教程(exe转py) 适用于目前最新的3.13
文章目录
一、将exe文件转换成pyc文件
- 新建一个
unpack.py文件,将以下代码复制粘贴进去
from __future__ import print_function import os import struct import marshal import zlib import sys from uuid import uuid4 as uniquename classCTOCEntry: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 classPyInstArchive: 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 pyinstallerdef__init__(self, path): self.filePath = path self.pycMagic =b'\0'*4 self.barePycList =[]# List of pyc's whose headers have to be fixeddefopen(self):try: self.fPtr =open(self.filePath,'rb') self.fileSize = os.stat(self.filePath).st_size except:print('[!] Error: Could not open {0}'.format(self.filePath))returnFalsereturnTruedefclose(self):try: self.fPtr.close()except:passdefcheckFile(self):print('[+] Processing {0}'.format(self.filePath)) searchChunkSize =8192 endPos = self.fileSize self.cookiePos =-1if endPos <len(self.MAGIC):print('[!] Error : File is too short or truncated')returnFalsewhileTrue: startPos = endPos - searchChunkSize if endPos >= searchChunkSize else0 chunkSize = endPos - startPos if chunkSize <len(self.MAGIC):break self.fPtr.seek(startPos, os.SEEK_SET) data = self.fPtr.read(chunkSize) offs = data.rfind(self.MAGIC)if offs !=-1: self.cookiePos = startPos + offs break endPos = startPos +len(self.MAGIC)-1if startPos ==0:breakif self.cookiePos ==-1:print('[!] Error : Missing cookie, unsupported pyinstaller version or not a pyinstaller archive')returnFalse self.fPtr.seek(self.cookiePos + self.PYINST20_COOKIE_SIZE, os.SEEK_SET)ifb'python'in self.fPtr.read(64).lower():print('[+] Pyinstaller version: 2.1+') self.pyinstVer =21# pyinstaller 2.1+else: self.pyinstVer =20# pyinstaller 2.0print('[+] Pyinstaller version: 2.0')returnTruedefgetCArchiveInfo(self):try:if self.pyinstVer ==20: self.fPtr.seek(self.cookiePos, os.SEEK_SET)# Read CArchive cookie(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, pyver, pylibname)= \ struct.unpack('!8sIIii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE))except:print('[!] Error : The file is not a pyinstaller archive')returnFalse self.pymaj, self.pymin =(pyver//100, pyver%100)if pyver >=100else(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 ==20else 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 self.tableOfContentsPos = self.overlayPos + toc self.tableOfContentsSize = tocLen print('[+] Length of package: {0} bytes'.format(lengthofPackage))returnTruedefparseTOC(self):# Go to the table of contents self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET) self.tocList =[] parsedLen =0# Parse table of contentswhile 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))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 directoryif name.startswith("/"): name = name.lstrip("/")iflen(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_writeRawData(self, filepath, data): nm = filepath.replace('\\', os.path.sep).replace('/', os.path.sep).replace('..','__') nmDir = os.path.dirname(nm)if nmDir !=''andnot os.path.exists(nmDir):# Check if path exists, create if not os.makedirs(nmDir)withopen(nm,'wb')as f: f.write(data)defextractFiles(self):print('[+] Beginning extraction...please standby') extractionDir = os.path.join(os.getcwd(), os.path.basename(self.filePath)+'_extracted')ifnot os.path.exists(extractionDir): os.mkdir(extractionDir) 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 caseassertlen(data)== entry.uncmprsdDataSize # Sanity Checkif entry.typeCmprsData ==b'd'or entry.typeCmprsData ==b'o':# d -> ARCHIVE_ITEM_DEPENDENCY# o -> ARCHIVE_ITEM_RUNTIME_OPTION# These are runtime options, not filescontinue basePath = os.path.dirname(entry.name)if basePath !='':# Check if path exists, create if notifnot os.path.exists(basePath): os.makedirs(basePath)if entry.typeCmprsData ==b's':# s -> ARCHIVE_ITEM_PYSOURCE# Entry point are expected to be python scriptsprint('[+] 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 intact# From PyInstaller 5.3 and above pyc headers are no longer stored# https://github.com/pyinstaller/pyinstaller/commit/a97fdfif data[2:4]==b'\r\n':# < pyinstaller 5.3if self.pycMagic ==b'\0'*4: self.pycMagic = data[0:4] self._writeRawData(entry.name +'.pyc', data)else:# >= pyinstaller 5.3if 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)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:withopen(pycFile,'r+b')as pycFile:# Overwrite the first four bytes pycFile.write(self.pycMagic)def_writePyc(self, filename, data):withopen(filename,'wb')as pycFile: pycFile.write(self.pycMagic)# pyc magicif self.pymaj >=3and 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)# Timestampif self.pymaj >=3and self.pymin >=3: pycFile.write(b'\0'*4)# Size parameter added in Python 3.3 pycFile.write(data)def_extractPyz(self, name): dirName = name +'_extracted'# Create a directory for the contents of the pyzifnot os.path.exists(dirName): os.mkdir(dirName)withopen(name,'rb')as f: pyzMagic = f.read(4)assert pyzMagic ==b'PYZ\0'# Sanity Check pyzPycMagic = f.read(4)# Python magic valueif 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 versionif 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}.{1} to prevent extraction errors during unmarshalling'.format(self.pymaj, self.pymin))print('[!] Skipping pyz extraction')return(tocPosition,)= struct.unpack('!i', f.read(4)) f.seek(tocPosition, os.SEEK_SET)try: toc = marshal.load(f)except:print('[!] Unmarshalling FAILED. Cannot extract {0}. Extracting remaining files.'.format(name))returnprint('[+] Found {0} files in PYZ archive'.format(len(toc)))# From pyinstaller 3.1+ toc is a list of tuplesiftype(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 fileName = fileName.decode('utf-8')except:pass# Prevent writing outside dirName fileName = fileName.replace('..','__').replace('.', os.path.sep)if ispkg ==1: filePath = os.path.join(dirName, fileName,'__init__.pyc')else: filePath = os.path.join(dirName, fileName +'.pyc') fileDir = os.path.dirname(filePath)ifnot os.path.exists(fileDir): os.makedirs(fileDir)try: data = f.read(length) data = zlib.decompress(data)except:print('[!] Error: Failed to decompress {0}, probably encrypted. Extracting as is.'.format(filePath))open(filePath +'.encrypted','wb').write(data)else: self._writePyc(filePath, data)defmain():iflen(sys.argv)<2:print('[+] Usage: pyinstxtractor.py <filename>')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()- 将
暴富.exe跟unpack.py放在同一个目录中,cmd执行如下命令:等待出现Successfully
python unpack.py 暴富.exe 
在同目录下生成的暴富.exe_extracted文件夹里找到暴富.pyc

二、将pyc文件反编译成py代码
将暴富.pyc拖入PyLingual反编译器进行反编译,如果打不开网址就使用魔法

经测试,PyLingual能够正确反编译3.13及以下所有版本的pyc🐂🍺