大家好,我是小小明,今天我將教大家如何反編譯exe檔案,
這次以最近寫的一篇gui《Python一鍵自動整理歸類檔案,GUI視窗程式拿來即用》為例進行演示,
地址:https://blog.csdn.net/as604049322/article/details/119619221
打包成單檔案所使用的命令為:
pyinstaller -Fw --icon=h.ico auto_organize_gui.py --add-data="h.ico;/"
打包成檔案夾所使用的命令為:
pyinstaller -w --icon=h.ico auto_organize_gui.py --add-data="h.ico;."
不管是哪種打包方式都會留下一個exe檔案,
文章目錄
- 🏆抽取pyinstaller打包的exe中的pyc檔案
- 🛬pyinstxtractor.py 腳本提取pyc檔案
- 🛬pyi-archive_viewer 工具提取pyc檔案
- 🏆反編譯pyc檔案為py腳本
- 🛬運行入口pyc檔案反編譯
- 🛬依賴性pyc檔案反編譯
- 🏆批量反編譯一個exe中的所有python腳本
- 🛬提取exe中的pyc
- 🛬預處理pyc檔案修護校驗頭
- 🛬開始反編譯
- 🛬完整代碼
- 🏆如何防止自己打包的exe被反編譯呢?
首先我們需要從exe檔案中抽取出其中的pyc檔案:
🏆抽取pyinstaller打包的exe中的pyc檔案
提取pyc檔案有兩種方法:
- 通過 pyinstxtractor.py 腳本提取pyc檔案
- 通過 pyi-archive_viewer 工具提取pyc檔案
🛬pyinstxtractor.py 腳本提取pyc檔案
pyinstxtractor.py 腳本可以在github專案python-exe-unpacker中下載,地址:
https://github.com/countercept/python-exe-unpacker
下載該專案后把其中的pyinstxtractor.py腳本檔案復制到與exe同級的目錄,
然后進入exe所在目錄的cmd執行:
python pyinstxtractor.py auto_organize_gui.exe
執行后便得到exe檔案名加上_extracted后綴的檔案夾:

對兩種打包方式產生的exe提取出的檔案結構稍有區別:

🛬pyi-archive_viewer 工具提取pyc檔案
pyi-archive_viewer是PyInstaller自己提供的工具,它可以直接提取打包結果exe中的pyc檔案,
詳細介紹可參考官方檔案:
https://pyinstaller.readthedocs.io/en/stable/advanced-topics.html#using-pyi-archive-viewer
執行pyi-archive_viewer [filename]即可查看 exe 內部的檔案結構:
pyi-archive_viewer auto_organize.exe
操作命令:
U: go Up one level
O <name>: open embedded archive name
X <name>: extract name
Q: quit
然后可以提取出指定需要提取的檔案:

要提取其他被匯入的pyc檔案,則需要先打開PYZ-00.pyz:

很顯然,使用PyInstaller的pyi-archive_viewer 工具操作起來比較麻煩,一次只能提取一個檔案,遇到子模塊還需執行一次打開操作,所以后面我也只使用pyinstxtractor.py 腳本來提取pyc檔案,
🏆反編譯pyc檔案為py腳本
有很多對pyc檔案進行解密的網站,例如:
- https://tool.lu/pyc/
不過我們直接使用 uncompyle6 庫進行解碼,使用pip可以直接安裝:
pip install uncompyle6
uncompyle6可以反編譯.pyc后綴結尾的檔案,兩種命令形式:
uncompyle6 xxx.pyc>xxx.pyuncompyle6 -o xxx.py xxx.pyc
以前面編碼程序中生成的快取為例進行演示:
uncompyle6 auto_organize.cpython-37.pyc>auto_organize.py
執行后便直接將.pyc檔案反編譯成python腳本了:

從編譯結果看注釋也被保留了下來:

對于不是pyc后綴結尾的檔案,使用uncompyle6反編譯時會報出must point to a Python source that can be compiled, or Python bytecode (.pyc, .pyo)的錯誤,
所以我們需要先對提取出的內容人工修改后綴:

🛬運行入口pyc檔案反編譯
對于從pyinstaller提取出來的pyc檔案并不能直接反編譯,入口運行類共16位元組的 magic 和 時間戳被去掉了,
如果直接進行反編譯,例如執行uncompyle6 auto_organize_gui.exe_extracted/auto_organize_gui.pyc
會報出如下錯誤:ImportError: Unknown magic number 227 in auto_organize_gui.exe_extracted\auto_organize_gui.pyc
使用支持16進制編輯的文本編輯器查看一探究竟,這里我使用UltraEdit32:
分別打開正常情況下編譯出的pyc和從pyinstaller提取出來的pyc檔案進行對比:

