別再問我exe反編譯成Python腳本了!

今年年初的時候,我發(fā)了一篇:別再問我Python打包成exe了!(終極版),相信解決了不少小伙伴的Pyinstaller打包問題。
不過我最近遇到了一個問題,自己打包好的exe文件還在,但是Python源文件不知什么時候被誤刪了。現(xiàn)在想改動一下功能,重寫Python腳本工程量也太大了,怎么辦?
請看下文:
以最近寫Python一鍵自動整理歸類文件為例進行演示,運行所需的代碼和文件都會在文末提供給大家。

打包成單文件所使用的命令為:
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文件。
首先我們需要從exe文件中抽取出其中的pyc文件:
抽取exe中的pyc文件
抽取pyinstaller打包的exe中的pyc文件,提取pyc文件有兩種方法:
通過 pyinstxtractor.py 腳本提取pyc文件 通過 pyi-archive_viewer 工具提取pyc文件
腳本提取pyc文件
pyinstxtractor.py 腳本可以在github項目 python-exe-unpacker 中下載,地址:
https://github.com/countercept/Python-exe-unpacker
下載該項目后把其中的pyinstxtractor.py腳本文件復(fù)制到與exe同級的目錄。
然后進入exe所在目錄的cmd執(zhí)行:
Python?pyinstxtractor.py?auto_organize_gui.exe
執(zhí)行后便得到exe文件名加上_extracted后綴的文件夾:

對兩種打包方式產(chǎn)生的exe提取出的文件結(jié)構(gòu)稍有區(qū)別:

工具提取pyc文件
pyi-archive_viewer是PyInstaller自己提供的工具,它可以直接提取打包結(jié)果exe中的pyc文件。
詳細介紹可參考官方文檔:ttps://pyinstaller.readthedocs.io/en/stable/advanced-topics.html#using-pyi-archive-viewer
執(zhí)行pyi-archive_viewer [filename]即可查看 exe 內(nèi)部的文件結(jié)構(gòu):
pyi-archive_viewer?auto_organize.exe
操作命令:
U:?go?Up?one?level
O?:?open?embedded?archive?name
X?:?extract?name
Q:?quit
然后可以提取出指定需要提取的文件:

要提取其他被導(dǎo)入的pyc文件,則需要先打開PYZ-00.pyz:

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

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

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

運行入口pyc文件反編譯
對于從pyinstaller提取出來的pyc文件并不能直接反編譯,入口運行類共16字節(jié)的 magic 和 時間戳被去掉了。
如果直接進行反編譯,例如執(zhí)行 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個字節(jié)都被去掉了,其中前四個字節(jié)是magic,這四個字節(jié)會隨著系統(tǒng)和Python版本發(fā)生變化,必須一致。后四個字節(jié)包括時間戳和一些其他的信息,都可以隨意填寫。
我們先通過UltraEdit32向pyinstaller提取的文件添加頭信息:

選擇開頭插入16個字節(jié)后,只需要替換前4個字節(jié)為當(dāng)前環(huán)境下的magic:

然后執(zhí)行:
uncompyle6?auto_organize_gui.exe_extracted/auto_organize_gui.pyc>auto_organize_gui.py
執(zhí)行后可以看到文件已經(jīng)順利的被反編譯:

依賴性pyc文件反編譯
考慮再反編譯導(dǎo)入的其他依賴文件:

先用UltraEdit32打開查看一下:

可以看到對于非入口運行的pyc文件是從12字節(jié)開始缺4個字節(jié)。
這里我們選擇第13個字節(jié)再插入四個字節(jié)即可:

然后再執(zhí)行:
uncompyle6?auto_organize_gui.exe_extracted/PYZ-00.pyz_extracted/auto_organize.pyc?>?auto_organize.py
然后成功的反編譯出依賴的文件:

代碼與原文件幾乎完全一致:

批量反編譯
如果一個exe需要被反編譯的Python腳本只有3個以內(nèi)的文件,我們都完全可以人工來操作。
但是假如一個exe涉及幾十個甚至上百個Python腳本需要反編譯的時候,人工操作未免工作量過于巨大,我們考慮將以上過程用Python實現(xiàn),從而達到批量反編譯的效果。
提取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()
#?恢復(fù)當(dāng)前目錄位置
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
預(yù)處理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個字節(jié)作基準:
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()

完整代碼下載見文末。
這樣我們只需將Python腳本、exe文件和pyinstxtractor.py腳本文件 放置到同一文件夾下,運行我們的Python腳本。即可反編譯exe。
可以看到已經(jīng)完美的反編譯出exe其中的Python腳本:

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

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

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

可以看到,常規(guī)手段就無法直接反編譯了。
這個時候還想反編譯就需要底層的逆向分析研究了,或者pyinstaller的源碼完整研究一遍,了解其加密處理的機制,看看有沒有破解的可能。
下載地址
如果大家想測試exe反編譯,手頭又沒有合適的文件,可以在后臺回復(fù)“反編譯”即可獲得。
5、因程序員開發(fā)速度太慢,公司索賠90萬敗訴,后續(xù)來了

