解包 PyInstaller 生成的可执行文件
最近使用的一个小工具,想了解一下它的工作原理,原本以为是 Native 开发,拖入 IDA 发现大量 PyInstaller 相关字符串,意识到这个程序是使用 Python 开发,PyInstaller 打包成的应用,遂尝试了对其进行解包和解密,记录如下。
解包
首先找到了 pyinstxtractor 这个工具,只有一个简单的 py 文件,即可从可执行文件中解包出来 .pyc
文件。不过遗憾的是对于 PyInstaller 生成的 PYZ
文件未能成功解出来,不知道是我的姿势问题还是工具未能支持。不过在这个项目的 see-also 部分,发现了 pyinstxtractor-ng 和 pyinstxtractor-web 两个工具,
图简单直接使用了 pyinstxtractor-web
,效果很好,一步到位就解包完成了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
.
├── PYZ-00.pyz_extracted
│ ├── __future__.pyc.pyc.encrypted
│ └── zipimport.pyc.pyc.encrypted
├── base_library.zip
├── certifi
│ ├── cacert.pem
│ └── py.typed
├── charset_normalizer
│ ├── md.cpython-310-darwin.so
│ └── md__mypyc.cpython-310-darwin.so
├── lib-dynload
│ └── zlib.cpython-310-darwin.so
├── libbz2.dylib
├── libcrypto.3.dylib
├── libffi.8.dylib
├── liblzma.5.dylib
├── libpython3.10.dylib
├── libssl.3.dylib
├── libz.1.dylib
├── main.pyc
├── pyi_rth_inspect.pyc
├── pyi_rth_pkgres.pyc
├── pyi_rth_pkgutil.pyc
├── pyiboot01_bootstrap.pyc
├── pyimod00_crypto_key.pyc
├── pyimod01_archive.pyc
├── pyimod02_importers.pyc
├── pyimod03_ctypes.pyc
├── struct.pyc
└── tinyaes.cpython-310-darwin.so
|
解密
对与 .pyc
文件,可以通过 pycdc 项目提供的 pycdc
命令反编译为 .py
源码。但是项目中大部分的 pyc
文件都被 PyInstaller 加密的,无法直接反编译。
观察一下就能发现,pyimod00_crypto_key.pyc
这个文件可能就是加密的 key,而 pyimod01_archive.pyc
文件可能是打包的相关逻辑。使用 pycdc
反编译结果如下:
pyimod00_crypto_key.py:
1
2
3
4
|
# Source Generated with Decompyle++
# File: pyimod00_crypto_key.pyc (Python 3.10)
key = 'xxxxxxxxxxxxx-'
|
pyimod01_archive.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
# Source Generated with Decompyle++
# File: pyimod01_archive.pyc (Python 3.10)
import sys
import os
import struct
import marshal
import zlib
import _frozen_importlib
PYTHON_MAGIC_NUMBER = _frozen_importlib._bootstrap_external.MAGIC_NUMBER
CRYPT_BLOCK_SIZE = 16
PYZ_ITEM_MODULE = 0
PYZ_ITEM_PKG = 1
PYZ_ITEM_DATA = 2
PYZ_ITEM_NSPKG = 3
class ArchiveReadError(RuntimeError):
pass
class Cipher:
'''
This class is used only to decrypt Python modules.
'''
def __init__(self):
import pyimod00_crypto_key
key = pyimod00_crypto_key.key
# WARNING: Decompyle incomplete
def __create_cipher(self, iv):
return self._aesmod.AES(self.key.encode(), iv)
def decrypt(self, data):
cipher = self.__create_cipher(data[:CRYPT_BLOCK_SIZE])
return cipher.CTR_xcrypt_buffer(data[CRYPT_BLOCK_SIZE:])
# 省略大部分
|
可以可以看到加密的这部门逻辑,使用了 AES 加密,块大小16字节,依据 CTR_xcrypt_buffer
这个方法名可以找到对应的 package 是 tinyaes。
再依据从网上找到的其他文章,解密 pyc
的脚本就出来了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#!/bin/env python3
import os
import tinyaes
import zlib
def decrypt(filepath, target_path):
key = b'xxxxxxxxxxxxxxxx' # 加密密钥
content = open(filepath, 'rb').read()
CRYPT_BLOCK_SIZE = 16
# 被加密文件的头 16 个字节是 iv
iv = content[:CRYPT_BLOCK_SIZE]
cipher = tinyaes.AES(key, iv)
try:
# 解密结果需要解压
decrypt_res = zlib.decompress(cipher.CTR_xcrypt_buffer(content[CRYPT_BLOCK_SIZE:]))
with open('tmp.pyc', 'wb') as f:
# pyc 文件的文件头被去掉了,需要手动补回来,16个字节
f.write(b'\x6f\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0')
f.write(decrypt_res)
# 这里直接调用 pycdc 反编译成 py 源码文件了
os.system(f'pycdc tmp.pyc -o {target_path}')
except zlib.error as e:
print(filepath)
|
Done~
参考
- [原创]Python逆向——Pyinstaller逆向
- pyinstaller 逆向筆記