PE File Analysis
Portable Executable analysis is the foundation of Windows binary reverse engineering. Every .exe, .dll, .sys, .ocx, and .scr on Windows follows the PE format. Master PE structure and you can triage unknown binaries in minutes -- determine if they are packed, what they do, what anomalies they exhibit, and whether they warrant deeper analysis.
Triage Workflow
Follow this sequence when analyzing an unknown PE file. Each step builds on the previous one.
Step 1 — Compute Hashes and Basic Identification
Hash the binary first. Check hashes against known databases before spending time on manual analysis.
# SHA256 + MD5 + file type
node ${CLAUDE_PLUGIN_ROOT}/scripts/binary-hasher.js hash <binary>
file <binary>
# Check analysis database for prior work
node ${CLAUDE_PLUGIN_ROOT}/scripts/analysis-tracker.js check <sha256> pe-analysis
Record the hash immediately. Every subsequent finding references this hash.
Step 2 — Header Analysis
Extract PE headers to determine architecture, compile time, entry point, and security features.
# Full header dump with radare2
r2 -qc 'iH' <binary>
r2 -qc 'iI' <binary>
import pefile, time
pe = pefile.PE('<binary>')
# Compile timestamp
ts = pe.FILE_HEADER.TimeDateStamp
print(f"Compile time: {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(ts))} UTC")
print(f"Machine: {hex(pe.FILE_HEADER.Machine)}")
print(f"Entry point: {hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint)}")
print(f"Image base: {hex(pe.OPTIONAL_HEADER.ImageBase)}")
print(f"Subsystem: {pe.OPTIONAL_HEADER.Subsystem}")
# Security features
flags = pe.OPTIONAL_HEADER.DllCharacteristics
print(f"ASLR: {bool(flags & 0x40)}, DEP: {bool(flags & 0x100)}, CFG: {bool(flags & 0x4000)}")
print(f"No SEH: {bool(flags & 0x400)}, Force integrity: {bool(flags & 0x80)}")
Check for anomalies: future timestamps, epoch zero, entry point outside .text, missing ASLR/DEP.
See references/pe-headers.md for complete field reference.
Step 3 — Section Analysis
Sections reveal packing, encryption, and structural manipulation.
r2 -qc 'iS' <binary> # Section table
r2 -qc 'iSS' <binary> # Sections with entropy
for s in pe.sections:
name = s.Name.decode().rstrip('\x00')
entropy = s.get_entropy()
raw = s.SizeOfRawData
virt = s.Misc_VirtualSize
ratio = virt / raw if raw > 0 else float('inf')
chars = s.Characteristics
rwx = f"{'R' if chars & 0x40000000 else '-'}{'W' if chars & 0x80000000 else '-'}{'X' if chars & 0x20000000 else '-'}"
packed = "PACKED" if entropy > 7.0 else "HIGH" if entropy > 6.5 else ""
inflated = "INFLATED" if ratio > 10 else ""
print(f"{name:8s} raw={raw:>8d} virt={virt:>8d} ratio={ratio:>6.1f} entropy={entropy:.2f} {rwx} {packed} {inflated}")
Red flags: entropy > 7.0, VirtualSize >> RawSize, RWX permissions, packer section names (.UPX, .aspack, .themida, .vmp).
See references/section-analysis.md for section deep dive.
Step 4 — Import Analysis
Imports reveal the binary's capabilities. Missing or minimal imports suggest packing or dynamic resolution.
r2 -qc 'ii' <binary>
r2 -qc 'ii~CreateRemote' <binary> # Search for injection APIs
r2 -qc 'ii~Virtual' <binary> # Search for memory manipulation
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
for entry in pe.DIRECTORY_ENTRY_IMPORT:
dll = entry.dll.decode()
funcs = [i.name.decode() if i.name else f"ord#{i.ordinal}" for i in entry.imports]
print(f"{dll} ({len(funcs)} imports): {', '.join(funcs[:5])}{'...' if len(funcs) > 5 else ''}")
else:
print("NO IMPORT TABLE — likely packed or manually resolved")
Few imports (< 5 functions) from kernel32.dll only = strong packing indicator. Look for LoadLibrary + GetProcAddress as the only imports -- this means the binary resolves everything at runtime.
See references/import-export-tables.md for import/export deep dive.
Step 5 — Resource Analysis
Resources can contain embedded executables, encrypted payloads, configuration data, and second-stage droppers.
r2 -qc 'ir' <binary> # List resources
if hasattr(pe, 'DIRECTORY_ENTRY_RESOURCE'):
def walk_resources(entry, level=0):
if hasattr(entry, 'directory'):
for e in entry.directory.entries:
walk_resources(e, level + 1)
elif hasattr(entry, 'data'):
rva = entry.data.struct.OffsetToData
size = entry.data.struct.Size
data = pe.get_data(rva, size)
entropy = pefile.SectionStructure.entropy_H(data)
header = data[:4].hex() if len(data) >= 4 else data.hex()
print(f"{' '*level}Resource at RVA {hex(rva)}: {size} bytes, entropy={entropy:.2f}, magic={header}")
for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries:
walk_resources(entry)
Look for: resources with MZ/PE headers (embedded executables), high-entropy resources (encrypted payloads), unusually large resources, RT_RCDATA entries.
See references/resource-analysis.md for resource deep dive.
Step 6 — Overlay and Appended Data
Data appended after the last PE section is called the overlay. Packed binaries, installers, and droppers use overlays to store payloads.
overlay_offset = pe.get_overlay_data_start_offset()
if overlay_offset:
file_size = os.path.getsize('<binary>')
overlay_size = file_size - overlay_offset
with open('<binary>', 'rb') as f:
f.seek(overlay_offset)
header = f.read(16)
print(f"Overlay at offset {hex(overlay_offset)}: {overlay_size} bytes ({overlay_size/1024:.1f} KB)")
print(f"First 16 bytes: {header.hex()}")
print(f"Overlay is {overlay_size * 100 / file_size:.1f}% of total file size")
else:
print("No overlay data")
Large overlays relative to PE size indicate appended payloads. Check overlay headers for known signatures (PK for ZIP, MZ for embedded PE, 7z/RAR headers).
See references/overlay-analysis.md for overlay extraction and encrypted overlay detection.
Step 7 — Anomaly Assessment
Run a comprehensive anomaly check to flag everything suspicious.
warnings = []
# Timestamp anomalies
ts = pe.FILE_HEADER.TimeDateStamp
if ts == 0: warnings.append("TIMESTAMP: epoch zero (zeroed out)")
if ts > time.time(): warnings.append("TIMESTAMP: future date (forged)")
if ts < 946684800: warnings.append("TIMESTAMP: before 2000 (suspicious)")
# Entry point anomalies
ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
ep_section = None
for s in pe.sections:
if s.VirtualAddress <= ep < s.VirtualAddress + s.Misc_VirtualSize:
ep_section = s.Name.decode().rstrip('\x00')
break
if ep_section and ep_section != '.text':
warnings.append(f"ENTRY POINT: in {ep_section} (expected .text)")
if ep == 0:
warnings.append("ENTRY POINT: zero (DLL or anomalous)")
# Section anomalies
for s in pe.sections:
name = s.Name.decode().rstrip('\x00')
entropy = s.get_entropy()
chars = s.Characteristics
if entropy > 7.0: warnings.append(f"SECTION {name}: entropy {entropy:.2f} (packed/encrypted)")
if (chars & 0x20000000) and (chars & 0x80000000): warnings.append(f"SECTION {name}: RWX permissions")
if s.SizeOfRawData == 0 and s.Misc_VirtualSize > 0: warnings.append(f"SECTION {name}: raw=0, virt>0 (runtime unpacking)")
# Checksum
reported = pe.OPTIONAL_HEADER.CheckSum
actual = pe.generate_checksum()
if reported != 0 and reported != actual:
warnings.append(f"CHECKSUM: mismatch (reported={hex(reported)}, actual={hex(actual)})")
# Security features missing
flags = pe.OPTIONAL_HEADER.DllCharacteristics
if not (flags & 0x40): warnings.append("SECURITY: ASLR disabled")
if not (flags & 0x100): warnings.append("SECURITY: DEP/NX disabled")
# Import anomalies
if not hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
warnings.append("IMPORTS: no import table (packed or manual resolution)")
elif len(pe.DIRECTORY_ENTRY_IMPORT)