PyInstaller:編譯exe與反編譯

1 簡單Python代碼示例

TestAdd.py

#__author__ = 'StubbornHuang'
#coding = utf-8

import io
import os
import sys

def addTest(a,b):
	print ("a+b={}".format(a+b))
	

if __name__ == '__main__':
	addTest(1,5)

2 安裝PyInstaller

輸入以下命令安裝pyinstaller:

pip install pyinstaller

驗證是否安裝成功,輸入以下命令:

pyinstaller

在這裏插入圖片描述

3 不加密直接編譯exe

在需要打包的py文件目錄下啓動cmd.exe,或者PowerShell.exe,我自己用的是Cmder.exe。
在這裏插入圖片描述
輸入以下命令:

pyinstaller TestAdd.py

在這裏插入圖片描述
在這裏插入圖片描述
打包的exe在py文件所在目錄的dist子目錄下
在這裏插入圖片描述
如果直接運行TestAdd.exe會一閃而過,最好在當前exe所在目錄下執行命令運行:

./TestAdd.exe

我們可以看到直接運行成功。
在這裏插入圖片描述

4 對PyInstaller打包的不加密編譯exe進行反編譯

使用pyinstxtractor.py 對上述不加密的exe進行反編譯,其中pyinstxtractor.py文件內容如下:

"""
PyInstaller Extractor v1.9 (Supports pyinstaller 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
Date   : 29-November-2017
Url    : https://sourceforge.net/projects/pyinstallerextractor/

For any suggestions, leave a comment on
https://forum.tuts4you.com/topic/34455-pyinstaller-extractor/

This script extracts a pyinstaller generated executable file.
Pyinstaller installation is not needed. The script has it all.

For best results, it is recommended to run this script in the
same version of python as was used to create the executable.
This is just to prevent unmarshalling errors(if any) while
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>
$ /path/to/exe/python pyinstxtractor.py <filename>

Licensed under GNU General Public License (GPL) v3.
You are free to modify this source.

CHANGELOG
================================================

Version 1.1 (Jan 28, 2014)
-------------------------------------------------
- First Release
- Supports only pyinstaller 2.0

Version 1.2 (Sept 12, 2015)
-------------------------------------------------
- Added support for pyinstaller 2.1 and 3.0 dev
- Cleaned up code
- Script is now more verbose
- Executable extracted within a dedicated sub-directory

(Support for pyinstaller 3.0 dev is experimental)

Version 1.3 (Dec 12, 2015)
-------------------------------------------------
- Added support for pyinstaller 3.0 final
- Script is compatible with both python 2.x & 3.x (Thanks to Moritz Kroll @ Avira Operations GmbH & Co. KG)

Version 1.4 (Jan 19, 2016)
-------------------------------------------------
- Fixed a bug when writing pyc files >= version 3.3 (Thanks to Daniello Alto: https://github.com/Djamana)

Version 1.5 (March 1, 2016)
-------------------------------------------------
- Added support for pyinstaller 3.1 (Thanks to Berwyn Hoyt for reporting)

Version 1.6 (Sept 5, 2016)
-------------------------------------------------
- Added support for pyinstaller 3.2
- Extractor will use a random name while extracting unnamed files.
- For encrypted pyz archives it will dump the contents as is. Previously, the tool would fail.

Version 1.7 (March 13, 2017)
-------------------------------------------------
- Made the script compatible with python 2.6 (Thanks to Ross for reporting)

Version 1.8 (April 28, 2017)
-------------------------------------------------
- Support for sub-directories in .pyz files (Thanks to Moritz Kroll @ Avira Operations GmbH & Co. KG)

Version 1.9 (November 29, 2017)
-------------------------------------------------
- Added support for pyinstaller 3.3
- Display the scripts which are run at entry (Thanks to Michael Gillespie @ malwarehunterteam for the feature request)

"""

from __future__ import print_function
import os
import struct
import marshal
import zlib
import sys
import imp
import types
from uuid import uuid4 as uniquename


class CTOCEntry:
    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


