# Analyzing Packed Malware With Upx Unpacker

> 识别并解包 UPX 加壳及其他加壳恶意软件样本，以暴露原始可执行代码供静态分析使用。 涵盖标准 UPX 解包及处理阻止自动解压缩的修改版 UPX 头部。 适用于恶意软件解包、UPX 解压缩、去除加壳保护或为分析准备加壳样本等请求。

- Skill: `killvxk/analyzing-packed-malware-with-upx-unpacker` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add killvxk/analyzing-packed-malware-with-upx-unpacker`
- Raw SKILL.md: https://api.skillmd.com/api/skills/killvxk/analyzing-packed-malware-with-upx-unpacker/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: killvxk (https://skillmd.com/u/killvxk)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/killvxk/analyzing-packed-malware-with-upx-unpacker

---


# 使用 UPX Unpacker 分析加壳恶意软件

## 适用场景

- 静态分析显示高熵节和极少的导入函数，表明二进制文件已加壳
- PEiD、Detect It Easy 或 PEStudio 识别出 UPX 或其他已知加壳工具
- 导入表仅包含 LoadLibrary 和 GetProcAddress（加壳二进制文件典型的运行时导入解析方式）
- 需要恢复原始二进制文件以在 Ghidra 或 IDA 中进行正常反汇编和反编译
- 因恶意软件作者修改了 UPX 魔数或头部导致自动 UPX 解压缩失败

**不适用于**处理自定义加壳、基于 VM 的保护器（Themida、VMProtect），或通过调试进行动态解包更合适的样本。

## 前置条件

- 已安装 UPX（Ultimate Packer for eXecutables）（`apt install upx-ucl` 或从 https://upx.github.io/ 下载）
- Detect It Easy (DIE)，用于加壳识别
- Python 3.8+，安装 `pefile` 库用于手动修复头部
- x64dbg 或 x32dbg，用于自动化工具失败时的手动解包
- PE-bear 或 CFF Explorer，用于 PE 头部检查和修复
- 隔离的分析虚拟机（无网络连接）

## 工作流程

### 步骤 1：识别加壳工具

确认样本是否加壳并识别加壳工具：

```bash
# 使用 Detect It Easy 检查
diec suspect.exe

# 使用 UPX 检查（仅测试，不解包）
upx -t suspect.exe

# 基于 Python 的熵值和加壳检测
python3 << 'PYEOF'
import pefile
import math

pe = pefile.PE("suspect.exe")

print("节分析：")
for section in pe.sections:
    name = section.Name.decode().rstrip('\x00')
    entropy = section.get_entropy()
    raw = section.SizeOfRawData
    virtual = section.Misc_VirtualSize
    print(f"  {name:8s} 熵值：{entropy:.2f}  原始大小：{raw:>8}  虚拟大小：{virtual:>8}")

# 检查 UPX 节名称
section_names = [s.Name.decode().rstrip('\x00') for s in pe.sections]
if 'UPX0' in section_names or 'UPX1' in section_names:
    print("\n[!] 检测到 UPX 节名称")
elif '.upx' in [s.lower() for s in section_names]:
    print("\n[!] 检测到 UPX 变体节名称")

# 检查导入数量（加壳二进制文件导入极少）
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
    total_imports = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT)
    print(f"\n总导入数：{total_imports}")
    if total_imports < 10:
        print("[!] 导入函数极少——可能已加壳")
else:
    print("\n[!] 无导入目录——高度加壳")
PYEOF
```

### 步骤 2：尝试标准 UPX 解压缩

尝试使用内置 UPX 解压功能：

```bash
# 标准 UPX 解压缩
upx -d suspect.exe -o unpacked.exe

# 如果 UPX 报 "not packed by UPX" 错误，说明头部可能已被修改
# 用详细输出进行调试
upx -d suspect.exe -o unpacked.exe -v 2>&1

# 验证解包后的文件
file unpacked.exe
diec unpacked.exe
```

### 步骤 3：修复被篡改的 UPX 头部

如果标准解压缩失败，修复被篡改的魔数：

```python
# 修复被篡改的 UPX 头部
import struct

with open("suspect.exe", "rb") as f:
    data = bytearray(f.read())

# UPX 魔数："UPX!"（0x55505821）
# 恶意软件作者通常会修改这些字节以阻止自动解包

# 搜索被修改的 UPX 签名
upx_magic = b"UPX!"
modified_patterns = [b"UPX0", b"UPX\x00", b"\x00PX!", b"UPx!"]

