1. 程式人生 > 其它 >將 exe 檔案反編譯成 Python 指令碼!

將 exe 檔案反編譯成 Python 指令碼!

最近遇到了一個問題,自己打包好的exe檔案還在,但是Python原始檔不知什麼時候被誤刪了。現在想改動一下功能,重寫Python指令碼工程量也太大了,怎麼辦?

今天我將教大家如何反編譯exe檔案,即將自己或別人寫好的exe,還原成Python原始碼。

以最近寫Python一鍵自動整理歸類檔案為例進行演示,執行所需的程式碼和檔案都會在文末提供給大家。

打包成單檔案所使用的命令為:

pyinstaller-Fw--icon=h.icoauto_organize_gui.py--add-data="h.ico;/"

打包成資料夾所使用的命令為:

pyinstaller-w--icon=h.icoauto_organize_gui.py--add-data="h.ico;."

不管是哪種打包方式都會留下一個exe檔案。

首先我們需要從exe檔案中抽取出其中的pyc檔案:

抽取exe中的pyc檔案

抽取pyinstaller打包的exe中的pyc檔案,提取pyc檔案有兩種方法:

  1. 通過 pyinstxtractor.py 指令碼提取pyc檔案
  2. 通過 pyi-archive_viewer 工具提取pyc檔案

指令碼提取pyc檔案

pyinstxtractor.py 指令碼可以在github專案 python-exe-unpacker 中下載,地址:

https://github.com/countercept/Python-exe-unpacker

下載該專案後把其中的pyinstxtractor.py指令碼檔案複製到與exe同級的目錄。

然後進入exe所在目錄的cmd執行:

Pythonpyinstxtractor.pyauto_organize_gui.exe

執行後便得到exe檔名加上_extracted字尾的資料夾:

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

工具提取pyc檔案

pyi-archive_viewer是PyInstaller自己提供的工具,它可以直接提取打包結果exe中的pyc檔案。

詳細介紹可參考官方文件:ttps://pyinstaller.readthedocs.io/en/stable/advanced-topics.html#using-pyi-archive-viewer

執行pyi-archive_viewer [filename]即可檢視 exe 內部的檔案結構:

pyi-archive_viewerauto_organize.exe

操作命令:

U:goUponelevel
O<name>:openembeddedarchivename
X<name>:extractname
Q:quit

然後可以提取出指定需要提取的檔案:

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

很顯然,使用PyInstaller的pyi-archive_viewer 工具操作起來比較麻煩,一次只能提取一個檔案,遇到子模組還需執行一次開啟操作。

所以後面我也只使用pyinstxtractor.py 指令碼來提取pyc檔案。

反編譯pyc檔案為py指令碼

有很多對pyc檔案進行解密的網站,例如:

  • https://tool.lu/pyc/

不過我們直接使用uncompyle6庫進行解碼,使用pip可以直接安裝:

pipinstalluncompyle6

uncompyle6可以反編譯.pyc字尾結尾的檔案,兩種命令形式:

  1. uncompyle6 xxx.pyc>xxx.py
  2. uncompyle6 -o xxx.py xxx.pyc

以前面編碼過程中生成的快取為例進行演示:

uncompyle6auto_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:

然後執行:

uncompyle6auto_organize_gui.exe_extracted/auto_organize_gui.pyc>auto_organize_gui.py

執行後可以看到檔案已經順利的被反編譯:

依賴性pyc檔案反編譯

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

先用UltraEdit32開啟檢視一下:

可以看到對於非入口執行的pyc檔案是從12位元組開始缺4個位元組。

這裡我們選擇第13個位元組再插入四個位元組即可:

然後再執行:

uncompyle6auto_organize_gui.exe_extracted/PYZ-00.pyz_extracted/auto_organize.pyc>auto_organize.py

然後成功的反編譯出依賴的檔案:

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

批量反編譯

如果一個exe需要被反編譯的QQ交易平臺地圖Python指令碼只有3個以內的檔案,我們都完全可以人工來操作。