可以看到前16個位元組都被去掉了,其中前四個位元組是magic,這四個位元組會隨著系統和python版本發生變化,必須一致,后四個位元組包括時間戳和一些其他的資訊,都可以隨意填寫,
我們先通過UltraEdit32向pyinstaller提取的檔案添加頭資訊:

選擇開頭插入16個位元組后,只需要替換前4個位元組為當前環境下的magic:

然后執行:
uncompyle6 auto_organize_gui.exe_extracted/auto_organize_gui.pyc>auto_organize_gui.py
執行后可以看到檔案已經順利的被反編譯:

🛬依賴性pyc檔案反編譯
考慮再反編譯匯入的其他依賴檔案:

先用UltraEdit32打開查看一下:

可以看到對于非入口運行的pyc檔案是從12位元組開始缺4個位元組,
這里我們選擇第13個位元組再插入四個位元組即可:

然后再執行:
uncompyle6 auto_organize_gui.exe_extracted/PYZ-00.pyz_extracted/auto_organize.pyc > auto_organize.py
然后成功的反編譯出依賴的檔案:

代碼與原檔案幾乎完全一致:

🏆批量反編譯一個exe中的所有python腳本
如果一個exe需要被反編譯的python腳本只有3個以內的檔案,我們都完全可以人工來操作,但是假如一個exe涉及幾十個甚至上百個python腳本需要反編譯的時候,人工操作未免作業量過于巨大,我們考慮將以上程序用python實作,從而達到批量反編譯的效果,
🛬提取exe中的pyc
import os
import sys
import pyinstxtractor
exe_file = r"D:/PycharmProjects/gui_project/dist/auto_organize_gui.exe"
sys.argv = ['pyinstxtractor', exe_file]
pyinstxtractor.main()
# 恢復當前目錄位置
os.chdir("..")
[*] Processing D:/PycharmProjects/gui_project/dist/auto_organize_gui.exe
[*] Pyinstaller version: 2.1+
[*] Python version: 37
[*] Length of package: 9491710 bytes
[*] Found 984 files in CArchive
[*] Beginning extraction...please standby
[*] Found 157 files in PYZ archive
[*] Successfully extracted pyinstaller archive: D:/PycharmProjects/gui_project/dist/auto_organize_gui.exe
You can now use a python decompiler on the pyc files within the extracted directory
🛬預處理pyc檔案修護校驗頭
def find_main(pyc_dir):
for pyc_file in os.listdir(pyc_dir):
if not pyc_file.startswith("pyi-") and pyc_file.endswith("manifest"):
main_file = pyc_file.replace(".exe.manifest", "")
result = f"{pyc_dir}/{main_file}"
if os.path.exists(result):
return main_file
pyc_dir = os.path.basename(exe_file)+"_extracted"
main_file = find_main(pyc_dir)
main_file
讀取從pyz目錄抽取的pyc檔案的前4個位元組作基準:
pyz_dir = f"{pyc_dir}/PYZ-00.pyz_extracted"
for pyc_file in os.listdir(pyz_dir):
if pyc_file.endswith(".pyc"):
file = f"{pyz_dir}/{pyc_file}"
break
with open(file, "rb") as f:
head = f.read(4)
list(map(hex, head))
['0x42', '0xd', '0xd', '0xa']
校準入口類:
import shutil
if os.path.exists("pycfile_tmp"):
shutil.rmtree("pycfile_tmp")
os.mkdir("pycfile_tmp")
main_file_result = f"pycfile_tmp/{main_file}.pyc"
with open(f"{pyc_dir}/{main_file}", "rb") as read, open(main_file_result, "wb") as write:
write.write(head)
write.write(b"\0"*12)
write.write(read.read())
校準子類:
pyz_dir = f"{pyc_dir}/PYZ-00.pyz_extracted"
for pyc_file in os.listdir(pyz_dir):
pyc_file_src = f"{pyz_dir}/{pyc_file}"
pyc_file_dest = f"pycfile_tmp/{pyc_file}"
print(pyc_file_src, pyc_file_dest)
with open(pyc_file_src, "rb") as read, open(pyc_file_dest, "wb") as write:
write.write(read.read(12))
write.write(b"\0"*4)
write.write(read.read())
🛬開始反編譯
from uncompyle6.bin import uncompile
if not os.path.exists("py_result"):
os.mkdir("py_result")
for pyc_file in os.listdir("pycfile_tmp"):
sys.argv = ['uncompyle6', '-o',
f'py_result/{pyc_file[:-1]}', f'pycfile_tmp/{pyc_file}']
uncompile.main_bin()

