python代码打包和逆向

文章最后更新时间为:2022年08月29日 16:52:04

python代码不太适合打包,但是由于用户多,总是有一些小众的需求需要打包,尤其是一些gui小工具,于是来研究下这块,做个记录。

1. python代码打包方案测试

下面是我之前测试打包flask写的web app写的笔记,测试下来的话:Nuitka速度最快,性能最好;pyinstaller的兼容性最好。

1.1 pyinstaller

一般来说python代码打包最常见的还是pyinstaller

pyinstaller -option ***.py
参数option可以有多个值:
  -F : 指定打包后只生成一个exe格式的文件
  -D : 生成一个文件目录包含可执行文件和相关动态链接库和资源文件等(默认选项)
  -c : –console, –nowindowed 使用控制台,无界面(默认选项)
  -w : –windowed, –noconsole 使用窗口,无控制台

如何减小打包后的体积?

  1. python虚拟环境要足够干净,最小依赖。
  2. 打包时加上增加 -w 参数 ,可以减小体积(测试下来用处不是特别大)
  3. 将import改为from...import...(测试下来用处不是特别大)

使用pyinstall -F index.py打包成单个文件的启动速度太慢,怎么提高可执行文件启动速度?
将-F换成-D:
-D:生成一个文件夹,里面是多文件模式,启动快。
-F:仅仅生成一个文件,不暴露其他信息,启动较慢。

但是测试下来还是很慢,app打包完启动要20s,python打包就是这么无解,因为https://stackoverflow.com/questions/9469932/app-created-with-pyinstaller-has-a-slow-startup

1.2 cx-Freeze

安装
pip install cx-Freeze

打包
cxfreeze run_app.py  --target-dir dist1

亲测bug太多,无法打包flask web。

1.3 py2exe

只能打包成exe文件,不跨平台,故未测试。

1.4 nuitka

Nuitka 是一个用于将Python程序打包为可执行文件的第三方模块。

相较于PyInstaller其能够实现打包体积更小、打包后的程序运行速度更快、程序编译加密

教程:https://zhuanlan.zhihu.com/p/165978688

安装

brew install gcc (windows上安装mingw64)
pip install nuitka

打包:

# 1. linux/mac上
nuitka3 --follow-imports run_app.py

--follow-imports是打包依赖,一般情况下这样就可以了
但是如果有些包依赖dll或者系统的组件的话,需要加上--stardalone

# 2. windows上:
nuitka3 --mingw64 --follow-imports run_app.py

启动速度很快、打包后的体积也是最小的,牛没白吹。

2. python可执行文件反编译

以下实验过程,我先写了一个index.py:

def test(a,b):

    print(a+b)

print("11111")
test(1,2)
print("22222")
input()

然后使用pyinstall -F index.py打包成一个exe,下面的实验都是基于这个exe来做的。

2.1 python打包的可执行文件识别

怎么识别一个可执行文件是什么语言写的,这是第一步。打包python代码生成的可执行文件有以下特征:

1.strings中出现__main____file__py等关键词。

可以使用ida也可以使用查壳工具 https://github.com/horsicq/DIE-engine

2021-11-03T09:43:32.png

2021-11-03T12:16:16.png

2.函数特征中有sub_140006490("_MEIPASS2")sub_140002790("Cannot open self %s or archive %s\n", v19, v21);等相似语句。

2021-11-03T12:13:15.png

2.2 反编译可执行文件到pyc

这里只考虑pyinstall打包的程序,需要注意 反编译EXE/ELF文件的Python版本必须与打包时的版本一致,主要有两种方式。

1.pyinstaller 自带脚本 archive_viewer.py

这个脚本是用来查看可执行文件的结构的,详细文档可以参考 https://pyinstaller.readthedocs.io/en/stable/advanced-topics.html#using-pyi-archive-viewer

archive_viewer.py在pyinstall package中,比如windows下的C:\Users\xxx\miniconda3\Lib\site-packages\PyInstaller\utils\cliutils目录。

使用教程为:

# 1. 打开文件
pyi-archive_viewer auto_organize.exe

# 2. 操作命令:
U: go Up one level
O <name>: open embedded archive name
X <name>: extract name
Q: quit

2021-11-03T12:46:11.png

这里看到分析出目标程序的模块,比如下面我们解析出index.pyc和struct.pyc

2021-11-03T12:47:53.png

这种只能一次提取一个文件,如果包含有多个文件的话,需要一个一个提取。另外如果要提取其他被导入的pyc文件,则需要先打开PYZ-00.pyz,然后再进行提取。

2.https://github.com/extremecoders-re/pyinstxtractor

使用archive_viewer比较麻烦,使用这个脚本可以一键提取。

2021-11-03T12:56:33.png

2.3 pyc to py

pyc到py基本上没啥难度,但是转换之前需要做一点改动。使用pyinstaller打包的文件,文件头会被去掉。再还原的过程中,我们需要手动进行修补,这个文件头长度一般为16字节。

