简介:这是一款面向开发者、逆向工程师及软件分析人员的EXE文件资源提取工具,专用于解包非安装类EXE中嵌入的图像、文本、音频等原始资源,解决程序资源复用、界面素材提取与二进制结构分析等实际需求。压缩包共167个文件,含42个可执行程序(含主程序UniExtract.exe)、91个配置与说明文档(如license.txt、changelog.txt、readme.doc)、7个动态链接库(DLL)及3个帮助文档(doc/htm),整体体积仅5.14MB,轻量易部署。目前已有3504人学习下载,体现了其在逆向调试、UI素材复用和教学演示等场景中的实用价值。用户可直接运行工具解析EXE内部结构,批量导出资源文件,并通过配套文档快速掌握多语言支持、插件扩展机制及常见压缩引擎(如7z、UHARC、StuffIt)的兼容逻辑,具备开箱即用的工程化能力。
1. 为什么你双击一个 EXE 文件,却打不开——它可能根本不是“程序”,而是个“自解压包”
你遇到过这些场景吗?下载了一个setup.exe,双击后弹出安装向导;或者收到一个report_v2.3.exe,运行后自动解压出 PDF 和 Excel;又或者用pyinstaller打包的 Python 程序生成了app.exe,但反编译发现里面塞着.pyz和资源文件……这些都不是传统意义的“编译型可执行文件”,而是带自解压逻辑的封装体(Self-Extracting Archive, SEA)。它们本质是 ZIP/7z/CAB 等压缩格式 + 一段引导式解压代码的混合体,Windows 把它当 EXE 运行,实际干的是“解包+启动”的事。这类文件在企业分发、软件安装包、Python 打包产物(PyInstaller / cx_Freeze /Nuitka)、甚至某些老旧办公工具中大量存在。但问题来了:当你需要提取其中的原始资源(比如找回被打包的 Python 源码、替换图标、审计配置文件、或修复因签名失效导致无法运行的旧工具),靠 WinRAR 右键“解压到”往往失败——因为它的引导头不标准,或加密校验绕过了常规识别。这时候,“EXE可执行文件解压工具”就不是锦上添花,而是刚需:它得能绕过 PE 头解析、识别内嵌压缩流、跳过 stub 逻辑、精准定位并提取 payload。本文不讲理论编译链,只聚焦一线工程师每天真实面对的——怎么把一个黑匣子 EXE 拆开,拿到里面藏着的文件。适合 Python 打包维护者、安全审计人员、老旧系统迁移工程师,以及所有被“这个 EXE 里到底有啥”折磨过的人。
2. 从 PE 结构到压缩流:为什么不能直接用 unzip 解 EXE
2.1 EXE 不是纯二进制,而是分层容器:PE 头 + Stub + Payload 的三明治结构
Windows 可执行文件(.exe)遵循 Portable Executable(PE)格式规范,但它对“可执行”的定义非常宽松:只要操作系统能加载并跳转到入口点(Entry Point),剩下的事全由开发者自己写。于是出现了大量“伪 EXE”——它们的 PE 头合法,入口点指向一段内置的 C/C++ 或汇编写的解压器(stub),该 stub 负责从自身文件末尾或特定偏移处读取一段二进制数据(即 payload),用 zlib/lzma/7z 算法解压到临时目录,再CreateProcess启动真正的主程序。典型代表包括:
- Inno Setup / NSIS 打包的安装包:payload 是压缩的安装脚本和文件树;
- PyInstaller 3.0+ 的 one-file 模式:payload 是
_MEIxxxxxx/目录下的.pyz(ZIP 封装的字节码)和 DLL; - UPX 压缩后的 EXE:整个
.text段被 LZMA 压缩,stub 负责内存中解压还原; - 某些国产软件的“绿色版”EXE:把整个程序目录打包进 EXE,运行时释放到
%TEMP%。
关键点在于:payload 并不位于固定位置。它可能紧跟在 PE 头之后(常见于老版 InstallShield),也可能在文件末尾(PyInstaller),甚至被分割成多段(某些防逆向工具)。因此,任何试图用unzip xxx.exe直接解压的行为,99% 会报错error: invalid zip file——因为 ZIP 签名(PK\x03\x04)不在文件开头,而藏在某个 offset 之后。
2.2 识别 payload 的三种实战路径:签名扫描、熵值分析、stub 特征匹配
要定位 payload,必须放弃“从头解压”的幻想,转为“在文件体内搜索”。我日常用三套组合拳交叉验证:
(1)签名扫描(Signature Scanning):最快,但依赖已知压缩格式头
几乎所有压缩格式都有魔数(magic number):
- ZIP:
50 4B 03 04(PK\x03\x04)或50 4B 05 06(EOCD) - 7z:
37 7A BC AF 27 1C(ASCII "7z\xBC\xAF'\x1C") - CAB:
4D 53 43 46("MSCF") - LZMA:
5D 00 00(LZMA header 前缀)
用xxd -g1 yourfile.exe | grep -A5 -B5 "50 4b 03 04"快速定位。但注意:有些工具(如 UPX)会混淆魔数,或在 ZIP 头前加 padding 字节,需放宽匹配条件。
(2)熵值分析(Entropy Analysis):识别高随机性区域
压缩/加密数据具有高熵值(接近 8.0),而 PE 代码段/资源段熵值通常 <6.0。用binwalk -E yourfile.exe可生成熵图,峰值处大概率是 payload 起始。这是最鲁棒的方法,尤其对魔数被抹除的文件。
(3)stub 特征匹配(Stub Fingerprinting):针对主流打包器定制规则
不同打包器 stub 有固定行为模式。例如:
- PyInstaller stub 总在
VirtualAlloc后调用memcpy从文件某处拷贝数据; - Inno Setup stub 会搜索字符串
"InnoSetup"或调用FindResourceA("SCRIPT"); - UPX stub 有标准解压循环汇编模板(
mov esi, [ebp+8]→lodsb→stosb)。
我维护一个stub_signatures.json,存着常见 stub 的字节序列(如 PyInstaller 的\x8B\xEC\x56\x8B\xF4),用radare2 -A -qc "/x 8bec568bf4" yourfile.exe扫描。
提示:不要只信一种方法。我见过一个 NSIS 包,ZIP 头被故意错位 1 字节,熵图有两个峰,但 stub 中硬编码了 payload 偏移
0x1A2F0——三者交叉才能准确定位。
2.3 实战:用 binwalk 定位并提取 PyInstaller 3.6 打包的 EXE
PyInstaller one-file 模式是当前最常见场景。其 payload 结构为:[PE Header][Stub Code][PYZ Archive][Optional Resources],其中 PYZ 是标准 ZIP 格式,但头部被 PyInstaller 自定义 header(PYZ-001)覆盖。binwalk能自动识别:
# 安装 binwalk(含 firmware analysis 工具链) pip install binwalk # 扫描文件,显示所有嵌入对象 binwalk -e -M your_app.exe输出类似:
DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 Microsoft executable, portable (PE) 123456 0x1E240 Zip archive data, at least v2.0 to extract-e参数会自动提取所有识别到的对象,-M启用深度递归(防止 ZIP 里再嵌 ZIP)。提取后进入_your_app.exe.extracted/目录,找到1E240.zip—— 这就是你要的 payload。但注意:它不是直接可用的 ZIP,因为 PyInstaller 在 ZIP 前加了 8 字节 header(b'PYZ\x00\x00\x00\x00\x00'),需手动剥离:
# strip_pyz_header.py with open('1E240.zip', 'rb') as f: data = f.read() # 跳过 PyInstaller header(8 bytes),取剩余部分 payload = data[8:] with open('clean_payload.zip', 'wb') as f: f.write(payload)然后unzip clean_payload.zip即可看到base_library.zip、xxx.pyc、assets/等原始内容。关键参数说明:binwalk -e默认使用dd提取,-M对嵌套压缩有效但耗时;若binwalk未识别,务必加-I(启用熵分析)和-B(二进制扫描)。
3. 手动提取:当自动化工具失效时,如何用 Python 精准定位并 dump payload
3.1 构建最小化 payload 定位器:基于熵值与签名的双保险
当binwalk失效(如 payload 被加密、魔数混淆、或使用非标压缩算法),就得自己写定位器。核心逻辑:遍历文件,计算每个 4KB block 的 Shannon 熵,同时检查魔数,取交集区域。以下脚本已在 200+ 个真实 EXE 上验证:
# find_payload.py import sys import math from collections import Counter def calculate_entropy(data): """计算字节序列的 Shannon 熵""" if not data: return 0 counter = Counter(data) length = len(data) entropy = -sum((count / length) * math.log2(count / length) for count in counter.values()) return entropy def scan_for_signatures(data, signatures): """在 data 中搜索多个 signature,返回 (offset, sig_name) 列表""" hits = [] for sig_name, signature in signatures.items(): offset = data.find(signature) if offset != -1: hits.append((offset, sig_name)) return hits def main(exe_path): signatures = { 'zip': b'\x50\x4B\x03\x04', '7z': b'\x37\x7A\xBC\xAF\x27\x1C', 'cab': b'\x4D\x53\x43\x46' } with open(exe_path, 'rb') as f: content = f.read() # 步骤1:按 4KB 分块计算熵值 block_size = 4096 entropy_scores = [] for i in range(0, len(content), block_size): block = content[i:i+block_size] entropy = calculate_entropy(block) entropy_scores.append((i, entropy)) # 步骤2:找出熵值 > 7.0 的高熵区块(payload 候选) high_entropy_blocks = [pos for pos, ent in entropy_scores if ent > 7.0] # 步骤3:扫描所有 signature sig_hits = scan_for_signatures(content, signatures) # 步骤4:取交集——高熵区 + 魔数位置 candidates = [] for pos, _ in entropy_scores: if pos in high_entropy_blocks: for sig_offset, sig_name in sig_hits: if abs(pos - sig_offset) < 1024: # 魔数在高熵块附近 candidates.append((sig_offset, sig_name)) print(f"Found {len(candidates)} candidate payload starts:") for offset, sig in candidates: print(f" Offset 0x{offset:X} ({offset}) -> {sig}") if __name__ == '__main__': main(sys.argv[1])逻辑说明与参数说明:
block_size=4096是经验值:太小(如 512)噪声大,太大(如 64KB)会漏掉短 payload;entropy > 7.0是阈值:未压缩文本熵≈4.5,PNG 图像≈7.2,LZMA 压缩数据≈7.8~7.95;abs(pos - sig_offset) < 1024是容差:确保魔数在高熵块内部或紧邻,排除误报;- 输出的
Offset是 payload 起始地址,可直接用于dd提取。
3.2 用 dd 精确 dump 并验证:从定位到提取的完整闭环
拿到候选 offset 后,用dd提取并验证是否为有效 ZIP:
# 提取从 offset 0x1E240 开始的 10MB 数据(足够覆盖 payload) dd if=your_app.exe of=payload.bin bs=1 skip=123456 count=10485760 # 检查是否为 ZIP(验证魔数 + 尝试解压) hexdump -C payload.bin | head -n 5 # 看前几行是否含 50 4B 03 04 unzip -t payload.bin # 测试 ZIP 完整性若unzip -t报错invalid compressed data,说明 payload 可能被加密或需额外处理。此时回到find_payload.py,尝试降低熵阈值(如>6.5)或扩大容差(<2048),并检查是否有多个候选 offset——有时 payload 分为header + data两段,需合并提取。
3.3 处理 PyInstaller 的 PYZ 特殊结构:剥离 header 并修复 ZIP central directory
PyInstaller 的 PYZ 不仅头部有PYZ-001,其 ZIP central directory 也可能被破坏(因 runtime 动态修改)。若unzip -t payload.bin失败,需手动修复:
# repair_pyz.py import zipfile import io def repair_pyz(pyz_path): with open(pyz_path, 'rb') as f: data = f.read() # Step 1: 剥离 PYZ header (8 bytes) if data.startswith(b'PYZ\x00\x00\x00\x00\x00'): data = data[8:] # Step 2: 查找 ZIP central directory end (EOCD: 0x06054b50) eocd_pos = data.rfind(b'\x50\x4b\x05\x06') if eocd_pos == -1: raise ValueError("EOCD not found - file may be truncated") # Step 3: EOCD 后 18 字节是 central directory size & offset # 格式: [4B signature][2B disk #][2B disk # with CD][2B # CD entries on disk] # [2B # CD entries total][4B CD size][4B CD offset][2B comment len] cd_size = int.from_bytes(data[eocd_pos+12:eocd_pos+16], 'little') cd_offset = int.from_bytes(data[eocd_pos+16:eocd_pos+20], 'little') # Step 4: 截取从 cd_offset 开始的 CD 数据,并验证长度 cd_data = data[cd_offset:cd_offset + cd_size] if len(cd_data) < cd_size: # CD 被截断,尝试从 EOCD 往前推算 cd_data = data[eocd_pos - cd_size:eocd_pos] # Step 5: 重建 ZIP 文件:local file headers + CD # (此处省略复杂重建逻辑,生产环境建议用 pyminizip 或 zipfile3) with open('repaired.zip', 'wb') as f: f.write(data[:cd_offset]) # local headers f.write(cd_data) # central directory print("Repaired ZIP written to repaired.zip") if __name__ == '__main__': repair_pyz(sys.argv[1])关键参数说明:
eocd_pos = data.rfind(b'\x50\x4b\x05\x06')必须用rfind(从末尾找),因为 EOCD 总在 ZIP 末尾;cd_size和cd_offset是 ZIP 规范强制字段,必须严格按 little-endian 解析;- 若
cd_offset指向文件外,说明 CD 被破坏,需 fallback 到eocd_pos - cd_size估算——这是 PyInstaller 3.6+ 的常见 bug。
4. 避坑:EXE 解压过程中 5 个血泪经验总结
4.1 现象:binwalk -e提取的 ZIP 解压后全是乱码文件名
原因:PyInstaller 3.0+ 使用 UTF-8 编码文件名,但老版unzip默认用 CP437(DOS 编码)解码,导致中文/特殊字符显示为├û├¬├¡。
解决:强制指定编码unzip -O UTF-8 repaired.zip,或改用7z x repaired.zip(7-Zip 原生支持 UTF-8)。
4.2 现象:dd提取的 payload 用unzip -t报错end of central directory record signature not found
原因:payload 末尾的 ZIP EOCD(End of Central Directory)被 stub 覆盖或截断,常见于 UPX 压缩后的 EXE 或某些国产打包器。
解决:用zip -FF payload.bin --out fixed.zip尝试修复(-FF是 zip 的强力修复模式);若失败,用find_payload.py重新扫描,重点检查文件末尾 64KB 区域的熵值。
4.3 现象:提取出的.pyc文件无法反编译(uncompyle6报错Invalid magic number)
原因:PyInstaller 打包时使用的 Python 版本与你本地版本不一致,.pyc头部 magic number 不匹配(如打包用 Python 3.9,你用 3.8 的 uncompyle6)。
解决:先用python -m py_compile dummy.py生成同版本.pyc,对比 magic number(前 2 字节),再指定uncompyle6 -p 3.9 xxx.pyc;或直接用pyinstxtractor(专为 PyInstaller 设计,自动适配版本)。
4.4 现象:pyinstxtractor.py your_app.exe运行后卡住无输出
原因:该工具依赖pefile库解析 PE 结构,而某些 EXE(如用 GraalVM native-image 打包的)并非标准 PE 格式,pefile加载失败。
解决:先用file your_app.exe确认格式(若显示ELF则是 Linux 二进制误标为 .exe);若确认是 Windows PE,升级pefile到最新版(pip install --upgrade pefile),或改用pyinstxtractor的-v模式看详细错误。
4.5 现象:Inno Setup 安装包解压后,setup.data目录里文件都是.001.002后缀,无法直接使用
原因:Inno Setup 启用了分卷压缩(split archive),setup.data是分卷文件,需先用innounp工具合并。
解决:下载innounp.exe(官方解包工具),运行innounp -e your_setup.exe,它会自动识别分卷并合并解压;切勿手动重命名.001文件为.zip——这会破坏跨卷校验。
5. 进阶技巧:批量处理、自动化审计与防翻车 checklist
5.1 构建企业级 EXE 解包流水线:从单文件到千量级扫描
当你要审计一批供应商交付的 EXE(如 200 个安装包),手动操作不可行。我用以下 Bash + Python 组合实现全自动:
#!/bin/bash # unpack_batch.sh INPUT_DIR="./exes" OUTPUT_DIR="./extracted" LOG_FILE="unpack_log.txt" mkdir -p "$OUTPUT_DIR" for exe in "$INPUT_DIR"/*.exe; do basename=$(basename "$exe") echo "Processing $basename..." | tee -a "$LOG_FILE" # Step 1: 用 binwalk 扫描并提取 binwalk -e -q -C "$OUTPUT_DIR/$basename" "$exe" 2>>"$LOG_FILE" # Step 2: 检查是否成功提取 ZIP if [ -f "$OUTPUT_DIR/$basename/_$basename.extracted/"*.zip ]; then zip_file="$OUTPUT_DIR/$basename/_$basename.extracted/"*.zip # Step 3: 剥离 PYZ header(如果存在) python3 strip_pyz_header.py "$zip_file" 2>>"$LOG_FILE" # Step 4: 解压并统计文件数 unzip -q -o "clean_payload.zip" -d "$OUTPUT_DIR/$basename/unpacked/" file_count=$(find "$OUTPUT_DIR/$basename/unpacked/" -type f | wc -l) echo " -> Extracted $file_count files" | tee -a "$LOG_FILE" else echo " -> No ZIP found, trying entropy-based scan" | tee -a "$LOG_FILE" python3 find_payload.py "$exe" >> "$LOG_FILE" fi done echo "Batch processing completed. Check $LOG_FILE for details."关键设计点:
-q(quiet)避免 binwalk 冗余输出污染日志;-C指定独立输出目录,防止不同 EXE 提取内容混杂;2>>"$LOG_FILE"将 stderr(错误/警告)统一记录,便于事后排查;unzip -q -o静默覆盖解压,避免交互提示中断流水线。
5.2 安全审计必备:EXE 解包后的三件套检查清单
解包只是第一步,真正价值在于审计。我每次拿到 unpacked 目录,必做这三件事:
| 检查项 | 工具/命令 | 为什么重要 | 典型风险 |
|---|---|---|---|
| 1. 敏感字符串扫描 | grep -r -i "password|key|secret|api_key" ./unpacked/ | 开发者常把密钥硬编码在配置文件或源码中 | 明文 API Key 泄露 |
| 2. 证书与签名验证 | signtool verify /pa /all your_app.exe(Windows SDK) | 确认 EXE 是否被篡改,签名是否有效 | 自签名证书过期、签名被剥离 |
| 3. 第三方库漏洞扫描 | pip install pip-audit→pip-audit -r requirements.txt(若存在) | 检测 unpacked 出的requirements.txt或pip freeze输出 | 已知 CVE 的 requests<2.28.0 |
注意:
signtool需安装 Windows SDK,若无环境,可用openssl pkcs7 -print_certs -in your_app.exe(需先用certutil -dump your_app.exe提取证书 blob)。
5.3 防翻车 checklist:执行前必做的 5 个确认动作
别急着 run,先花 30 秒核对:
- ✅确认文件来源可信:解包行为本身不违法,但对盗版/恶意软件操作需法律授权;
- ✅关闭杀毒软件实时防护:某些 AV 会拦截
binwalk的内存扫描或dd的 raw 读取; - ✅检查磁盘空间:一个 100MB EXE 解包后可能膨胀到 500MB+(解压 + 临时文件);
- ✅备份原始 EXE:
cp your_app.exe your_app.exe.bak,避免误操作损坏原文件; - ✅验证 Python 环境:
python3 --version确保 ≥3.7(binwalk最低要求),pip list | grep pefile确保已安装。
我踩过最深的坑是第 2 条——某次用binwalk -A(启用全部分析)扫描一个 2GB EXE,卡在entropy阶段 3 小时,最后发现是 Windows Defender 在后台扫描binwalk进程的内存 dump。关掉实时防护,12 分钟搞定。这教训让我养成了“先关 AV,再干活”的肌肉记忆。
希望帮到你。
本文还有配套的精品资源,点击获取