找回密码
 会员注册
查看: 34|回复: 0

记一次python逆向:反编译pyinstaller打包生成的exe

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64098
发表于 2024-9-13 11:18:21 | 显示全部楼层 |阅读模式
首先声明一点,逆向这块我是纯小白,我是通过看各种博客,反复折腾才搞出来的,写的有错误的地方,还请各位大佬多多指正0前置条件确认需要逆向的exe文件是不是通过pyinstaller打包的,如果不是,那么这个方法就是无效的我们可以通过图标来确定,pyinstaller打包的图标一般如下两种1需要用到的环境及工具(1)电脑需要有python环境,我用的是python3.9(2)pyinstxtractor.py----将.exe解包官方GitHub地址https://github.com/countercept/python-exe-unpacker/blob/master/pyinstxtractor.py(3)uncompyle6----.pyc转.py#安装可以加上清华镜像源,自行搜索安装即可pipinstalluncompyle6(4)WinHex----16进制编辑(用其他也可以)2将exe文件进行解包将pyinstxtractor.py文件和需要解包的exe文件(我这里叫xxxx.exx)放在同一个目录下,执行如下命令pythonpyinstxtractor.pyxxxx.exe解包后会生成一个xxxx.exe_extracted文件夹,我们可以通过这个文件夹看到这个exe文件是通过什么版本的python打包的可以看到,我这个exe文件是通过python3.8打包的,但具体是哪个版本的,就无法得知在xxxx.exe_extracted文件中,我们只需要关注该目录下没有后缀名的文件和PYZ-00.pyz_extracted这个文件夹没有后缀名的文件,在我这个例子中有两个:1和struct其中,这个1文件其实就是程序的入口文件,PYZ-00.pyz_extracted文件夹存放主程序的一些依赖库,包括自己写的PYZ-00.pyz_extracted这个文件夹下的文件大概如下,分两种情况1文件都是以pyc结尾的,说明没有加密,我们可以用uncompyle6将其反编译成.py文件(也就是源码)2文件都是以.pyc.encrypted结尾的,说明经过了加密,感兴趣的可以研究如何解密,我这里就不说明了3将pyc文件转为py文件将pyc文件转为py文件,最重要的是需要补全magichead。想要快速将pyc转为py文件,而不需要补全magichead,请直接看3.3节。3.1反编译程序入口文件上面说过,在我这个例子中,1这个文件就是程序的入口文件,一般来说你的exe文件如果是demo.exe,那么你这个入口文件大概率是demo。(1)重命名程序入口文件上面说过1这个文件是没有后缀名的,需要给它加上.pyc的后缀名,变为1.pyc;同理,将struct文件改为struct.pyc(2)用WinHex打开上面1.pyc和struct.pyc,对比字节码的差异,补全magichead这里我就有点懵,因为我看其它博客struct.pyc文件打开后,第一行并不是E3开头,而是类似下面这种这里无法根据struct.pyc进行补全,我又搜索其它博客,发现有人和我情况一样,struct.pyc打开直接就是E3开头,可惜没有人回答这个问题不多说,直接上解决办法:在exe解包的目录下有一个base_library.zip压缩包,打开这个压缩包,随便找一个oyc文件,使用WinHex打开,这里我打开的是base_library中的abc.pyc文件对比两个文件,发现550d0d0a01这第一行的字节码就是我们需要的magichead我们直接把abc.pyc文件中的第一行复制,添加到1.pyc文件中的第一行保存,然后用uncompyle6反编译uncompyle6-o1.py1.pyc反编译后,没有报错,并且生成了1.py文件,打开1.pyc文件,能够看到源码,说明magichead是正确的3.2反编译依赖库反编译程序入口后,我们可以看到,该程序依赖了一个demo的库,这个库肯定是作者自己写的依赖的demo库在PYZ-00.pyz_extracted文件夹中可以找到,打开demo.pyc与1.pyc进行对比这里我们可以发现,依赖库缺失的字节是在中间位置,这里我们看到缺少的是12-15位的四个字节00000000补全缺失的四个字节保存,然后用uncompyle6反编译uncompyle6-odemo.pydemo.pyc反编译后,没有报错,并且生成了demo.py文件,打开demo.pyc文件,能够看到源码,说明我们补全的字节码是正确的可以看到,作者这里使用了PyArmor对源码进行了加密处理,这种加密方式目前无解但如果你发编译的代码没有经过类型的加密,那么你到这一步是可以完全解密出来对应的依赖库,当然,依赖库可能不止一个,但方法都是一样的3.3快速补全magichead上面的3.1和3.2节,均是在教你如何补全通过分析文件来补全magichead,这对新手来说理解起来有些困难并且在操作过程中,如果遇到想我一个struct.pyc文件直接就是E3开头,更是天崩开局,,,,所谓的快速补全magichead,其实就是使用最新版本的pyinstxtractor.py文件,经过实测,我发现最新版本的pyinstxtractor.py能够直接对程序入口pyc文件和依赖库pyc文件自动补全magichead,着实厉害。最新的pyinstxtractor.py文件内容,我会贴在下方,避免有些伙伴无法连接GitHub不用补全magichead后,你就可以直接使用uncompyle6反编译,帮助我们节约了不少时间uncompyle6-oxxx.pyxxx.pyc最新的pyinstxtractor内容如下"""PyInstallerExtractorv2.0(Supportspyinstaller6.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:ExtremeCodersE-mail:extremecoders(at)hotmail(dot)comWeb:https://0xec.blogspot.comDate:26-March-2020Url:https://github.com/extremecoders-re/pyinstxtractorForanysuggestions,leaveacommentonhttps://forum.tuts4you.com/topic/34455-pyinstaller-extractor/Thisscriptextractsapyinstallergeneratedexecutablefile.Pyinstallerinstallationisnotneeded.Thescripthasitall.Forbestresults,itisrecommendedtorunthisscriptinthesameversionofpythonaswasusedtocreatetheexecutable.Thisisjusttopreventunmarshallingerrors(ifany)whileextractingthePYZarchive.Usage:JustcopythisscripttothedirectorywhereyourexeresidesandrunthescriptwiththeexefilenameasaparameterC:\\path\\to\\exe\\>pythonpyinstxtractor.py$/path/to/exe/pythonpyinstxtractor.pyLicensedunderGNUGeneralPublicLicense(GPL)v3.Youarefreetomodifythissource.CHANGELOG================================================Version1.1(Jan28,2014)--------------------------------------------------FirstRelease-Supportsonlypyinstaller2.0Version1.2(Sept12,2015)--------------------------------------------------Addedsupportforpyinstaller2.1and3.0dev-Cleanedupcode-Scriptisnowmoreverbose-Executableextractedwithinadedicatedsub-directory(Supportforpyinstaller3.0devisexperimental)Version1.3(Dec12,2015)--------------------------------------------------Addedsupportforpyinstaller3.0final-Scriptiscompatiblewithbothpython2.x&3.x(ThankstoMoritzKroll@AviraOperationsGmbH&Co.KG)Version1.4(Jan19,2016)--------------------------------------------------Fixedabugwhenwritingpycfiles>=version3.3(ThankstoDanielloAlto:https://github.com/Djamana)Version1.5(March1,2016)--------------------------------------------------Addedsupportforpyinstaller3.1(ThankstoBerwynHoytforreporting)Version1.6(Sept5,2016)--------------------------------------------------Addedsupportforpyinstaller3.2-Extractorwillusearandomnamewhileextractingunnamedfiles.-Forencryptedpyzarchivesitwilldumpthecontentsasis.Previously,thetoolwouldfail.Version1.7(March13,2017)--------------------------------------------------Madethescriptcompatiblewithpython2.6(ThankstoRossforreporting)Version1.8(April28,2017)--------------------------------------------------Supportforsub-directoriesin.pyzfiles(ThankstoMoritzKroll@AviraOperationsGmbH&Co.KG)Version1.9(November29,2017)--------------------------------------------------Addedsupportforpyinstaller3.3-Displaythescriptswhicharerunatentry(ThankstoMichaelGillespie@malwarehunterteamforthefeaturerequest)Version2.0(March26,2020)--------------------------------------------------Projectmigratedtogithub-Supportspyinstaller3.6-AddedsupportforPython3.7,3.8-Theheaderofallextractedpyc'sarenowautomaticallyfixed"""from__future__importprint_functionimportosimportstructimportmarshalimportzlibimportsysfromuuidimportuuid4asuniquenameclassCTOCEntry:def__init__(self,position,cmprsdDataSize,uncmprsdDataSize,cmprsFlag,typeCmprsData,name):self.position=positionself.cmprsdDataSize=cmprsdDataSizeself.uncmprsdDataSize=uncmprsdDataSizeself.cmprsFlag=cmprsFlagself.typeCmprsData=typeCmprsDataself.name=nameclassPyInstArchiveYINST20_COOKIE_SIZE=24#Forpyinstaller2.0PYINST21_COOKIE_SIZE=24+64#Forpyinstaller2.1+MAGIC=b'MEI\014\013\012\013\016'#Magicnumberwhichidentifiespyinstallerdef__init__(self,path):self.filePath=pathself.pycMagic=b'\0'*4self.barePycList=[]#Listofpyc'swhoseheadershavetobefixeddefopen(self):try:self.fPtr=open(self.filePath,'rb')self.fileSize=os.stat(self.filePath).st_sizeexcept:print('[!]Error:Couldnotopen{0}'.format(self.filePath))returnFalsereturnTruedefclose(self):try:self.fPtr.close()except:passdefcheckFile(self):print('[+]Processing{0}'.format(self.filePath))searchChunkSize=8192endPos=self.fileSizeself.cookiePos=-1ifendPos=searchChunkSizeelse0chunkSize=endPos-startPosifchunkSize=100else(pyver//10,pyver%10)print('[+]Pythonversion:{0}.{1}'.format(self.pymaj,self.pymin))#AdditionaldataafterthecookietailBytes=self.fileSize-self.cookiePos-(self.PYINST20_COOKIE_SIZEifself.pyinstVer==20elseself.PYINST21_COOKIE_SIZE)#OverlayisthedataappendedattheendofthePEself.overlaySize=lengthofPackage+tailBytesself.overlayPos=self.fileSize-self.overlaySizeself.tableOfContentsPos=self.overlayPos+tocself.tableOfContentsSize=tocLenprint('[+]Lengthofpackage:{0}bytes'.format(lengthofPackage))returnTruedefparseTOC(self):#Gotothetableofcontentsself.fPtr.seek(self.tableOfContentsPos,os.SEEK_SET)self.tocList=[]parsedLen=0#ParsetableofcontentswhileparsedLenARCHIVE_ITEM_DEPENDENCY#o->ARCHIVE_ITEM_RUNTIME_OPTION#Theseareruntimeoptions,notfilescontinuebasePath=os.path.dirname(entry.name)ifbasePath!='':#Checkifpathexists,createifnotifnotos.path.exists(basePath)s.makedirs(basePath)ifentry.typeCmprsData==b's':#s->ARCHIVE_ITEM_PYSOURCE#Entrypointareexpectedtobepythonscriptsprint('[+]Possibleentrypoint:{0}.pyc'.format(entry.name))ifself.pycMagic==b'\0'*4:#ifwedon'thavethepycheaderyet,fixtheminalaterpassself.barePycList.append(entry.name+'.pyc')self._writePyc(entry.name+'.pyc',data)elifentry.typeCmprsData==b'M'orentry.typeCmprsData==b'm':#M->ARCHIVE_ITEM_PYPACKAGE#m->ARCHIVE_ITEM_PYMODULE#packagesandmodulesarepycfileswiththeirheaderintact#FromPyInstaller5.3andabovepycheadersarenolongerstored#https://github.com/pyinstaller/pyinstaller/commit/a97fdfifdata[2:4]==b'\r\n':#=pyinstaller5.3ifself.pycMagic==b'\0'*4:#ifwedon'thavethepycheaderyet,fixtheminalaterpassself.barePycList.append(entry.name+'.pyc')self._writePyc(entry.name+'.pyc',data)else:self._writeRawData(entry.name,data)ifentry.typeCmprsData==b'z'orentry.typeCmprsData==b'Z':self._extractPyz(entry.name)#Fixbarepyc'sifanyself._fixBarePycs()def_fixBarePycs(self):forpycFileinself.barePycList:withopen(pycFile,'r+b')aspycFile:#OverwritethefirstfourbytespycFile.write(self.pycMagic)def_writePyc(self,filename,data):withopen(filename,'wb')aspycFile:pycFile.write(self.pycMagic)#pycmagicifself.pymaj>=3andself.pymin>=7:#PEP552--DeterministicpycspycFile.write(b'\0'*4)#BitfieldpycFile.write(b'\0'*8)#(Timestamp+size)||hashelse:pycFile.write(b'\0'*4)#Timestampifself.pymaj>=3andself.pymin>=3:pycFile.write(b'\0'*4)#SizeparameteraddedinPython3.3pycFile.write(data)def_extractPyz(self,name):dirName=name+'_extracted'#Createadirectoryforthecontentsofthepyzifnotos.path.exists(dirName)s.mkdir(dirName)withopen(name,'rb')asf:pyzMagic=f.read(4)assertpyzMagic==b'PYZ\0'#SanityCheckpyzPycMagic=f.read(4)#Pythonmagicvalueifself.pycMagic==b'\0'*4:self.pycMagic=pyzPycMagicelifself.pycMagic!=pyzPycMagic:self.pycMagic=pyzPycMagicprint('[!]Warning:pycmagicoffilesinsidePYZarchivearedifferentfromthoseinCArchive')#SkipPYZextractionifnotrunningunderthesamepythonversionifself.pymaj!=sys.version_info.majororself.pymin!=sys.version_info.minor:print('[!]Warning:ThisscriptisrunninginadifferentPythonversionthantheoneusedtobuildtheexecutable.')print('[!]PleaserunthisscriptinPython{0}.{1}topreventextractionerrorsduringunmarshalling'.format(self.pymaj,self.pymin))print('[!]Skippingpyzextraction')return(tocPosition,)=struct.unpack('!i',f.read(4))f.seek(tocPosition,os.SEEK_SET)try:toc=marshal.load(f)except:print('[!]UnmarshallingFAILED.Cannotextract{0}.Extractingremainingfiles.'.format(name))returnprint('[+]Found{0}filesinPYZarchive'.format(len(toc)))#Frompyinstaller3.1+tocisalistoftuplesiftype(toc)==list:toc=dict(toc)forkeyintoc.keys()ispkg,pos,length)=toc[key]f.seek(pos,os.SEEK_SET)fileName=keytry:#forPython>3.3somekeysarebytesobjectsomearestrobjectfileName=fileName.decode('utf-8')except:pass#PreventwritingoutsidedirNamefileName=fileName.replace('..','__').replace('.',os.path.sep)ifispkg==1:filePath=os.path.join(dirName,fileName,'__init__.pyc')else:filePath=os.path.join(dirName,fileName+'.pyc')fileDir=os.path.dirname(filePath)ifnotos.path.exists(fileDir)s.makedirs(fileDir)try:data=f.read(length)data=zlib.decompress(data)except:print('[!]Error:Failedtodecompress{0},probablyencrypted.Extractingasis.'.format(filePath))open(filePath+'.encrypted','wb').write(data)else:self._writePyc(filePath,data)defmain():iflen(sys.argv)')else:arch=PyInstArchive(sys.argv[1])ifarch.open():ifarch.checkFile():ifarch.getCArchiveInfo():arch.parseTOC()arch.extractFiles()arch.close()print('[+]Successfullyextractedpyinstallerarchive:{0}'.format(sys.argv[1]))print('')print('Youcannowuseapythondecompileronthepycfileswithintheextracteddirectory')returnarch.close()if__name__=='__main__':main()
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 12:18 , Processed in 0.597947 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表