PE Analysis Tools
The practical toolkit for static PE triage: PE-Bear, PE Studio, pefile, Detect It Easy, and CAPA — what each shows you, how to combine them, and the YARA patterns that catch PE anomalies
An alert fires on a new binary dropped to %APPDATA%\Roaming. You have two minutes before the incident escalates. Your goal: determine if it's malicious before running it in a sandbox. You pull the file, open three tools in parallel, and within 90 seconds have a verdict. This chapter is the workflow that gets you there.
Why Static PE Analysis First
Before dynamic analysis (sandbox, debugger), static PE analysis answers: Is this file packed? What capabilities does it claim? Does anything look spoofed? A 90-second static pass often gives enough signal to triage without touching a sandbox. It also tells you what to look for when you do run it dynamically.
PE-Bear
PE-Bear is a free, cross-platform PE viewer focused on raw structure visualization. It shows every byte of every header with annotation, lets you see mismatches between on-disk and in-memory representations, and highlights anomalies in section permissions, imports, and the TLS directory.
| Feature | What to Check |
|---|---|
| DOS/NT headers | Verify e_lfanew, check if DOS stub has non-standard content |
| Optional Header | ASLR/NX/CFG flags, entry point RVA, ImageBase, subsystem |
| Sections table | Permissions (look for RWX), VirtualSize vs SizeOfRawData gaps, unusual names |
| Imports tab | Which DLLs, which functions; look for the minimal-import pattern |
| Exports tab | DLL name in header vs filename; forwarder chains |
| TLS tab | Callbacks present? How many? EP = 0? |
| Rich Header | Compiler/linker fingerprint (can be faked but useful) |
PE Studio
PE Studio (by Marc Ochsenmeier) is the most widely-used PE triage tool in malware analysis. It combines static header parsing with automated threat intelligence lookups and a built-in indicator scoring system.
Key features for detection:
- Indicators panel: PE Studio automatically flags suspicious imports, strings, and header anomalies with a severity score (red/yellow/gray). A file with 40 red indicators needs immediate investigation.
- Strings: Extracts and classifies strings (URLs, IPs, registry paths, file paths, suspicious keywords) with source indication (which section each string came from).
- VirusTotal integration: Right-click to query hash against VT without uploading the file.
- Signature matching: Built-in signature database identifies common packers, protectors, compilers, and known malware families by PE structure patterns.
- Version info cross-check: Flags when version info claims a Microsoft product but the hash doesn't match any known Microsoft binary.
PE Studio's indicators panel gives you a quick risk score. But don't stop at the score — read the individual indicators to understand why it's flagged. "Imports VirtualAllocEx" alone is medium risk; combined with "imports CreateRemoteThread" and "imports WriteProcessMemory" and "entropy > 7.0 in .text", those together make a near-certain case for malicious intent.
pefile (Python Automation)
pefile is the Python library for programmatic PE parsing. It's what you use when you need to process hundreds of files, build custom detection scripts, or automate PE analysis into a pipeline.
import pefile, sys, math, hashlib
from collections import Counter
def quick_triage(filepath: str) -> dict:
report = {"file": filepath, "risks": [], "info": {}}
try:
pe = pefile.PE(filepath)
except pefile.PEFormatError as e:
report["risks"].append(f"Invalid PE: {e}")
return report
opt = pe.OPTIONAL_HEADER
fhdr = pe.FILE_HEADER
report["info"] = {
"arch": "x64" if fhdr.Machine == 0x8664 else "x86",
"is_dll": bool(fhdr.Characteristics & 0x2000),
"ep_rva": hex(opt.AddressOfEntryPoint),
"aslr": bool(opt.DllCharacteristics & 0x0040),
"nx": bool(opt.DllCharacteristics & 0x0100),
}
# Entry point = 0
if opt.AddressOfEntryPoint == 0:
report["risks"].append("EP=0: TLS callbacks may be sole execution point")
# No ASLR
if not (opt.DllCharacteristics & 0x0040):
report["risks"].append("No DYNAMICBASE: fixed load address")
# Section anomalies
for s in pe.sections:
c = s.Characteristics
name = s.Name.decode("utf-8", errors="replace").strip("\x00")
rwx = (c & 0x20000000) and (c & 0x80000000)
if rwx:
report["risks"].append(f"Section '{name}' is executable+writable (RWX)")
data = s.get_data()
if data:
ent = _entropy(data)
if ent > 7.0:
report["risks"].append(f"Section '{name}' entropy={ent:.2f} (possible packed payload)")
# Minimal import table
if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"):
all_imports = []
for entry in pe.DIRECTORY_ENTRY_IMPORT:
for imp in entry.imports:
if imp.name: all_imports.append(imp.name.decode("utf-8", errors="ignore"))
if len(all_imports) <= 5:
report["risks"].append(f"Only {len(all_imports)} imports — likely dynamically resolves")
if "VirtualAllocEx" in all_imports and "WriteProcessMemory" in all_imports:
report["risks"].append("Process injection triad detected (VirtualAllocEx+WriteProcessMemory)")
else:
report["risks"].append("No import table (shellcode-style PE or reflective loader)")
# TLS callbacks
if hasattr(pe, "DIRECTORY_ENTRY_TLS"):
report["risks"].append("TLS directory present — check for pre-EP execution")
pe.close()
return report
def _entropy(data):
c = Counter(data); n = len(data)
return -sum((v/n)*math.log2(v/n) for v in c.values())
Detect It Easy (DIE)
Detect It Easy (by horsicq) is the best packer/compiler detection tool. It analyzes PE structure and byte patterns to identify:
- Packers: UPX, MPRESS, Themida, VMProtect, ASPack, PECompact
- Compilers: MSVC, GCC, Delphi, AutoIt, NSIS, Go, Rust, .NET
- Protectors: Code virtualizers, anti-debug layers
- Linkers: specific linker versions that can correlate with malware toolkits
DIE uses a script-based detection engine — its signatures are readable JavaScript-like scripts in a db/ directory. You can write custom signatures for your environment. The command-line version (diec) integrates into automated pipelines.
# DIE command-line usage
$ diec suspicious.exe
PE32+: compiler: Microsoft Visual C/C++ (2019-2022)
PE32+: linker: Microsoft Linker (14.31)
# Clean output — legitimate MSVC build
$ diec packed_malware.exe
PE32: packer: UPX(4.0)[NRV,brute]
# UPX-packed — unpack before further analysis
$ diec protected_malware.exe
PE32+: protector: VMProtect(3.x)[-,virtual]
# Virtualized — static analysis of code will be very difficult
# UPX unpacking (when possible):
$ upx -d packed_malware.exe -o unpacked_malware.exe
CAPA — Capability Analysis
CAPA (by Mandiant/Google) is the most powerful free static capability detection tool. It analyzes a PE file and produces a structured report of what the binary can do, mapped to MITRE ATT&CK techniques. CAPA matches against thousands of rules that look for specific API call combinations, string patterns, and byte sequences.
# CAPA example output
$ capa suspicious.exe
+------------------------------------------------------------------------+
| ATT&CK Technique | T1055 - Process Injection |
| Evidence | VirtualAllocEx, WriteProcessMemory, CreateRemoteThread|
+------------------------------------------------------------------------+
| ATT&CK Technique | T1082 - System Information Discovery |
| Evidence | GetSystemInfo, IsWow64Process, RtlGetVersion |
+------------------------------------------------------------------------+
| ATT&CK Technique | T1027 - Obfuscated Files or Information |
| Evidence | High entropy section (.upx1), UPX magic bytes |
+------------------------------------------------------------------------+
# CAPA with JSON output for pipeline integration:
$ capa -j suspicious.exe | python3 -c "
import json, sys
data = json.load(sys.stdin)
for rule_name, match in data['rules'].items():
if 'attack' in match.get('meta', {}):
techniques = [t['id'] for t in match['meta']['attack']]
print(rule_name, '→', techniques)
"
YARA Rules for PE Anomalies
rule PE_RWX_Section {
meta:
description = "PE with a section that is both writable and executable"
severity = "high"
condition:
pe.number_of_sections > 0 and
for any i in (0..pe.number_of_sections - 1): (
(pe.sections[i].characteristics & pe.SECTION_MEM_EXECUTE) != 0 and
(pe.sections[i].characteristics & pe.SECTION_MEM_WRITE) != 0
)
}
rule PE_High_Entropy_Text {
meta:
description = "PE .text section entropy > 7.0 (likely packed)"
condition:
pe.sections[0].name == ".text" and
math.entropy(pe.sections[0].raw_data_offset,
pe.sections[0].raw_data_size) > 7.0
}
rule PE_Minimal_Imports_Loader {
meta:
description = "PE imports only LoadLibrary and GetProcAddress (dynamic loader)"
condition:
pe.number_of_imports == 1 and
pe.imports("kernel32.dll", "LoadLibraryA") and
pe.imports("kernel32.dll", "GetProcAddress") and
not pe.imports("kernel32.dll", "CreateFileA")
}
rule PE_No_ASLR {
meta:
description = "PE without DYNAMICBASE — fixed load address"
condition:
not (pe.dll_characteristics & pe.IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)
}
rule PE_TLS_Callbacks_With_Zero_EP {
meta:
description = "PE has TLS callbacks and AddressOfEntryPoint = 0"
severity = "high"
condition:
pe.entry_point == 0 and
pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_TLS].size > 0
}
90-Second Triage Workflow
Triage Flow: Unknown PE Binary
──────────────────────────────────────────────────────────────────
Step 1 (15s): Hash + VT lookup
sha256sum binary.exe → paste into VirusTotal
Known bad? → escalate immediately
Known clean? → still check anomalies
Step 2 (20s): DIE — packer/compiler check
diec binary.exe
UPX/VMProtect/Themida? → packed, extract first
Compiler matches claimed version info? → note discrepancy
Step 3 (30s): PE Studio — indicators panel
Open PE Studio, check indicators count and severity
Red indicators: entropy, imports, version spoofing, TLS
Strings tab: any URLs/IPs/registry paths?
Step 4 (15s): CAPA — capability map
capa binary.exe
ATT&CK techniques present?
T1055 = injection, T1082 = discovery, T1562 = defense evasion
Step 5 (10s): Verdict
≥3 red indicators OR ATT&CK technique match → malicious/sandbox
≤1 indicator, clean VT, known compiler → low risk
High entropy + no imports → packed, sandbox required
Q & A
Why run both PE Studio and CAPA? Don't they overlap?
They complement each other differently. PE Studio is best at structural anomalies: suspicious permissions, entropy, version spoofing, indicator scoring. It shows you the raw structure and flags what looks wrong about the PE itself. CAPA is best at behavioral capability identification: it pattern-matches across the code to identify what the binary is capable of doing, mapped to ATT&CK. PE Studio might flag "imports VirtualAllocEx" as a red indicator without telling you the full attack pattern; CAPA might recognize the full injection triplet (VirtualAllocEx + WriteProcessMemory + CreateRemoteThread) and map it to T1055.001. In practice, PE Studio gives you a fast risk signal and CAPA gives you the ATT&CK mapping you need for your detection report. They take about 10 seconds each on most files.
When is static analysis insufficient and you must go to dynamic?
Static analysis fails when the binary's behavior is hidden from static inspection: (1) Packing/encryption: if entropy is high and imports are minimal, the real code is decrypted at runtime. You can try to unpack (upx -d works for UPX; custom packers need debugging or emulation). (2) Code virtualization: VMProtect and Themida convert code to a custom virtual machine bytecode. Static analysis tools can't determine what the VM executes. (3) Environment-specific behavior: the binary checks for domain, username, or registry key before activating malicious behavior. In a sandbox it does nothing; on the target system it activates. (4) Network-fetched payload: the binary itself is benign but downloads and executes a stage-2 payload from C2. Static analysis sees no payload. In these cases, dynamic analysis in a monitored environment (Cuckoo, ANY.RUN, Triage) is required, potentially combined with network traffic simulation.