class PyInstArchive:
    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 pyinstaller

    def __init__(self, path):
        self.filePath = path


    def open(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))
            return False
        return True


    def close(self):
        try:
            self.fPtr.close()
        except:
            pass


    def checkFile(self):
        print('[*] Processing {0}'.format(self.filePath))
        # Check if it is a 2.0 archive
        self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET)
        magicFromFile = self.fPtr.read(len(self.MAGIC))

        if magicFromFile == self.MAGIC:
            self.pyinstVer = 20     # pyinstaller 2.0
            print('[*] Pyinstaller version: 2.0')
            return True

        # Check for pyinstaller 2.1+ before bailing out
        self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET)
        magicFromFile = self.fPtr.read(len(self.MAGIC))

        if magicFromFile == self.MAGIC:
            print('[*] Pyinstaller version: 2.1+')
            self.pyinstVer = 21     # pyinstaller 2.1+
            return True

        print('[*] Error : Unsupported pyinstaller version or not a pyinstaller archive')
        return False


    def getCArchiveInfo(self):
        try:
            if self.pyinstVer == 20:
                self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET)

                # Read CArchive cookie
                (magic, lengthofPackage, toc, tocLen, self.pyver) = \
                struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE))

            elif self.pyinstVer == 21:
                self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET)

                # Read CArchive cookie
                (magic, lengthofPackage, toc, tocLen, self.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))

        # Overlay is the data appended at the end of the PE
        self.overlaySize = lengthofPackage
        self.overlayPos = self.fileSize - self.overlaySize
        self.tableOfContentsPos = self.overlayPos + toc
        self.tableOfContentsSize = tocLen

        print('[*] Length of package: {0} bytes'.format(self.overlaySize))
        return True


    def parseTOC(self):
        # Go to the table of contents
        self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET)

        self.tocList = []
        parsedLen = 0

        # Parse table of contents
        while 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))

            name = name.decode('utf-8').rstrip('\0')
            if len(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 extractFiles(self):
        print('[*] Beginning extraction...please standby')
        extractionDir = os.path.join(os.getcwd(), os.path.basename(self.filePath) + '_extracted')

        if not os.path.exists(extractionDir):
            os.mkdir(extractionDir)

        os.chdir(extractionDir)

        for entry in self.tocList:
            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

            with open(entry.name, 'wb') as f:
                f.write(data)

            if entry.typeCmprsData == b's':
            	print('[+] Possible entry point: {0}'.format(entry.name))

            elif entry.typeCmprsData == b'z' or entry.typeCmprsData == b'Z':
                self._extractPyz(entry.name)


    def _extractPyz(self, name):
        dirName =  name + '_extracted'
        # Create a directory for the contents of the pyz
        if not os.path.exists(dirName):
            os.mkdir(dirName)

        with open(name, 'rb') as f:
            pyzMagic = f.read(4)
            assert pyzMagic == b'PYZ\0' # Sanity Check

            pycHeader = f.read(4) # Python magic value

            if imp.get_magic() != pycHeader:
                print('[!] Warning: The script is running in a different python version than the one used to build the executable')
                print('    Run this script in Python{0} to prevent extraction errors(if any) during unmarshalling'.format(self.pyver))

            (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))
                return

            print('[*] Found {0} files in PYZ archive'.format(len(toc)))

            # From pyinstaller 3.1+ toc is a list of tuples
            if type(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 = key.decode('utf-8')
                except:
                    pass

                # Make sure destination directory exists, ensuring we keep inside dirName
                destName = os.path.join(dirName, fileName.replace("..", "__"))
                destDirName = os.path.dirname(destName)
                if not os.path.exists(destDirName):
                    os.makedirs(destDirName)

                try:
                    data = f.read(length)
                    data = zlib.decompress(data)
                except:
                    print('[!] Error: Failed to decompress {0}, probably encrypted. Extracting as is.'.format(fileName))
                    open(destName + '.pyc.encrypted', 'wb').write(data)
                    continue

                with open(destName + '.pyc', 'wb') as pycFile:
                    pycFile.write(pycHeader)      # Write pyc magic
                    pycFile.write(b'\0' * 4)      # Write timestamp
                    if self.pyver >= 33:
                        pycFile.write(b'\0' * 4)  # Size parameter added in Python 3.3
                    pycFile.write(data)


def main():
    if len(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()

將pyinstxtractor.py文件複製到TestAdd.exe同目錄下,運行cmd.exe,輸入以下命令進行反編譯:

python pyinstxtractor.py TestAdd.exe

在這裏插入圖片描述
反編譯的文件在exe所在目錄的後綴爲exe_extracted文件夾下,示例的提取目錄爲TestAdd.exe_extracted/PYZ-00.pyz_extracted文件下
在這裏插入圖片描述
在TestAdd.exe_extracted下有從TextAdd.exe提取出來的pyc文件,我們可以使用反編譯工具進行反編譯。

5 加密編譯exe

如果我們要增加反編譯pyinstaller打包的exe文件的難度該怎麼辦?添加key值。
在py目錄啓動cmd.exe,輸入以下命令:

pyinstaller -F --key 123456789 TestAdd.py

其中運行選項:
-F:強制編譯爲單個exe文件,不要多餘的文件;
–key 123456789:使用key123456789進行加密編譯;

5.1 注意事項

需要注意的是,在運行上述命令時如果你沒有安裝pycrypto第三方庫,則需要執行下述命令進行安裝:

pip install pycrypto

進行安裝,在這其中大概率會出現如下錯誤:

error C2061: 語法錯誤: 標識符“intmax_t”;

error C2059: 語法錯誤:“;” ;

error C2143: 語法錯誤: 缺少“{”(在“__cdecl”的前面)等等

等。
在這裏插入圖片描述
解決方案:
1 進入電腦VS安裝目錄下,搜索stdint.h(示例路徑:D:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include),將該文件複製到以下路徑,示例路徑:C:/Program Files (x86)/Windows Kits/10/Include/10.0.15063.0/ucrt/
2 然後在C:/Program Files (x86)/Windows Kits/10/Include/10.0.15063.0/ucrt/下找到inttypes.h文件,進行文件編譯,
將包含頭文件的代碼:

#include <stdint.h>

修改爲

#include "stdint.h"

3 重新運行

pip install pycrypto

進行安裝pycrypto,應該就可以了。

5.2 加密編譯

在這裏插入圖片描述
在這裏插入圖片描述
這是dist目錄下只有單個的TestAdd.exe文件。
在這裏插入圖片描述
這是我們依然採用pyinstxtractor.py對其進行反編譯
在這裏插入圖片描述
可以看到出現了較多的decompress Error 解壓錯誤,TestAdd.exe_extracted/PYZ-00.pyz_extracted文件夾下的文件都是加密的。
在這裏插入圖片描述
這種方式增加了反編譯pyinstaller打包的exe文件的難度,在一定程度上增加了python源代碼的保護性。

6 將外部數據打包到exe中

將TestAdd.py文件內容修改如下:

#__author__ = 'StubbornHuang'
#coding = utf-8

import io
import os
import sys

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

def printTestFile1():
	file = 'Test1.txt'
	print(resource_path(file))
	with open(resource_path(file), 'r',encoding='UTF-8') as f:
		while True:
			line = f.readline()     # 逐行讀取
			if not line:
				break
			print(line)

def printTestFile2():
	file = 'data/Test2.txt'
	print(resource_path(file))
	with open(resource_path(file), 'r',encoding='UTF-8') as f:
		while True:
			line = f.readline()     # 逐行讀取
			if not line:
				break
			print(line)

	

if __name__ == '__main__':
	printTestFile1()
	printTestFile2()

然後在py所在目錄新建Test1.txt,並輸入以下內容:

pyinstaller外部數據打包測試(單文件)

然後在py所在目錄新建data子文件夾,在data文件夾下新建Test2.txt,並輸入以下內容:

pyinstaller外部數據打包測試(文件夾下的目錄)
在這裏插入圖片描述
在這裏插入圖片描述
好的,將上述準備工作做好之後則開始進行將外部數據增加到exe中

6.1 如果不需要加密編譯

則在py文件所在目錄輸入以下命令

pyi-makespec -F TestAdd.py

執行完命令後,可以看到py所在目錄下新增了文件TestAdd.spec

在這裏插入圖片描述
該文件內容爲:

# -*- mode: python -*-

block_cipher = None


a = Analysis(['TestAdd.py'],
             pathex=['C:\\Users\\Administrator\\Desktop\\PythonExe\\Test'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='TestAdd',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=True )

將上述文件進行修改,添加外部數據,修改如下:

# -*- mode: python -*-

block_cipher = None


a = Analysis(['TestAdd.py'],
             pathex=['C:\\Users\\Administrator\\Desktop\\PythonExe\\Test'],
             binaries=[], 
             datas=[('Test1.txt','.'),('data/Test2.txt','data')],#修改處
             hiddenimports=[], #填入需要導入的第三方庫,例如flask
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='TestAdd',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=True )

上述修改會將Test1.txt以及/data/Test2.txt文件在運行時複製到可執行程序的臨時目錄以便可執行程序可以找到相應的文件。
修改後,使用命令:

pyinstaller TestAdd.spec

進行編譯。
在這裏插入圖片描述
如果出現找不到Test1.txt或者/data/Test2.txt的錯誤,是因爲運行可執行文件時,會先將可執行文件進行壓縮,壓縮的位置在 /tmp 下,再執行,所以被打包進去的數據文件在被解壓的路徑下,而,程序是在運行的路徑下搜索,即可執行文件的目錄下,所以找不到數據文件。
所以我們在編寫TestAdd.py文件時,添加了如下函數

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

用於尋找pyinstaller臨時文件目錄。

6.2 如果需要加密編譯

如果需要加密編譯,參照第5節,運行以下命令:

pyinstaller -F --key 123456789 TestAdd.py

生成TestAdd.spec,其文件內容如下:

# -*- mode: python -*-

block_cipher = pyi_crypto.PyiBlockCipher(key='123456789')


a = Analysis(['TestAdd.py'],
             pathex=['C:\\Users\\Administrator\\Desktop\\PythonExe\\Test'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='TestAdd',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=True )

將其修改爲:

# -*- mode: python -*-

block_cipher = pyi_crypto.PyiBlockCipher(key='123456789')


a = Analysis(['TestAdd.py'],
             pathex=['C:\\Users\\Administrator\\Desktop\\PythonExe\\Test'],
             binaries=[],
             datas=[('Test1.txt','.'),('data/Test2.txt','data')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='TestAdd',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=True )

保存之後,執行命令:

pyinstaller TestAdd.spec

編譯文件,這樣,編譯出來的exe既是加密後的也是引入外部數據的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章