Python3 如何反编译EXE
Posted xuxingzhuang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python3 如何反编译EXE相关的知识,希望对你有一定的参考价值。
1. 需求分析
只支持通过py2exe和pyinstaller 工具编译生成的EXE文件
公司内部使用Python编写的代码,最终需要在发布前编译成windows执行的.EXE文件,所以今天在网上看到有相关牛人,github开源写了一个反编译代码程序,可以将Windows EXE文件反编译处pyc文件,最终再将pyc文件转换成可以编译查看的py文件,觉得比较牛,今天测试一下,看看效果如何,已经整个操作步骤是怎样的,做一个留存。
2. 环境描述
两个测试使用环境来完成反编译:
本地主测试是Mac,Python版本3.7.5
一台Windows10,主要用来安装16进制编译器,方便我们来可视化分析,目前这个16进制编译器010Editor 只能安装到windows系统下,所以使用了windows10系统,主要的作用是在这。
3. 步骤分解
主要的步骤分为以下几个步骤来完成整个反编译过程。
这里再确认下我们的最终需求,将EXE可执行文件反编译成最终可视、可编辑的py结尾的Python代码文件
有了目标之后,那我们整理具体的操作步骤,并进行细化分解
- 获取可执行的EXE文件
- 下载反编译程序包
- 分析EXE可执行文件的打包工具(是否为py2exe和pyinstaller)
- 执行解包操作
- 执行反编译操作
4. 操作步骤
4.1. 获取可执行EXE文件
我这里准备的EXE可执行文件是通过pyinstaller打包工具完成的编译,软件包名称如下:
4.2. 下载反编译程序包
github下载连接地址:
countercept/python-exe-unpacker
# git clone https://github.com/countercept/python-exe-unpacker.git
(venv_3.7.5) CarltonXu@CarltonXus-MacBook-Pro # cd python-exe-unpacker
(venv_3.7.5) CarltonXu@CarltonXus-MacBook-Pro # ls -tlr
total 160
-rw-r--r-- 1 CarltonXu wheel 35096 Apr 7 20:41 LICENSE
-rw-r--r-- 1 CarltonXu wheel 4110 Apr 7 20:41 README.md
-rw-r--r-- 1 CarltonXu wheel 12392 Apr 7 20:41 pyinstxtractor.py
-rw-r--r-- 1 CarltonXu wheel 15377 Apr 7 20:41 python_exe_unpack.py
-rw-r--r-- 1 CarltonXu wheel 97 Apr 7 20:41 requirements.txt
drwxr-xr-x 3 CarltonXu wheel 96 Apr 7 20:42 __pycache__
4.2.1. 安装依赖包
在代码下载后,需要安装运行代码所需要的依赖包,执行下面指令即可完成安装
(venv_3.7.5) CarltonXu@CarltonXus-MacBook-Pro # sudo pip3 install -r requirements.txt
Password:
WARNING: The directory '/Users/CarltonXu/Library/Caches/pip' or its parent directory is not owned or is not writable by the current user. The cache has been disabled. Check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
Looking in indexes: http://pypi.douban.com/simple/
Requirement already satisfied: pefile==2017.9.3 in /Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages (from -r requirements.txt (line 1)) (2017.9.3)
Requirement already satisfied: unpy2exe==0.3 in /Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages (from -r requirements.txt (line 2)) (0.3)
Collecting uncompyle6==2.11.5
Downloading http://pypi.doubanio.com/packages/35/a5/f0b734adba414239e144007904207b2fa2ce3ac0b4c87f4a7b0edcf74c0b/uncompyle6-2.11.5.tar.gz (1.4 MB)
|████████████████████████████████| 1.4 MB 2.2 MB/s
Requirement already satisfied: xdis==3.5.5 in /Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages (from -r requirements.txt (line 4)) (3.5.5)
Requirement already satisfied: pycrypto==2.6.1 in /Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages (from -r requirements.txt (line 5)) (2.6.1)
Requirement already satisfied: configparser==3.5.0 in /Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages (from -r requirements.txt (line 6)) (3.5.0)
Requirement already satisfied: future in /Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages (from pefile==2017.9.3->-r requirements.txt (line 1)) (0.18.2)
Collecting spark-parser<1.7.0,>=1.6.1
Downloading http://pypi.doubanio.com/packages/f3/4e/a95a1ff543744bfaa33449b301fe74272556db2e852c5c3852517a5024be/spark_parser-1.6.1-py3-none-any.whl (17 kB)
Requirement already satisfied: six in /Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages (from uncompyle6==2.11.5->-r requirements.txt (line 3)) (1.13.0)
Collecting argparse
Downloading http://pypi.doubanio.com/packages/f2/94/3af39d34be01a24a6e65433d19e107099374224905f1e0cc6bbe1fd22a2f/argparse-1.4.0-py2.py3-none-any.whl (23 kB)
Requirement already satisfied: click in /Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages (from spark-parser<1.7.0,>=1.6.1->uncompyle6==2.11.5->-r requirements.txt (line 3)) (7.0)
Using legacy 'setup.py install' for uncompyle6, since package 'wheel' is not installed.
Installing collected packages: spark-parser, argparse, uncompyle6
Attempting uninstall: spark-parser
Found existing installation: spark-parser 1.8.9
Uninstalling spark-parser-1.8.9:
Successfully uninstalled spark-parser-1.8.9
Attempting uninstall: uncompyle6
Found existing installation: uncompyle6 3.7.4
Uninstalling uncompyle6-3.7.4:
Successfully uninstalled uncompyle6-3.7.4
Running setup.py install for uncompyle6 ... done
Successfully installed argparse-1.4.0 spark-parser-1.6.1 uncompyle6-2.11.5
4.3. 分析EXE文件属性
这里分析EXE文件属性其实就是看EXE是否为py2exe及pyinstaller工具打包出来的,其实这个动作在执行解包动作的时候会进行precheck动作,如果检测失败会终止并提示,我们看下代码里面怎么做检测的。
文件路径:python-exe-unpacker/pyinstxtractor.py
检测的逻辑,如果是通过pyinstaller打包的,会在EXE文件的添加一个Magic number,这个Magic number就是 b"MEI\\014\\013\\012\\013\\016" 划算成16进制就是 “4d45490c0b0a0b0e”
# 转换成16进制
In [30]: MAGIC = b'MEI\\014\\013\\012\\013\\016'
In [31]: MAGIC.encode('hex')
Out[31]: '4d45490c0b0a0b0e'
检测公式:(文件大小 - PYINSTALL COOKIE SIZE)字节之后的 8个字节 是否为b"MEI\\014\\013\\012\\013\\016"
下面我们再通过010Edit工具打开EXE文件,找到最后位置的8个字节看看16进制显示的值
最后我们看到代码解析EXE文件和通过010Editor工具解析,EXE文件是属于pyinstaller进行打包的,那我们就可以进行后续操作了
4.4. 执行解包操作
解包操作通过下载的python-exe-unpacker说明,执行以下指令即可。
# (venv_3.7.5) ✘ CarltonXu@CarltonXus-MacBook-Pro # python python_exe_unpack.py -i /Users/CarltonXu/Downloads/SMS-Agent.exe
[*] On Python 3.7
[*] Processing /Users/CarltonXu/Downloads/SMS-Agent.exe
[*] Pyinstaller version: 2.1+
[*] This exe is packed using pyinstaller
[*] Unpacking the binary now
[*] Python version: 37
[*] Length of package: 17411420 bytes
[*] Found 1631 files in CArchive
[*] Beginning extraction...please standby
[*] Found 754 files in PYZ archive
[*] Successfully extracted pyinstaller exe.
最终执行完成,看到成功后,如果没有-o指定输出目录的话,默认会在当前目录输出unpacked/SMS-agent.exe/目录,此目录下便是解包后的代码,有一堆的文件和一个目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WR0W2AsA-1621560980704)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd4df5c8-7284-4e7d-9ff0-6b434304df30/Untitled.png)]
下面输出的目录也就是源代码目录,但是目录下面全是pyc文件,我们还注意到此目录下还有一个PYZ-00.pyz_extracted文件夹,里面都是引入的依赖库,当然程序的源代码在下这个下面,当然也是我们需要反编译的对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RV18UBLB-1621560980706)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/606c4a4e-d6d8-4c7b-9452-62059a153acd/Untitled.png)]
4.4. 执行反编译操作
前边我们看到找到了pyc文件,下面自然就是对它进行解密了。pyc其实是python程序执行过程中产生的缓存文件,我们直接运行python代码时也会看到。对于这种格式的反编译是比较简单的,网上有许多工具,甚至还有很多在线工具及开源代码,我们也可以使用最长用的uncompyle6工具来恢复py文件,操作试试。
# 现将解压主目录下的sms_agent主程序,后缀修改为sms_agent.pyc
(venv_3.7.5) ✘ CarltonXu@CarltonXus-MacBook-Pro [ /tmp ] # cp /tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent /tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent.pyc
# 执行反编译指令
(venv_3.7.5) ✘ CarltonXu@CarltonXus-MacBook-Pro [ /tmp ] # uncompyle6 /tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent.pyc > /tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent.py
Traceback (most recent call last):
File "/Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages/xdis/load.py", line 197, in load_module_from_file_object
float_version = magic_int2float(magic_int)
File "/Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages/xdis/magics.py", line 426, in magic_int2float
return py_str2float(magicint2version[magic_int])
KeyError: 227
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/CarltonXu/workspace/venv_3.7.5/bin/uncompyle6", line 8, in <module>
sys.exit(main_bin())
File "/Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages/uncompyle6/bin/uncompile.py", line 194, in main_bin
**options)
File "/Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages/uncompyle6/main.py", line 324, in main
do_fragments,
File "/Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages/uncompyle6/main.py", line 184, in decompile_file
filename, code_objects
File "/Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages/xdis/load.py", line 170, in load_module
get_code=get_code,
File "/Users/CarltonXu/workspace/venv_3.7.5/lib/python3.7/site-packages/xdis/load.py", line 205, in load_module_from_file_object
% (ord(magic[0:1]) + 256 * ord(magic[1:2]), filename)
ImportError: Unknown magic number 227 in /tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent.pyc
我们执行上面的代码将pyc转换成py,发现失败了,提示是Unknown magic number 227,这个失败因为pyinstaller工具打包的时候,会将代码文件的magic number(python的版本及编译时间)给清除掉,所以反编译时候需要将magic number添加回去才能识别,magic number我们可以通过解压主目录下的struct结构体文件中提取出来(一般是前16个字节,可以对比打包前的源文件),将struct文件体中的前16个字节提取出来,然后在添加到文件中,然后再执行uncompyle6反编译试试。那struct前16个字节值如何获取,我们可以有两种方式获取
4.4.1. 获取struct文件结构体magic number
- 第一种:通过010Editor获取
对比两个文件,获取struct的前16个字节内容
第一张图struct结构体文件E3之前的16个字节便是magic number
第二张图sms_agent主程序文件E3之前为空,所以缺少了16个字节magic number
第一张图黄色框里面的内容都是16进制的值
第一张图:struct
第二张图:sms_agent
- 第二种:通过python获取
In [1]: struct_path = "/tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/struct"
In [2]: fn = open(struct_path, 'rb') # 以2进制读模式打开文件
In [3]: fn.seek(0) # 切换指针位置到开头
Out[3]: 0
In [5]: fn.tell() # 查看当前指针位置
Out[5]: 0
In [8]: magic_number = fn.read(16) # 读取文件的前16个字节,从0开始
In [10]: magic_number
Out[10]: b'B\\r\\r\\n\\x00\\x00\\x00\\x00pyi0\\x10\\x01\\x00\\x00' # 因为我们是2进值打开的文件,这里输出的是前16个字节的2进制值
In [11]: magic_number.hex()
Out[11]: '420d0d0a000000007079693010010000' # 转换成16进制,是不是和010Editor文件打开的是一致的
In [12]: fn.tell() # 获取当前指针位置
Out[12]: 16
In [13]: fn.read(1) # 再读取第17个字节,输出值是e3
Out[13]: b'\\xe3'
In [14]: fn.read(1).hex() # 再读取第17个字节,输出值是e3
Out[14]: 'e3'
这时候,我们两种方式都可以获取struct前16个字节的magic number值,我们记录下来,添加到pyc文件中,magic number 2进制值:b’B\\r\\r\\n\\x00\\x00\\x00\\x00pyi0\\x10\\x01\\x00\\x00’
4.4.2. 将Magic number添加到pyc文件
In [1]: struct_path = "/tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent"
In [2]: add_magic_num_file_name = "/tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent"
In [3]: MAGIC_NUMBER = b'B\\r\\r\\n\\x00\\x00\\x00\\x00pyi0\\x10\\x01\\x00\\x00'
In [4]: f = open(add_magic_num_file_name, 'rb')
In [5]: f.seek(0)
Out[5]: 0
In [6]: f.tell()
Out[6]: 0
In [7]: new_content = f.read()
In [8]: new_add_magic_number_file_name = "/tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent.pyc"
In [9]: n_f = open(new_add_magic_number_file_name, "wb")
In [10]: n_f.seek(0)
Out[10]: 0
In [11]: n_f.tell()
Out[11]: 0
In [12]: n_f.write(MAGIC_NUMBER + new_content)
Out[12]: 10972
In [13]: f.close()
In [14]: n_f.close()
4.4.3. 反编译新添加magic number的pyc文件
(venv_3.7.5) CarltonXu@CarltonXus-MacBook-Pro [ /tmp ] # python /Users/CarltonXu/workspace/venv_3.7.5/bin/uncompyle6 /tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent.pyc > /tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/sms_agent.py
执行反编译指令没有出错,证明一切正常,查看新生成的sms_agent.py,代码一切正常。
通过反编译出来的py文件,我们看到已经可以正常看到代码了,只不过有些中文字符被解析成了Unicode编码,可以再使用相应工具转换即可。这个不影响正常查看,我们反编译的sms_agent是主目录的主程序文件,其实还有最重要的,我们需要将PYZ-00.pyz_extracted文件夹下的所有pyc文件都进行反编译。
这里需要注意下PYZ-00.pyz_extracted目录下的依赖库的pyc文件缺少的字节数与主程序不同,通过010Editor查看,依赖库下面的pyc文件不是缺少了16个字节,而是中间少了4个字节,那么,我们只需要把struct头部的16个字节覆盖掉PYZ-00.pyz_extracted目录下的依赖库的pyc的前12个字节。
4.4.5. 反编译PYZ-00.pyz_extracted依赖库下的pyc文件
由于依赖库下的文件较多,这里我写了一个脚本来自动完成转换,仅供参
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2019 OneProCloud (Shanghai) Ltd
#
# Authors: XuXingZhuang xuxingzhuang@oneprocloud.com
#
# Copyright (c) 2019. This file is confidential and proprietary.
# All Rights Reserved, OneProCloud (Shanghai) Ltd(http://www.oneprocloud.com).)
#
import os
MAGIC_HEAD = b'B\\r\\r\\n\\x00\\x00\\x00\\x00pyi0\\x10\\x01\\x00\\x00'
FILES_DIRS = "/Users/CarltonXu/Downloads/SMS-Agent-unpacked/SMS-Agent.exe/PYZ-00.pyz_extracted/"
CONVERT_CMD = "/Users/CarltonXu/workspace/venv_3.7.5/bin/python /Users/CarltonXu/workspace/venv_3.7.5/bin/uncompyle6"
num = 0
for root, dirs, files in os.walk(FILES_DIRS):
print(files)
for f_name in files:
num += 1
print("Execute Number: %s" %num)
if not f_name.endswith(".pyc"):
continue
old_file = os.path.join(root, f_name)
new_file = os.path.join(root + "/pyz_workspace/" + f_name)
with open(old_file, "rb") as o_f:
o_f.seek(12)
od_content = o_f.read()
with open(new_file, "wb") as n_f:
n_f.seek(0)
new_content = MAGIC_HEAD + od_content
n_f.write(new_content)
GENERA_PY_FILE = os.path.join(root + "/pyz_workspace/" + f_name[0:-4] + ".py")
EXEC_CONVERT_CMD = CONVERT_CMD + " %s > %s" %(new_file, GENERA_PY_FILE)
exec_ret = os.popen(EXEC_CONVERT_CMD).read()
if exec_ret:
print("INFO: Execute convert cmd successful.")
else:
print("[ERROR]: Execute convert cmd filed, please check it.")
4.4.6. 整理PYZ-00.pyz_extracted依赖库下的py文件
由于反编译完成后的py文件较多,而且没有相关目录,所有的文件名称都是xxx.xxxx.xxx.py
我们正常的代码编写会进行目录划分,用于不同的功能,以及程序调用,所以这里写了一个脚本,来将py文件进行归类创建目录,后续好进行查看
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2019 OneProCloud (Shanghai) Ltd
#
# Authors: XuXingZhuang xuxingzhuang@oneprocloud.com
#
# Copyright (c) 2019. This file is confidential and proprietary.
# All Rights Reserved, OneProCloud (Shanghai) Ltd(http://www.oneprocloud.com).)
#!/usr/bin/env python
# coding=utf-8
#
import os
import shutil
ORDER_DIRS = "/Users/CarltonXu/Downloads/SMS-Agent-unpacked/SMS-Agent.exe/PYZ-00.pyz_extracted/pyz_workspace/"
# 遍历所有目录下的文件
for f in os.listdir(ORDER_DIRS):
if f.split(".")[-1].endswith("pyc"):
continue
mk_dir = f.split(".")[0:-2]
if mk_dir:
mk_path = ORDER_DIRS + "/".join(mk_dir)
mv_file = ORDER_DIRS + f
mv_to_path = mk_path + "/"
new_file_name = ".".join(f.split(".")[-2:])
os.makedirs(mk_path, exist_ok=True)
shutil.move(mv_file, mv_to_path)
os.renames(mv_to_path + f, mv_to_path + new_file_name)
else:
continue
4.4.7. 整体脚本
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2019 OneProCloud (Shanghai) Ltd
#
# Authors: XuXingZhuang xuxingzhuang@oneprocloud.com
#
# Copyright (c) 2019. This file is confidential and proprietary.
# All Rights Reserved, OneProCloud (Shanghai) Ltd(http://www.oneprocloud.com).)
#
import os
import shutil
MAGIC_HEAD = b'B\\r\\r\\n\\x00\\x00\\x00\\x00pyi0\\x10\\x01\\x00\\x00'
FILES_DIRS = "/tmp/python-exe-unpacker/unpacked/SMS-Agent.exe/PYZ-00.pyz_extracted"
CONVERT_CMD = "/Users/CarltonXu/workspace/venv_3.7.5/bin/python /Users/CarltonXu/workspace/venv_3.7.5/bin/uncompyle6"
def order_dirs(dirs):
for f in os.listdir(dirs):
if f.split(".")[-1].endswith("pyc"):
os.popen("rm -rf %s" %(dirs + f)).read()
continue
mk_dir = f.split(".")[0:-2]
if mk_dir:
mk_path = dirs + "/".join(mk_dir)
mv_file = dirs + f
mv_to_path = mk_path + "/"
new_file_name = ".".join(f.split(".")[-2:])
os.makedirs(mk_path, exist_ok=True)
shutil.move(mv_file, mv_to_path)
os.renames(mv_to_path + f, mv_to_path + new_file_name)
else:
continue
num = 0
for root, dirs, files in os.walk(FILES_DIRS):
print(files)
for f_name in files:
num += 1
print("Execute Number: %s" %num)
if not f_name.endswith(".pyc"):
continue
new_dirs = root + "/pyz_workspace/"
os.makedirs(new_dirs, exist_ok=True)
old_file = os.path.join(root, f_name)
new_file = os.path.join(new_dirs + f_name)
with open(old_file, "rb") as o_f:
o_f.seek(12)
od_content = o_f.read()
with open(new_file, "wb") as n_f:
n_f.seek(0)
new_content = MAGIC_HEAD + od_content
n_f.write(new_content)
GENERA_PY_FILE = os.path.join(new_dirs + f_name[0:-4] + ".py")
EXEC_CONVERT_CMD = CONVERT_CMD + " %s > %s" %(new_file, GENERA_PY_FILE)
exec_ret = os.popen(EXEC_CONVERT_CMD).read()
if not exec_ret:
print("INFO: Execute convert cmd successful.")
order_dirs(new_dirs)
print("INFO: Order directory successful.")
else:
print("[ERROR]: Execute convert cmd filed, please check it.")
以上是关于Python3 如何反编译EXE的主要内容,如果未能解决你的问题,请参考以下文章