# 查找并还原节名称
pe_offset = struct.unpack_from("<I", data, 0x3C)[0]
num_sections = struct.unpack_from("<H", data, pe_offset + 6)[0]
section_table_offset = pe_offset + 0x18 + struct.unpack_from("<H", data, pe_offset + 0x14)[0]

print(f"PE 偏移：0x{pe_offset:X}")
print(f"节数量：{num_sections}")
print(f"节表偏移：0x{section_table_offset:X}")

for i in range(num_sections):
    offset = section_table_offset + (i * 40)
    name = data[offset:offset+8]
    print(f"节 {i}：{name}")

# 还原二进制文件中的 UPX 魔数
# 搜索 UPX 头部签名位置（通常在加壳数据末尾附近）
for i in range(len(data) - 4):
    if data[i:i+3] == b"UPX" and data[i+3] != ord("!"):
        print(f"在偏移 0x{i:X} 处发现被修改的 UPX 魔数：{data[i:i+4]}")
        data[i:i+4] = b"UPX!"
        print(f"已还原为：UPX!")

# 如果节名称被修改也一并还原
for i in range(num_sections):
    offset = section_table_offset + (i * 40)
    name = data[offset:offset+8].rstrip(b'\x00')
    if name in [b"UPX0", b"UPX1", b"UPX2"]:
        continue  # 已正确
    # 检查常见的修改方式
    if name.startswith(b"UP") or name.startswith(b"ux"):
        original = f"UPX{i}".encode().ljust(8, b'\x00')
        data[offset:offset+8] = original
        print(f"在 0x{offset:X} 处还原节名称为 {original}")

with open("suspect_fixed.exe", "wb") as f:
    f.write(data)

print("\n已写入修复文件。重试：upx -d suspect_fixed.exe -o unpacked.exe")
```

### 步骤 4：使用调试器手动解包

当自动解包完全失败时，使用动态解包：

```
使用 x64dbg 手动 UPX 解包：
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 在 x64dbg 中加载加壳样本
2. 运行到入口点（系统断点后按 F9）
3. UPX 解包桩模式：
   a. PUSHAD（保存所有寄存器）
   b. 解压缩循环（处理加壳的节）
   c. 解析导入（LoadLibrary/GetProcAddress 调用）
   d. POPAD（恢复寄存器）
   e. JMP 到 OEP（原始入口点）
4. 在 PUSHAD 后对 ESP 设置硬件断点：
   - PUSHAD 后，在寄存器中右键点击 ESP -> 在内存中跟随
   - 在 [ESP] 地址处设置访问硬件断点
   - 运行（F9）——在 POPAD 前跳转到 OEP 时中断
5. 单步执行（F7/F8）直到到达 JMP 到 OEP
6. 在 OEP 处：使用 Scylla 插件转储并修复导入：
   - 插件 -> Scylla -> OEP = 当前 EIP
   - 点击"IAT Autosearch" -> "Get Imports"
   - 点击"Dump"保存解包后的二进制文件
   - 点击"Fix Dump"修复导入表
```

### 步骤 5：验证解包后的二进制文件

验证解包样本的有效性和完整性：

```bash
# 验证解包后的 PE 文件是否有效
python3 << 'PYEOF'
import pefile

pe = pefile.PE("unpacked.exe")

# 检查节是否正常
print("解包后节分析：")
for section in pe.sections:
    name = section.Name.decode().rstrip('\x00')
    entropy = section.get_entropy()
    print(f"  {name:8s} 熵值：{entropy:.2f}")

# 验证导入已解析
print(f"\n导入数量：")
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
    for entry in pe.DIRECTORY_ENTRY_IMPORT:
        dll = entry.dll.decode()
        count = len(entry.imports)
        print(f"  {dll}：{count} 个函数")
    total = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT)
    print(f"  总计：{total} 个导入")

