Anthropic-Cybersecurity-Skills analyzing-packed-malware-with-upx-unpacker
'Identifies and unpacks UPX-packed and other packed malware samples to expose the original executable code for
install
source · Clone the upstream repo
git clone https://github.com/mukul975/Anthropic-Cybersecurity-Skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/mukul975/Anthropic-Cybersecurity-Skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/analyzing-packed-malware-with-upx-unpacker" ~/.claude/skills/mukul975-anthropic-cybersecurity-skills-analyzing-packed-malware-with-upx-unpack && rm -rf "$T"
manifest:
skills/analyzing-packed-malware-with-upx-unpacker/SKILL.mdsource content
Analyzing Packed Malware with UPX Unpacker
When to Use
- Static analysis reveals high entropy sections and minimal imports indicating the binary is packed
- PEiD, Detect It Easy, or PEStudio identifies UPX or another known packer
- The import table contains only LoadLibrary and GetProcAddress (runtime import resolution typical of packed binaries)
- You need to recover the original binary for proper disassembly and decompilation in Ghidra or IDA
- Automated UPX decompression fails because the malware author modified UPX magic bytes or headers
Do not use when dealing with custom packers, VM-based protectors (Themida, VMProtect), or samples where dynamic unpacking via debugging is more appropriate.
Prerequisites
- UPX (Ultimate Packer for eXecutables) installed (
or download from https://upx.github.io/)apt install upx-ucl - Detect It Easy (DIE) for packer identification
- Python 3.8+ with
library for manual header repairpefile - x64dbg or x32dbg for manual unpacking when automated tools fail
- PE-bear or CFF Explorer for PE header inspection and repair
- Isolated analysis VM without network connectivity
Workflow
Step 1: Identify the Packer
Determine if the sample is packed and identify the packer:
# 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
Step 2: Attempt Standard UPX Decompression
Try the built-in UPX decompression:
# 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
Step 3: Repair Modified UPX Headers
If standard decompression fails, repair tampered magic bytes:
# 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")
Step 4: Manual Unpacking with Debugger
When automated unpacking fails entirely, use dynamic unpacking:
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
Step 5: Validate Unpacked Binary
Verify the unpacked sample is valid and complete:
# 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
Key Concepts
| Term | Definition |
|---|---|
| Packing | Compressing or encrypting executable code to reduce file size and hinder static analysis; the binary contains an unpacking stub that restores code at runtime |
| UPX | Ultimate Packer for eXecutables; open-source executable packer commonly abused by malware authors because it is free and effective |
| Original Entry Point (OEP) | The real starting address of the malware code before packing; the unpacking stub decompresses code then jumps to the OEP |
| Import Reconstruction | Process of rebuilding the import address table after dumping an unpacked process from memory using tools like Scylla or ImpRec |
| PUSHAD/POPAD | x86 instructions that save/restore all general-purpose registers; UPX uses this pattern to preserve register state during unpacking |
| Section Entropy | Randomness measure of PE section data; packed sections show entropy > 7.0 while normal code sections average 5.0-6.5 |
| Magic Bytes | Signature bytes within a file identifying its format; UPX uses "UPX!" which malware authors modify to prevent automated decompression |
Tools & Systems
- UPX: Open-source executable packer with built-in decompression capability for properly packed files
- Detect It Easy (DIE): Packer, compiler, and linker detection tool that identifies protection on PE, ELF, and Mach-O files
- x64dbg/x32dbg: Open-source Windows debugger used for manual unpacking through dynamic execution and breakpoint-based OEP finding
- Scylla: Import reconstruction tool integrated with x64dbg for rebuilding IAT after memory dumping
- PE-bear: PE file viewer and editor for inspecting and repairing PE headers after unpacking
Common Scenarios
Scenario: Unpacking Malware with Modified UPX Headers
Context: A malware sample is identified as UPX-packed by section names (UPX0, UPX1) but
upx -d fails with "CantUnpackException: header corrupted". The malware author modified the UPX magic bytes to prevent automated decompression.
Approach:
- Open the binary in a hex editor and search for the UPX header area (typically at the end of packed data)
- Identify the modified magic bytes (e.g., "UPX!" changed to "UPX\x00" or completely zeroed)
- Use the Python repair script to restore "UPX!" magic and correct section names
- Retry
on the repaired binaryupx -d - If repair fails, fall back to manual unpacking with x64dbg (PUSHAD -> hardware BP on ESP -> POPAD -> JMP OEP)
- Validate the unpacked binary has proper imports and reasonable entropy values
- Import into Ghidra or IDA for full static analysis
Pitfalls:
- Assuming UPX is the only packer; the binary may be double-packed (UPX + custom layer)
- Modifying the original packed sample instead of working on a copy
- Not reconstructing imports after manual memory dump (the dumped binary will crash without IAT fix)
- Forgetting to check for overlay data appended after the UPX-packed PE sections
Output Format
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