比如我们直接编译py到pyc,然后用010 editor 对比下文件头

# 编译py到pyc
python -m py_compile index.py

2021-11-03T13:22:38.png

可以看到反编译出来的pyc少了16个字节的文件头。那么这16个字节的文件头是多少呢?我们可以直接复制反编译出来的struct.pyc的文件头,然后插入index.pyc的开头。

2021-11-03T13:33:13.png

2021-11-03T13:33:26.png

2021-11-03T13:34:31.png

然后使用uncompyle6反编译即可

安装uncompyle6
pip install -U uncompyle6

反编译
uncompyle6 -o xxx.py xxx.pyc

2021-11-03T13:37:11.png

可以看出来和源文件基本没差。

3. 一键编译脚本

上面对于反编译的过程比较繁琐,拿到一个exe,要是可以一键反编译exe可执行文件就完美了,于是写了一个脚本把上面的步骤给自动化了。

用法和脚本放在github上了:https://github.com/saucer-man/exe2py

#!/usr/bin/env python3
# coding: utf-8


import os
import sys
import pyinstxtractor
try:
    from uncompyle6.bin import uncompile
except:
    print("没有安装uncompyle6,尝试pip install uncompyle6")
    sys.exit()
import shutil

# 读取校验头
def find_magic(pyc_dir):
    struct_file = os.path.join(pyc_dir,"struct.pyc")
    if not os.path.exists(struct_file):
        print(f"没有找到{struct_file}")
        sys.exit()
    with open (struct_file,"rb") as f:
        magic = f.read(16)

    return magic

# 找到程序中的pyc文件
def find_pyc(pyc_dir):
    for pyc_file in os.listdir(pyc_dir):
        if not pyc_file.startswith("_") and pyc_file.endswith("manifest"):
            print("--------------")
            print(pyc_file)
            main_file = pyc_file.replace(".exe.manifest", ".pyc")
            result = f"{pyc_dir}/{main_file}"

            if os.path.exists(result):
                return main_file



def exe2py(exe_file, complie_child=False):
    # 先执行pyinstxtractor将exe转化为pyc文件
    sys.argv = ['pyinstxtractor.py', exe_file]
    pyinstxtractor.main()


    # 恢复当前目录位置
    os.chdir("..")

    # 下面解析pyc文件
    # 1. 先找到文件头
    pyc_dir = os.path.basename(exe_file) + "_extracted"
    magic = find_magic(pyc_dir)
    print(f"找到文件头:{magic.hex()}")

    # 然后遍历pyc文件加上文件头
    if os.path.exists("pycfile_tmp"):
        shutil.rmtree("pycfile_tmp")
    os.mkdir("pycfile_tmp")

    main_file = find_pyc(pyc_dir)
    if not main_file:
        print("没有找到主程序pyc文件")
        sys.exit()
    print(f"找到主程序pyc文件:{main_file}")
    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:

        magic_ = read.read(4)
        if magic_ == magic[:4]:
            print(f"{pyc_dir}/{main_file}已经存在文件头")
            pass
        else:
            write.write(magic)
        write.write(magic_)
        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()

    # 下面开始反编译 引用的包里面的pyc文件
    pyz_dir = f"{pyc_dir}/PYZ-00.pyz_extracted"
    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:
            magic_ = read.read(4)
            if magic_ == magic[:4]:
                # print(f"{pyc_dir}/{main_file}已经存在文件头")
                pass
            else:
                write.write(magic)
            write.write(magic_)
            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()

if __name__=="__main__":
    if len(sys.argv) < 2:
        print('[+] Usage: exe2py.py <filename>')
    exe2py(exe_file=sys.argv[1])

需要注意的是使用的python版本要和exe的python版本对应,否则会出错。

4. 反编译实战

逛论坛时发现一个帖子 https://www.52pojie.cn/thread-1535961-1-1.html ,作者写了一个程序可以检测apk中的引流配置,下载下来时发现是一个gui程序,于是打算反编译看看源码是怎么写的,膜拜一下大佬们。下面用我写的一键脚本反编译:

2021-11-04T04:21:06.png

反编译完成后,在py_result目录下,可以看到主程序入口2.pyc.py。下面尝试直接运行这个py代码,发现可以正常运行:

2021-11-04T04:23:51.png

2021-11-04T04:22:33.png

下面就可以自由学习代码了

5. 防止反编译

  • 加壳
  • pyinstall在打包命令后面加上--key命令,可以给python的字节码加密,但是还是有被破解的可能。
  • 修改python编译器,修改opcode,这算是最安全的方式了

6. 参考

1 + 8 =
1 评论
    lee Chrome 101 OSX
    2022年06月07日 回复

    一般小的程序按照你说的这套打包下来exe或者mac这边大概有多大?1M左右?