Python中如何安全地進行編譯和反編譯?
用Python寫腳本,小程序可謂非常方便,但它需要有特定的python環境才能運行,因此如果你想在別的電腦上運行時就會出現許多問題,就算已經安裝了Python,但版本可能相差較大,且相關的依賴庫沒有安裝,同樣不能正常運行。那有沒有一種工具能把我們寫的代碼和依賴庫以及編譯環境打包到一起呢?答案是肯定的,Pyinstaller就是一款不錯的工具,可以一鍵把你的代碼打包成exe文件。下面就先來聊一聊pyinstaller的使用方法。
一、用Pyinstaller打包python代碼
1. 安裝Pyinstaller
安裝過程非常簡單,在命令行中運行:
- pip install pyinstaller
即可完成安裝。
2. 打包代碼
我寫了一段簡單的代碼作為例子,為了更清晰地演示打包過程,我將main()函數寫在了單獨的文件中,并將mylib.py作為一個庫引入。
- # mylib.py
- #
- import time
- def myfunc():
- now = time.time()
- timetime_str = time.strftime("%Y-%m-%d %H:%M", time.localtime(now))
- print('現在是' + time_str)
- print("Have a nice day!")
- # main.py
- #
- import mylib
- import os
- if __name__ == "__main__":
- mylib.myfunc()
- os.system('pause')
這時只需要在命令行中運行:
- pyinstaller.exe -F yourcode.py
即可。會看到一下輸出:
- PS D:\文檔\tmp\test> pyinstaller.exe -F main.py
- 580 INFO: PyInstaller: 3.6
- 582 INFO: Python: 3.7.3
- 585 INFO: Platform: Windows-10-10.0.18362-SP0
- 592 INFO: wrote D:\文檔\tmp\test\main.spec
- 596 INFO: UPX is not available.
- 611 INFO: Extending PYTHONPATH with paths
- ['D:\\文檔\\tmp\\test', 'D:\\文檔\\tmp\\test']
- 612 INFO: checking Analysis
- 614 INFO: Building Analysis because Analysis-00.toc is non existent
- 614 INFO: Initializing module dependency graph...
- 620 INFO: Caching module graph hooks...
- 657 INFO: Analyzing base_library.zip ...
- 13893 INFO: Caching module dependency graph...
- 14161 INFO: running Analysis Analysis-00.toc
- 14233 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
- required by d:\programfiles\python\python.exe
- 15748 INFO: Analyzing D:\文檔\tmp\test\main.py
- 15751 INFO: Processing module hooks...
- 15752 INFO: Loading module hook "hook-encodings.py"...
- 16003 INFO: Loading module hook "hook-pydoc.py"...
- 16011 INFO: Loading module hook "hook-xml.py"...
- 16916 INFO: Looking for ctypes DLLs
- 16917 INFO: Analyzing run-time hooks ...
- 16925 INFO: Looking for dynamic libraries
- 17373 INFO: Looking for eggs
- 17374 INFO: Using Python library d:\programfiles\python\python37.dll
- 17374 INFO: Found binding redirects:
- []
- 17377 INFO: Warnings written to D:\文檔\tmp\test\build\main\warn-main.txt
- 17447 INFO: Graph cross-reference written to D:\文檔\tmp\test\build\main\xref-main.html
- 17506 INFO: checking PYZ
- 17507 INFO: Building PYZ because PYZ-00.toc is non existent
- 17508 INFO: Building PYZ (ZlibArchive) D:\文檔\tmp\test\build\main\PYZ-00.pyz
- 18600 INFO: Building PYZ (ZlibArchive) D:\文檔\tmp\test\build\main\PYZ-00.pyz completed successfully.
- 18637 INFO: checking PKG
- 18639 INFO: Building PKG because PKG-00.toc is non existent
- 18640 INFO: Building PKG (CArchive) PKG-00.pkg
- 22329 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
- 22332 INFO: Bootloader d:\programfiles\python\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe
- 22334 INFO: checking EXE
- 22335 INFO: Building EXE because EXE-00.toc is non existent
- 22336 INFO: Building EXE from EXE-00.toc
- 22416 INFO: Appending archive to EXE D:\文檔\tmp\test\dist\main.exe
- 22641 INFO: Building EXE from EXE-00.toc completed successfully.
你將在當前文件夾下看到如下文件:
- D:.
- │ main.py
- │ main.spec
- │ mylib.py
- ├─build
- │ └─main
- │ Analysis-00.toc
- │ base_library.zip
- │ EXE-00.toc
- │ main.exe.manifest
- │ PKG-00.pkg
- │ PKG-00.toc
- │ PYZ-00.pyz
- │ PYZ-00.toc
- │ warn-main.txt
- │ xref-main.html
- └─dist
- main.exe
其中dist文件夾中就是生成的exe文件。
直接雙擊exe文件就能正常運行了。
但這篇文章的重點并不是介紹如何使用pyinstaller。此時,我又在思考,如此簡單的打包過程究竟安全嗎?打包成的exe文件會不會輕而易舉地被反編譯?
查閱了相關資料后發現,確實可能。
二、pyinstaller 的反編譯過程
1. 下載并使用pyinstxtractor解包
我們第一步采用的工具是pyinstxtractor.py,可以將pyinstaller 生成的exe文件解包成pyc文件。
項目地址:
https://sourceforge.net/projects/pyinstallerextractor/
之后把這個文件復制到待解包exe同級目錄下,運行如下命令:
- python pyinstxtractor.py xx.exe
運行后生成xx.exe_extracted文件夾 ,里面有一堆dll ,pyd等文件,我們需要注意的是里面有一個xxx.exe.manifest文件,xxx可能與你的exe文件名不同,但這才是它的真實名字。然后找到一個叫xxx的沒有后綴名的文件,它其實就是你之前打包的那個.py文件對應的pyc文件。
找到一個叫xxx的沒有后綴名的文件
我們還注意到此目錄下還有一個PYZ-00.pyz_extracted文件夾,里面都是引入的依賴庫,當然,我們自己寫的mylib.py也在其中,它也是我們反編譯的對象。
2. 反編譯pyc文件
找到了pyc文件,下面自然就是對它進行解密了。pyc其實是python程序執行過程中產生的緩存文件,我們直接運行python代碼時也會看到。對于這種格式的反編譯是比較簡單的,網上有許多工具,甚至還有很多在線工具。這里為了方便,我就采用了一款在線工具。附上鏈接:http://tools.bugscaner.com/decompyle/
但直接將我們找到的pyc文件上傳會發現無法反編譯。原因是什么呢?我們用十六進制編輯器(大家網上搜就行,我這里用的是wxMEdit)打開這個文件,與之前直接運行py文件生成的pyc文件比較。
我先來看一下main.pyc的區別,左邊是我們解包出來的,右邊是運行生成的。
左邊是我們解包出來的,右邊是運行生成的
發現唯一的差別就是少了第一行16個字節(叫做 magic number 表示python的版本和編譯時間),那我們把它加上是不是就能正常解析了呢?確實是這樣,但沒有原始pyc文件怎么辦?我們再到xx.exe_extracted文件夾里找一找。會發現有一個叫struct的文件,我們給他加上后綴.pyc反編譯試試。發現成功反編譯出如下內容:
struct反編譯結果
這就說明它的 magic number 是正確的,那我們只要把它的前16個字節復制過去不就行了?我們再來試試,成了!main.py中的內容被成功反編譯出來了。
main.pyc反編譯結果
下面同理也能反編譯出mylib.py等依賴庫中的內容,不過值得注意的是,網上很多教程都沒有提到依賴庫的pyc文件缺少的字節數與主程序的不同!
左:struct文件 | 中:解包出的mylib.pyc | 右:正確的pyc文件
我們發現它不是缺少了16個字節,而是中間少了4個字節!那么,我們只需要把struct頭部的16個字節覆蓋掉mylib.pyc的前12個字節。
改好之后再進行反編譯。
mylib.pyc反編譯內容
反編譯成功!不過中文字符被解析成了Unicode編碼,可以再使用相應工具轉換。
可以看到,通過pyinstaller打包的exe,還是能被較為容易地反編譯的。那么有加密打包的方法嗎?其實pyinstaller本身就是支持加密的,下面就來說一說如何加密打包。
三、使用pyinstaller加密打包exe
其實只要在打包時加個key參數就能加密
- pyinstaller.exe -F --key 123456 xxx.py
不過需要依賴pycrypto包,而python一般是不自帶這個包的。因此需要我們手動安裝。
1. 安裝pycrypto包
原本安裝過程應該很簡單,通過pip就能安裝。
- pip install pycrypto
不過安裝過程好像要調用VS編譯器編譯,這就造成了莫名其妙的問題,如果你在安裝過程中沒有報錯,那么恭喜你,你可以跳過這部分了。
我在網上找了很多解決方法都沒效,最后終于在StackOverflow上找到了一篇回答,完美解決了這個問題。原答案地址:https://stackoverflow.com/a/46921479/12954728
解決方法如下,前提是你電腦上安裝了Visual studio
以我的vs2015為例
(1)在開始菜單中找到VS文件夾,用管理員身份運行這個”兼容工具命令提示符“
(2)在你的VS安裝目錄下找到stdint.h這個文件,最好用everything搜索一下
(3)輸入set CL=-FI"你的路徑\stdint.h"設置環境變量
(4)然后再執行pip install pycrypto就能成功安裝了
2. 使用pyinstaller加密打包
現在執行如下命令就能加密打包了。key后面為密鑰可以隨便輸。
- pyinstaller.exe -F --key 123456 xxx.py
3. 反編譯測試
那么我們再來測試一下加密打包的exe還能不能被反編譯。
再次執行pyinstxtractor.py
- PS > python pyinstxtractor.py .\main-encrypt.exe
- import imp
- [*] Processing .\main-encrypt.exe
- [*] Pyinstaller version: 2.1+
- [*] Python version: 37
- [*] Length of package: 5787283 bytes
- [*] Found 63 files in CArchive
- [*] Beginning extraction...please standby
- [+] Possible entry point: pyiboot01_bootstrap
- [+] Possible entry point: main
- [*] Found 136 files in PYZ archive
- [!] Error: Failed to decompress Crypto, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress Crypto.Cipher, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress __future__, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress _compat_pickle, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress argparse, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress ast, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress base64, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress bdb, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress bisect, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress bz2, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress calendar, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress cmd, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress code, probably encrypted. Extracting as is.
- [!] Error: Failed to decompress codeop, probably encrypted. Extracting as is.
這次下面輸出了一長串Error,看來確實是被加密了。
我們再來看一看文件夾。
main-encrypt.exe_extracted文件夾里似乎沒什么變化,但PYZ-00.pyz_extracted文件夾里全是加密文件,應該是無法反編譯了。
不過對外層文件夾中的main文件進行同樣操作后依然是可以反編譯出源碼的。
看來這個加密只針對依賴庫。
四、總結
如果你不希望別人得到你的源碼,建議將你程序的入口函數寫在一個單獨的文件里,并采用加密方式打包exe。這樣的話,就算別人嘗試反編譯也只能得到你的入口函數。