mcpField
A
analyzing-packed-malware-with-upx-unpacker

analyzing-packed-malware-with-upx-unpacker

github.com/mukul975安全35.2k304 次安装8 周前更新
代码仓库

关于 analyzing-packed-malware-with-upx-unpacker

用于识别 UPX 加壳可执行文件并还原二进制文件的安全分析技能,供恶意软件分析人员使用。涵盖标准解压、修复篡改的文件头,以及自动方法失效时通过调试器手动脱壳。

主要功能

  1. 01识别 UPX 加壳
  2. 02使用 UPX 解压
  3. 03修复篡改的文件头
  4. 04使用调试器手动脱壳
  5. 05验证还原后的二进制文件

适用场景

  1. 01还原二进制文件以供 Ghidra 或 IDA 分析
  2. 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 外,安装时一起下载的参考资料和脚本。

LICENSE
references/
api-reference.md
scripts/
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)压缩或加密可执行代码,以减小文件大小并阻碍静态分析;二进制文件中包含一个脱壳存根,在运行时恢复代码
UPXUltimate Packer for eXecutables;开源的加壳器,因其免费且有效而常被恶意软件作者滥用
原始入口点(OEP)加壳前恶意软件代码的真实起始地址;脱壳存根解压代码后跳转到 OEP
导入重建使用 Scylla 或 ImpRec 等工具从内存中转储脱壳进程后,重建导入地址表的过程
PUSHAD/POPADx86 指令,用于保存/恢复所有通用寄存器;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 魔数字节,以阻止自动解压。

方法:

  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 节区之后追加的覆盖数据

输出格式

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