但是假如一個exe涉及幾十個甚至上百個Python指令碼需要反編譯的時候,人工操作未免工作量過於巨大,我們考慮將以上過程用Python實現,從而達到批量反編譯的效果。

提取exe中的pyc

importos
importsys
importpyinstxtractor

exe_file=r"D:/PycharmProjects/gui_project/dist/auto_organize_gui.exe"
sys.argv=['pyinstxtractor',exe_file]
pyinstxtractor.main()
#恢復當前目錄位置
os.chdir("..")
[*]ProcessingD:/PycharmProjects/gui_project/dist/auto_organize_gui.exe
[*]Pyinstallerversion:2.1+
[*]Pythonversion:37
[*]Lengthofpackage:9491710bytes
[*]Found984filesinCArchive
[*]Beginningextraction...pleasestandby
[*]Found157filesinPYZarchive
[*]Successfullyextractedpyinstallerarchive:D:/PycharmProjects/gui_project/dist/auto_organize_gui.exe

YoucannowuseaPythondecompileronthepycfileswithintheextracteddirectory

預處理pyc檔案修護校驗頭

deffind_main(pyc_dir):
forpyc_fileinos.listdir(pyc_dir):
ifnotpyc_file.startswith("pyi-")andpyc_file.endswith("manifest"):
main_file=pyc_file.replace(".exe.manifest","")
result=f"{pyc_dir}/{main_file}"
ifos.path.exists(result):
returnmain_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"
forpyc_fileinos.listdir(pyz_dir):
ifpyc_file.endswith(".pyc"):
file=f"{pyz_dir}/{pyc_file}"
break
withopen(file,"rb")asf:
head=f.read(4)
list(map(hex,head))
['0x42','0xd','0xd','0xa']

校準入口類:

importshutil
ifos.path.exists("pycfile_tmp"):
shutil.rmtree("pycfile_tmp")
os.mkdir("pycfile_tmp")
main_file_result=f"pycfile_tmp/{main_file}.pyc"
withopen(f"{pyc_dir}/{main_file}","rb")asread,open(main_file_result,"wb")aswrite:
write.write(head)
write.write(b"\0"*12)
write.write(read.read())

校準子類:

pyz_dir=f"{pyc_dir}/PYZ-00.pyz_extracted"
forpyc_fileinos.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)
withopen(pyc_file_src,"rb")asread,open(pyc_file_dest,"wb")aswrite:
write.write(read.read(12))
write.write(b"\0"*4)
write.write(read.read())

開始反編譯

fromuncompyle6.binimportuncompile

ifnotos.path.exists("py_result"):
os.mkdir("py_result")
forpyc_fileinos.listdir("pycfile_tmp"):
sys.argv=['uncompyle6','-o',
f'py_result/{pyc_file[:-1]}',f'pycfile_tmp/{pyc_file}']
uncompile.main_bin()

完整程式碼下載見文末。

這樣我們只需將Python指令碼、exe檔案和pyinstxtractor.py指令碼檔案 放置到同一資料夾下,執行我們的Python指令碼。即可反編譯exe。

可以看到已經完美的反編譯出exe其中的Python指令碼:

好了,相信大家已經明白了反編譯的原理。那麼既然是攻防,如何防止自己打包的exe被反編譯呢?

如何防止exe被反編譯呢

只需在打包命令後面加上--key命令即可,例如文章開頭的命令可以更換為:

pyinstaller-Fw--icon=h.icoauto_organize_gui.py--add-data="h.ico;/"--key123456

123456是你用來加密的金鑰,可以隨意更換。

該加密引數依賴tinyaes,可以通過以下命令安裝:

pipinstalltinyaes

打包後再次執行反編譯:

exe_file=r"D:/PycharmProjects/gui_project/dist/auto_organize_gui.exe"
uncompyle_exe(exe_file,True)

結果只有入口指令碼反編譯成功,被依賴的指令碼均被加密,無法直接被反編譯:

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

可以看到,常規手段就無法直接反編譯了。

這個時候還想反編譯就需要底層的逆向分析研究了,或者pyinstaller的原始碼完整研究一遍,瞭解其加密處理的機制,看看有沒有破解的可能。