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既是加密后的也是引入外部数据的。

7 为exe添加图标

找一个在线生成图标ico的网站,生成自己想要的图标,我用的是http://www.faviconico.org/favicon,然后将图标放在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,
		  icon='ico.ico') #增加的图标

然后使用命令

pyinstaller TestAdd.spec

生成exe文件,我们可以看到exe是带图标的了
在这里插入图片描述

如果您觉得这篇博文有用,请访问我的个人站:http://www.stubbornhuang.com/,更多博文干货等着您。

  • 12
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HW140701

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值