# 比较文件大小
import os
packed_size = os.path.getsize("suspect.exe")
unpacked_size = os.path.getsize("unpacked.exe")
print(f"\n加壳：   {packed_size:>10} 字节")
print(f"解包后：{unpacked_size:>10} 字节")
print(f"比率：    {unpacked_size/packed_size:.1f}x")
PYEOF
```

## 核心概念

| 术语 | 定义 |
|------|------|
| **加壳（Packing）** | 压缩或加密可执行代码以减小文件大小并阻碍静态分析；二进制文件包含在运行时恢复代码的解包桩 |
| **UPX** | Ultimate Packer for eXecutables；开源可执行文件加壳工具，因免费且有效而常被恶意软件作者滥用 |
| **原始入口点（OEP）** | 加壳前恶意软件代码的真实起始地址；解包桩解压代码后跳转到 OEP |
| **导入重建（Import Reconstruction）** | 使用 Scylla 或 ImpRec 等工具，在从内存转储解包进程后重建导入地址表的过程 |
| **PUSHAD/POPAD** | 保存/恢复所有通用寄存器的 x86 指令；UPX 使用此模式在解包过程中保留寄存器状态 |
| **节熵（Section Entropy）** | PE 节数据的随机性度量；加壳节熵值 > 7.0，而正常代码节平均为 5.0-6.5 |
| **魔数（Magic Bytes）** | 文件中用于标识格式的签名字节；UPX 使用"UPX!"，恶意软件作者会修改这些字节以阻止自动解压缩 |

## 工具与系统

- **UPX**：开源可执行文件加壳工具，具有对正常加壳文件进行内置解压缩的能力
- **Detect It Easy (DIE)**：加壳工具、编译器和链接器检测工具，可识别 PE、ELF 和 Mach-O 文件上的保护措施
- **x64dbg/x32dbg**：开源 Windows 调试器，用于通过动态执行和基于断点的 OEP 查找进行手动解包
- **Scylla**：与 x64dbg 集成的导入重建工具，用于内存转储后重建 IAT
- **PE-bear**：PE 文件查看器和编辑器，用于解包后检查和修复 PE 头部

## 常见场景

### 场景：解包带有修改版 UPX 头部的恶意软件

**场景背景**：一个恶意软件样本通过节名称（UPX0、UPX1）被识别为 UPX 加壳，但 `upx -d` 失败并报"CantUnpackException: header corrupted"。恶意软件作者修改了 UPX 魔数以阻止自动解压缩。

**方法**：
1. 在十六进制编辑器中打开二进制文件，搜索 UPX 头部区域（通常在加壳数据末尾）
2. 识别被修改的魔数（如"UPX!"被改为"UPX\x00"或完全清零）
3. 使用 Python 修复脚本还原"UPX!"魔数并修正节名称
4. 在修复后的二进制文件上重试 `upx -d`
5. 如果修复失败，回退到使用 x64dbg 手动解包（PUSHAD -> ESP 硬件断点 -> POPAD -> JMP OEP）
6. 验证解包后的二进制文件具有正常的导入和合理的熵值
7. 导入 Ghidra 或 IDA 进行完整静态分析

**常见陷阱**：
- 假设 UPX 是唯一的加壳工具；二进制文件可能是双重加壳（UPX + 自定义层）
- 在原始加壳样本上操作而不是在副本上工作
- 手动内存转储后未重建导入（未修复 IAT 的转储二进制文件会崩溃）
- 忘记检查附加在 UPX 加壳 PE 节之后的 overlay 数据

## 输出格式

```
解包分析报告
===========================
样本：           suspect.exe
SHA-256：        e3b0c44298fc1c149afbf4c8996fb924...
加壳工具：       UPX 3.96（修改版头部）

加壳二进制文件
节：         UPX0（熵值：0.00）UPX1（熵值：7.89）.rsrc（熵值：3.45）
导入：       2 个（kernel32.dll：LoadLibraryA、GetProcAddress）
文件大小：   98,304 字节

解包方法
方法：       头部修复 + UPX -d
头部修复：   在偏移 0x1F000 处还原 UPX! 魔数
命令：       upx -d suspect_fixed.exe -o unpacked.exe
结果：       成功

解包后二进制文件
节：         .text（熵值：6.21）.rdata（熵值：4.56）.data（熵值：3.12）.rsrc（熵值：3.45）
导入：       147 个（kernel32、user32、advapi32、wininet、ws2_32）
文件大小：   245,760 字节（扩展 2.5 倍）
OEP：        0x00401000

验证
PE 有效：         是
导入已解析：      是（8 个 DLL 共 147 个函数）
可执行：          是（在沙箱中运行不崩溃）

后续步骤
- 将 unpacked.exe 导入 Ghidra 进行完整反汇编
- 对解包后的二进制文件运行 YARA 规则
- 将解包后的二进制文件提交 VirusTotal 以获得更好的检测率
```

