mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-20 14:30:59 +03:00
300 lines
12 KiB
Markdown
300 lines
12 KiB
Markdown
---
|
|
name: analyzing-packed-malware-with-upx-unpacker
|
|
description: >
|
|
Identifies and unpacks UPX-packed and other packed malware samples to expose the original
|
|
executable code for static analysis. Covers both standard UPX unpacking and handling
|
|
modified UPX headers that prevent automated decompression. Activates for requests involving
|
|
malware unpacking, UPX decompression, packer removal, or preparing packed samples for analysis.
|
|
domain: cybersecurity
|
|
subdomain: malware-analysis
|
|
tags: [malware, unpacking, UPX, packing, static-analysis]
|
|
version: 1.0.0
|
|
author: mahipal
|
|
license: MIT
|
|
---
|
|
|
|
# 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 (`apt install upx-ucl` or download from https://upx.github.io/)
|
|
- Detect It Easy (DIE) for packer identification
|
|
- Python 3.8+ with `pefile` library for manual header repair
|
|
- 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:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```bash
|
|
# 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**:
|
|
1. Open the binary in a hex editor and search for the UPX header area (typically at the end of packed data)
|
|
2. Identify the modified magic bytes (e.g., "UPX!" changed to "UPX\x00" or completely zeroed)
|
|
3. Use the Python repair script to restore "UPX!" magic and correct section names
|
|
4. Retry `upx -d` on the repaired binary
|
|
5. If repair fails, fall back to manual unpacking with x64dbg (PUSHAD -> hardware BP on ESP -> POPAD -> JMP OEP)
|
|
6. Validate the unpacked binary has proper imports and reasonable entropy values
|
|
7. 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
|
|
```
|