关于 analyzing-packed-malware-with-upx-unpacker
用于识别 UPX 加壳可执行文件并还原二进制文件的安全分析技能,供恶意软件分析人员使用。涵盖标准解压、修复篡改的文件头,以及自动方法失效时通过调试器手动脱壳。
主要功能
- 01识别 UPX 加壳
- 02使用 UPX 解压
- 03修复篡改的文件头
- 04使用调试器手动脱壳
- 05验证还原后的二进制文件
适用场景
- 01还原二进制文件以供 Ghidra 或 IDA 分析
- 02处理 UPX 签名被修改的样本
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mukul975/ anthropic-cybersecurity-skills --skill analyzing-packed-malware-with-upx-unpacker安装后在 Claude Code 中输入/analyzing-packed-malware-with-upx-unpacker确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSEreferences/api-reference.mdscripts/agent.py详细介绍
使用 UPX Unpacker 分析加壳恶意软件
何时使用
- 静态分析显示高熵区段且导入表极小,表明该二进制文件已加壳
- PEiD、Detect It Easy 或 PEStudio 识别出 UPX 或其他已知加壳器
- 导入表仅包含 LoadLibrary 和 GetProcAddress(加壳二进制文件典型的运行时导入解析)
- 你需要恢复原始二进制文件,以便在 Ghidra 或 IDA 中正确进行反汇编和反编译
- 自动化 UPX 脱壳失败,因为恶意软件作者修改了 UPX 魔数字节或头部
不要使用 于以下情况:处理自定义加壳器、基于虚拟机的保护器(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 头部的 PE-bear 或 CFF Explorer
- 无网络连接的隔离分析虚拟机
工作流
步骤 1:识别加壳器
确定样本是否已加壳,并识别加壳器:
# Check with Detect It Easy
diec suspect.exe
# Check with UPX (test without unpacking)
upx -t suspect.exe
# Python-based entropy and packer detection
python3 << 'PYEOF'
import pefile
import math
pe = pefile.PE("suspect.exe")
print("Section Analysis:")
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: {entropy:.2f} Raw: {raw:>8} Virtual: {virtual:>8}")
# Check for UPX section names
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 section names detected")
elif '.upx' in [s.lower() for s in section_names]:
print("\n[!] UPX variant section names detected")
# Check import count (packed binaries have very few)
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
total_imports = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT)
print(f"\nTotal imports: {total_imports}")
if total_imports < 10:
print("[!] Very few imports - likely packed")
else:
print("\n[!] No import directory - heavily packed")
PYEOF
步骤 2:尝试标准 UPX 脱壳
尝试使用 UPX 内置的脱壳功能:
# Standard UPX decompress
upx -d suspect.exe -o unpacked.exe
# If UPX fails with "not packed by UPX" error, the headers may be modified
# Verbose output for debugging
upx -d suspect.exe -o unpacked.exe -v 2>&1
# Verify the unpacked file
file unpacked.exe
diec unpacked.exe
步骤 3:修复被修改的 UPX 头部
如果标准脱壳失败,修复被篡改的魔数字节:
# Repair modified UPX headers
import struct
with open("suspect.exe", "rb") as f:
data = bytearray(f.read())
# UPX magic bytes: "UPX!" (0x55505821)
# Malware authors commonly modify these to prevent automatic unpacking
# Search for modified UPX signatures
upx_magic = b"UPX!"
modified_patterns = [b"UPX0", b"UPX\x00", b"\x00PX!", b"UPx!"]
# Find and restore section names
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 offset: 0x{pe_offset:X}")
print(f"Number of sections: {num_sections}")
print(f"Section table offset: 0x{section_table_offset:X}")
for i in range(num_sections):
offset = section_table_offset + (i * 40)
name = data[offset:offset+8]
print(f"Section {i}: {name}")
# Restore UPX magic bytes in the binary
# Search for the UPX header signature location (typically near the end of packed data)
for i in range(len(data) - 4):
if data[i:i+3] == b"UPX" and data[i+3] != ord("!"):
print(f"Found modified UPX magic at offset 0x{i:X}: {data[i:i+4]}")
data[i:i+4] = b"UPX!"
print(f"Restored to: UPX!")
# Also restore section names if modified
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 # Already correct
# Check for common modifications
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"Restored section name at 0x{offset:X} to {original}")
with open("suspect_fixed.exe", "wb") as f:
f.write(data)
print("\nFixed file written. Retry: upx -d suspect_fixed.exe -o unpacked.exe")
步骤 4:使用调试器手动脱壳
当自动化脱壳完全失败时,使用动态脱壳:
Manual UPX Unpacking with x64dbg:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Load packed sample in x64dbg
2. Run to the entry point (system breakpoint then F9)
3. UPX unpacking stub pattern:
a. PUSHAD (saves all registers)
b. Decompression loop (processes packed sections)
c. Resolves imports (LoadLibrary/GetProcAddress calls)
d. POPAD (restores registers)
e. JMP to OEP (original entry point)
4. Set hardware breakpoint on ESP after PUSHAD:
- After PUSHAD, right-click ESP in registers -> Follow in Dump
- Set hardware breakpoint on access at [ESP] address
- Run (F9) - breaks at POPAD before JMP to OEP
5. Step forward (F7/F8) until you reach the JMP to OEP
6. At OEP: Use Scylla plugin to dump and fix imports:
- Plugins -> Scylla -> OEP = current EIP
- Click "IAT Autosearch" -> "Get Imports"
- Click "Dump" to save unpacked binary
- Click "Fix Dump" to repair import table
步骤 5:验证脱壳后的二进制文件
验证脱壳后的样本是否有效且完整:
# Verify unpacked PE is valid
python3 << 'PYEOF'
import pefile
pe = pefile.PE("unpacked.exe")
# Check sections are normal
print("Unpacked Section Analysis:")
for section in pe.sections:
name = section.Name.decode().rstrip('\x00')
entropy = section.get_entropy()
print(f" {name:8s} Entropy: {entropy:.2f}")
# Verify imports are resolved
print(f"\nImport count:")
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} functions")
total = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT)
print(f" Total: {total} imports")
# Compare file sizes
import os
packed_size = os.path.getsize("suspect.exe")
unpacked_size = os.path.getsize("unpacked.exe")
print(f"\nPacked: {packed_size:>10} bytes")
print(f"Unpacked: {unpacked_size:>10} bytes")
print(f"Ratio: {unpacked_size/packed_size:.1f}x")
PYEOF
核心概念
| 术语 | 定义 |
|---|---|
| 加壳(Packing) | 压缩或加密可执行代码,以减小文件大小并阻碍静态分析;二进制文件中包含一个脱壳存根,在运行时恢复代码 |
| UPX | Ultimate Packer for eXecutables;开源的加壳器,因其免费且有效而常被恶意软件作者滥用 |
| 原始入口点(OEP) | 加壳前恶意软件代码的真实起始地址;脱壳存根解压代码后跳转到 OEP |
| 导入重建 | 使用 Scylla 或 ImpRec 等工具从内存中转储脱壳进程后,重建导入地址表的过程 |
| PUSHAD/POPAD | x86 指令,用于保存/恢复所有通用寄存器;UPX 使用此模式在脱壳期间保留寄存器状态 |
| 区段熵 | PE 区段数据的随机性度量;加壳区段的熵 > 7.0,而正常代码区段的平均值为 5.0-6.5 |
| 魔数字节 | 文件中标识其格式的签名字节;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 魔数字节,以阻止自动解压。
方法:
- 在十六进制编辑器中打开该二进制文件,搜索 UPX 头区域(通常位于压缩数据的末尾)
- 识别被修改的魔数字节(例如 “UPX!” 被改为 “UPX\x00” 或完全置零)
- 使用 Python 修复脚本恢复 “UPX!” 魔数并修正节区名称
- 对修复后的二进制文件重试
upx -d - 如果修复失败,则回退到使用 x64dbg 进行手动脱壳(PUSHAD -> 在 ESP 上设置硬件断点 -> POPAD -> JMP OEP)
- 验证脱壳后的二进制文件具有正确的导入表和合理的熵值
- 导入 Ghidra 或 IDA 进行完整的静态分析
陷阱:
- 假设 UPX 是唯一的加壳工具;该二进制文件可能是双重加壳(UPX + 自定义层)
- 修改原始加壳样本,而不是在副本上操作
- 手动内存转储后未重建导入表(未修复 IAT 的转储二进制文件会崩溃)
- 忘记检查 UPX 加壳的 PE 节区之后追加的覆盖数据
输出格式
UNPACKING ANALYSIS REPORT
===========================
Sample: suspect.exe
SHA-256: e3b0c44298fc1c149afbf4c8996fb924...
Packer: UPX 3.96 (modified headers)
PACKED BINARY
Sections: UPX0 (entropy: 0.00) UPX1 (entropy: 7.89) .rsrc (entropy: 3.45)
Imports: 2 (kernel32.dll: LoadLibraryA, GetProcAddress)
File Size: 98,304 bytes
UNPACKING METHOD
Method: Header repair + UPX -d
Header Fix: Restored UPX! magic at offset 0x1F000
Command: upx -d suspect_fixed.exe -o unpacked.exe
Result: SUCCESS
UNPACKED BINARY
Sections: .text (entropy: 6.21) .rdata (entropy: 4.56) .data (entropy: 3.12) .rsrc (entropy: 3.45)
Imports: 147 (kernel32, user32, advapi32, wininet, ws2_32)
File Size: 245,760 bytes (2.5x expansion)
OEP: 0x00401000
VALIDATION
PE Valid: Yes
Imports Resolved: Yes (147 functions across 8 DLLs)
Executable: Yes (runs without crash in sandbox)
NEXT STEPS
- Import unpacked.exe into Ghidra for full disassembly
- Run YARA rules against unpacked binary
- Submit unpacked binary to VirusTotal for improved detection
评论
支持 Markdown