analyzing-packed-malware-with-upx-unpacker について
UPXでパックされた実行ファイルを特定し、解析用に復元するセキュリティ分析スキルです。標準的な展開、ヘッダー修復、自動処理に失敗した場合のデバッガーによる展開を扱います。
主な機能
- 01UPXパックの特定
- 02UPXによる展開
- 03改変されたヘッダーの修復
- 04デバッガーによる手動展開
- 05復元したバイナリの検証
活用シーン
- 01GhidraやIDAで解析するバイナリの復元
- 02UPXシグネチャが改変されたサンプルへの対応
インストール
コマンドをコピーしてターミナルで実行します。
$
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 アンパッカーを使用したパック済みマルウェアの解析
使用する場面
- 静的解析で高エントロピーのセクションと最小限のインポートが確認され、バイナリがパックされていることが示される場合
- 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)
- 手動でヘッダーを修復するための
pefileライブラリを備えた Python 3.8 以降 - 自動ツールが失敗した場合に手動でアンパックするための x64dbg または x32dbg
- PE ヘッダーを検査および修復するための PE-bear または CFF Explorer
- ネットワークに接続されていない隔離された解析 VM
ワークフロー
ステップ 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
主な概念
| 用語 | 定義 |
|---|---|
| パッキング | ファイルサイズを削減し、静的解析を妨げるために実行可能コードを圧縮または暗号化することです。バイナリには、実行時にコードを復元するアンパック用スタブが含まれています |
| 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:動的実行とブレークポイントを利用した OEP の特定により手動でアンパックするための、オープンソースの Windows デバッガーです
- Scylla:メモリダンプ後に IAT を再構築するためのインポート再構築ツールです。x64dbg に統合されています
- PE-bear:アンパック後に PE ヘッダーを検査および修復するための、PE ファイルビューアー兼エディターです
よくあるシナリオ
シナリオ:変更された UPX ヘッダーを持つマルウェアの展開
コンテキスト:セクション名(UPX0、UPX1)からマルウェアのサンプルが UPX でパックされていると特定されましたが、upx -d は「CantUnpackException: header corrupted」というエラーで失敗します。マルウェアの作成者は、自動解凍を防ぐために UPX のマジックバイトを変更しています。
アプローチ:
- バイナリを 16 進エディターで開き、UPX ヘッダー領域を検索します(通常はパックされたデータの末尾にあります)
- 変更されたマジックバイトを特定します(例:「UPX!」が「UPX\x00」に変更されている、または完全にゼロで埋められている)
- Python の修復スクリプトを使用して、「UPX!」マジックと正しいセクション名を復元します
- 修復したバイナリに対して
upx -dを再実行します - 修復に失敗した場合は、x64dbg で手動展開に切り替えます(PUSHAD → ESP にハードウェア BP を設定 → POPAD → OEP に JMP)
- 展開したバイナリのインポートが適切で、エントロピー値が妥当であることを検証します
- 完全な静的解析を行うため、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 対応