🛬完整代碼
#!/usr/bin/env python
# coding: utf-8
# 提取exe中的pyc
import os
import sys
import pyinstxtractor
from uncompyle6.bin import uncompile
import shutil
# 預處理pyc檔案修護校驗頭
def find_main(pyc_dir):
for pyc_file in os.listdir(pyc_dir):
if not pyc_file.startswith("pyi-") and pyc_file.endswith("manifest"):
main_file = pyc_file.replace(".exe.manifest", "")
result = f"{pyc_dir}/{main_file}"
if os.path.exists(result):
return main_file
def uncompyle_exe(exe_file, complie_child=False):
sys.argv = ['pyinstxtractor', exe_file]
pyinstxtractor.main()
# 恢復當前目錄位置
os.chdir("..")
pyc_dir = os.path.basename(exe_file)+"_extracted"
main_file = find_main(pyc_dir)
pyz_dir = f"{pyc_dir}/PYZ-00.pyz_extracted"
for pyc_file in os.listdir(pyz_dir):
if pyc_file.endswith(".pyc"):
file = f"{pyz_dir}/{pyc_file}"
break
else:
print("子檔案中沒有找到pyc檔案,無法反編譯!")
return
with open(file, "rb") as f:
head = f.read(4)
if os.path.exists("pycfile_tmp"):
shutil.rmtree("pycfile_tmp")
os.mkdir("pycfile_tmp")
main_file_result = f"pycfile_tmp/{main_file}.pyc"
with open(f"{pyc_dir}/{main_file}", "rb") as read, open(main_file_result, "wb") as write:
write.write(head)
write.write(b"\0"*12)
write.write(read.read())
if os.path.exists("py_result"):
shutil.rmtree("py_result")
os.mkdir("py_result")
sys.argv = ['uncompyle6', '-o',
f'py_result/{main_file}.py', main_file_result]
uncompile.main_bin()
if not complie_child:
return
for pyc_file in os.listdir(pyz_dir):
if not pyc_file.endswith(".pyc"):
continue
pyc_file_src = f"{pyz_dir}/{pyc_file}"
pyc_file_dest = f"pycfile_tmp/{pyc_file}"
print(pyc_file_src, pyc_file_dest)
with open(pyc_file_src, "rb") as read, open(pyc_file_dest, "wb") as write:
write.write(read.read(12))
write.write(b"\0"*4)
write.write(read.read())
os.mkdir("py_result/other")
for pyc_file in os.listdir("pycfile_tmp"):
if pyc_file==main_file+".pyc":
continue
sys.argv = ['uncompyle6', '-o',
f'py_result/other/{pyc_file[:-1]}', f'pycfile_tmp/{pyc_file}']
uncompile.main_bin()
呼叫:
exe_file = r"D:/PycharmProjects/gui_project/dist/auto_organize_gui.exe"
uncompyle_exe(exe_file, True)
可以看到已經完美的反編譯出exe其中的python腳本:

🏆如何防止自己打包的exe被反編譯呢?
只需在打包命令后面加上--key命令即可,例如文章開頭的命令可以更換為:
pyinstaller -Fw --icon=h.ico auto_organize_gui.py --add-data="h.ico;/" --key 123456
123456是你用來加密的密鑰,可以隨意更換,
該加密引數依賴tinyaes,可以通過以下命令安裝:
pip install tinyaes
打包后再次執行反編譯:
exe_file = r"D:/PycharmProjects/gui_project/dist/auto_organize_gui.exe"
uncompyle_exe(exe_file, True)
結果只有入口腳本反編譯成功,被依賴的腳本均被加密,無法直接被反編譯:

可以看到抽取的中間結果變成了.pyc.encrypted格式,無法直接被反編譯:

可以看到,常規手段就無法直接反編譯了,這個時候還想反編譯就需要底層的逆向分析研究了,或者pyinstaller的原始碼完整研究一遍,了解其加密處理的機制,看看有沒有破解的可能,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/295660.html
標籤:python
