Malware Analysis — Part 5: Static Analysis Part 1 — File Identification and PE Structure
A deep dive into static analysis — examining malware without executing it. Covers the Windows PE file format in detail, what headers and sections reveal, entropy analysis, packer detection, and reading import tables like a story.
This series covers the analysis of malicious software in controlled, isolated environments. All techniques described are for educational and defensive security purposes only. The author accepts no responsibility for damage to hardware, software, data, or systems resulting from following this guide.
Previous: Part 4 — Safe Sample Acquisition, Triage, and Permissions
What Is Static Analysis?
Static analysis is the examination of a file's contents without executing it. You are reading the file — its structure, its code, its data — and drawing conclusions about its behavior from what you observe.
This might sound limiting. How can you know what a program does without running it? The answer is that programs are written in code, and code reveals intent. The Windows APIs a program imports tell you what it's capable of doing. The strings embedded in its binary often include C2 server addresses, file paths, registry keys, and error messages. The structure of its sections tells you whether it's been packed to hide its contents.
Static analysis has several important advantages over dynamic analysis:
Safety first. You cannot accidentally trigger malware behavior while reading a file. Static analysis is always safe to perform, even on samples you haven't yet characterized.
Works on broken samples. Dynamic analysis requires a sample to execute successfully. Static analysis works on incomplete samples, samples designed for a different OS, samples that have been corrupted or modified, and samples that won't run in a VM.
Reveals design, not just behavior. Dynamic analysis shows you what the malware does in a specific environment at a specific moment. Static analysis reveals the code itself — including code paths that might never execute in your test environment (backup C2 servers, time-triggered behaviors, conditional logic based on system configuration).
The limitation: Modern malware almost always uses packing, encryption, or obfuscation to defeat static analysis. When you open a packed sample in a disassembler, you see the unpacker stub — not the actual malicious code, which is encrypted on disk. This is why Part 7 (dynamic analysis) and Part 10 (reverse engineering) follow this part — each technique complements the others.
The Windows PE File Format
The Portable Executable (PE) format is the file format used by all
Windows executable files — .exe, .dll, .sys, .scr, and others.
Understanding PE structure is fundamental to malware analysis because
the structure tells you an enormous amount about what a file does before
you've run a single tool.
Think of a PE file like a book. The table of contents (the headers) tells you where everything is. The chapters (the sections) contain the actual content. Some chapters are code, some are data, some are resources like images and strings.
The PE File Layout
┌─────────────────────────────────────────┐
│ DOS Header (64 bytes) │ ← "MZ" magic bytes
├─────────────────────────────────────────┤
│ DOS Stub Program │ ← "This program cannot be
├─────────────────────────────────────────┤ run in DOS mode" message
│ PE Signature ("PE\0\0") │ ← Marks start of PE format
├─────────────────────────────────────────┤
│ COFF File Header (20 bytes) │ ← Machine type, timestamps,
├─────────────────────────────────────────┤ number of sections
│ Optional Header │ ← Entry point, image base,
├─────────────────────────────────────────┤ import/export directory
│ Section Table │ ← Table of contents for sections
├─────────────────────────────────────────┤
│ .text (code section) │ ← Machine code instructions
├─────────────────────────────────────────┤
│ .data (initialized data) │ ← Global variables, constants
├─────────────────────────────────────────┤
│ .rdata (read-only data) │ ← Strings, import/export tables
├─────────────────────────────────────────┤
│ .rsrc (resources) │ ← Icons, dialogs, embedded files
├─────────────────────────────────────────┤
│ Other sections... │ ← .bss, .tls, .reloc, etc.
└─────────────────────────────────────────┘The DOS Header and MZ Magic Bytes
Every PE file begins with the same two bytes: 4D 5A in hexadecimal,
which is MZ in ASCII. This is called the magic number — a signature
that identifies the file type. "MZ" are the initials of Mark Zbikowski,
one of the original MS-DOS developers.
When you open a file in a hex editor and the first two bytes are MZ,
you're looking at a Windows executable regardless of its extension. A
file named document.pdf with MZ at the start is actually an executable
masquerading as a PDF.
The DOS header also contains a critical field: e_lfanew, which is a
pointer (offset from the start of the file) to where the PE signature
begins. Follow this pointer and you find 50 45 00 00 — the bytes "PE"
followed by two null bytes — which marks the start of the actual PE
structure.
The COFF File Header
The COFF (Common Object File Format) header immediately follows the PE signature and contains:
Machine type: What CPU architecture this code was compiled for.
0x014C= x86 (32-bit Intel/AMD)0x8664= x86-64 (64-bit Intel/AMD)0xAA64= ARM64
Number of sections: How many sections the file contains. Most normal programs have 5–8 sections. Packed malware often has very few (2–3) with unusual names.
Time/Date Stamp: When the executable was compiled, as a Unix timestamp. This is easily faked by malware authors — don't trust it as evidence of when the malware was created. A timestamp of 1970-01-01 (Unix epoch zero) or a date far in the future is a red flag suggesting it was deliberately manipulated.
Characteristics: Flags describing the file type.
0x0002= Executable image0x2000= DLL0x0020= Large address aware (can use >2GB of memory space)
The Optional Header
Despite its name, the Optional Header is required in PE executables. It contains critical information for the Windows loader:
AddressOfEntryPoint: The address where execution begins — where the first instruction of the program is located. For normal programs this points to some initialization code. For packed malware this points to the unpacker stub — the first code that runs, which decrypts the real malicious payload.
ImageBase: The preferred load address — where the program wants to
be loaded in memory. Traditionally 0x00400000 for 32-bit executables
and 0x0000000140000000 for 64-bit. Modern Windows uses ASLR (Address
Space Layout Randomization) to randomize this, but the ImageBase field
tells you what the developer intended.
SectionAlignment and FileAlignment: How sections are aligned in memory and on disk. Unusual values can indicate manual PE construction (common in malware that builds its own PE structure) or specific packing techniques.
DataDirectory: An array of 16 entries pointing to important data structures within the file:
- Entry 0: Export Table (functions this file exports — important for DLLs)
- Entry 1: Import Table (functions this file imports — key for analysis)
- Entry 2: Resource Table (embedded resources)
- Entry 5: Base Relocation Table
- Entry 12: Import Address Table (IAT)
The Import Table — Reading Malware's Intentions
The Import Table is arguably the single most valuable part of a PE
file for initial static analysis. It lists every Windows API function
the program calls by name. Because Windows API function names are
descriptive (CreateFile, WriteProcessMemory, RegSetValueEx), the
import table essentially describes what the program does before you've
read a single line of code.
How Imports Work
When Windows loads a program, it reads the import table and finds entries like:
kernel32.dll:
- CreateFileA
- WriteFile
- ReadFile
- CloseHandle
ws2_32.dll:
- WSAStartup
- connect
- send
- recv
Windows then locates these functions in the listed DLLs and fills in the
Import Address Table (IAT) with the actual memory addresses. When the
program calls CreateFile, it's actually calling the address that was
filled into the IAT at load time.
Reading the Import Table Like a Story
Here's how to read imports to understand what a program does.
Kernel32.dll imports — Core Windows operations:
| Import | What it suggests |
|---|---|
CreateFileA/W | Creates or opens files — file manipulation |
WriteFile | Writes data to files — file dropping, log writing |
DeleteFileA/W | Deletes files — cleanup, self-deletion |
CopyFileA/W | Copies files — spreading to new locations |
FindFirstFileA/W | Searches for files — filesystem enumeration |
CreateProcessA/W | Creates new processes — spawning child processes, executing payloads |
OpenProcess | Opens a handle to another process — prerequisite for process injection |
VirtualAllocEx | Allocates memory in another process — process injection |
WriteProcessMemory | Writes to another process's memory — process injection |
CreateRemoteThread | Creates a thread in another process — completing process injection |
LoadLibraryA/W | Loads a DLL — dynamic loading, evasion |
GetProcAddress | Resolves a function by name at runtime — API hiding |
CreateMutexA/W | Creates a mutex — single-instance check, a classic persistence/coordination technique |
GetTempPathA/W | Gets the temp directory path — where malware often drops files |
CreateServiceA/W | Creates a Windows service — persistence mechanism |
RegOpenKeyExA/W | Opens a registry key — reading configuration or persistence |
RegSetValueExA/W | Sets a registry value — writing persistence, changing configuration |
WS2_32.dll imports — Network operations:
| Import | What it suggests |
|---|---|
WSAStartup | Initializes network operations — any network communication |
socket | Creates a network socket |
connect | Connects to a remote host — outbound C2 connection |
send | Sends data — exfiltration or C2 communication |
recv | Receives data — receiving commands from C2 |
gethostbyname | DNS resolution — resolving C2 domain |
bind / listen / accept | Listening for connections — backdoor server |
Advapi32.dll imports — Security and registry:
| Import | What it suggests |
|---|---|
RegCreateKeyExA/W | Creates registry keys — persistence |
AdjustTokenPrivileges | Modifies process privileges — privilege escalation |
OpenSCManagerA/W | Opens Service Control Manager — service manipulation |
LookupPrivilegeValueA/W | Looks up privilege constants — privilege escalation |
CryptEncrypt / CryptDecrypt | Cryptographic operations — data encryption/decryption |
A classic process injection import set: If you see this combination, you're almost certainly looking at process injection malware:
OpenProcess
VirtualAllocEx
WriteProcessMemory
CreateRemoteThread
This is the standard four-function pattern for injecting code into another process — it appears in almost every RAT, banking trojan, and advanced malware sample.
Import Hiding — Why Packed Samples Have Few Imports
A packed sample often shows almost no imports — just the handful needed to unpack itself:
kernel32.dll:
- LoadLibraryA
- GetProcAddress
- VirtualAlloc
With just these three functions, the unpacker can:
- Allocate new memory (
VirtualAlloc) - Decrypt the real payload into that memory
- Load any DLL it needs (
LoadLibraryA) - Find any function it needs by name (
GetProcAddress)
The real import table is hidden inside the encrypted payload. This is why packed samples require unpacking before import analysis is useful.
Section Analysis
PE sections contain the actual content of the executable. Understanding what each section is, what it should contain, and what anomalies look like is a key analysis skill.
Standard Sections
| Section | Normal Contents | Characteristics |
|---|---|---|
.text | Executable code — machine instructions | Execute + Read permissions, low entropy |
.data | Initialized global variables | Read + Write permissions, low entropy |
.rdata | Read-only data — strings, constants, import/export tables | Read-only, low entropy |
.bss | Uninitialized data (zero-filled at load time) | Read + Write, no disk space |
.rsrc | Resources — icons, dialogs, version info, embedded files | Read-only, variable entropy |
.reloc | Base relocation table | Read-only |
.tls | Thread Local Storage data | Variable |
Section Anomalies in Malware
Unusual section names: Legitimate compilers produce sections with
standard names. Malware sometimes uses sections with no name, random
names, or names suggesting packing tools (.UPX0, .packed, etc.).
Unusual permissions: A section that is both writable and executable is highly suspicious. In a normal program, code sections are executable but not writable, and data sections are writable but not executable. A writable+executable section suggests self-modifying code — common in packers and shellcode.
Section size discrepancies: Each section has a Virtual Size (how much space it takes in memory) and a Raw Size (how much space it takes on disk). For a packed section, the Virtual Size is much larger than the Raw Size — because the packed data expands when decompressed in memory.
Entropy Analysis
Entropy is a measure of randomness or unpredictability in data. Shannon entropy is measured from 0 to 8 bits per byte:
| Entropy Range | What It Suggests |
|---|---|
| 0.0 – 1.0 | Highly repetitive data (lots of zero bytes) |
| 3.5 – 5.0 | Normal compiled code — mix of opcodes and data |
| 5.0 – 7.0 | Compressed or encoded data, text, natural language |
| 7.0 – 8.0 | Encrypted or well-compressed data — very random-looking |
High entropy (7.0+) in a PE section is a strong indicator of packing or encryption. When malware authors pack a binary, the encrypted payload section has entropy close to 8.0 because encrypted data looks indistinguishable from random bytes.
A normal Windows executable looks like this in PEStudio:
.text (code): entropy ~5.5–6.5
.rdata (data): entropy ~3.5–5.0
.data: entropy ~2.0–4.0
A packed malware sample might look like:
.text: entropy ~7.8 ← Suspicious
.data: entropy ~7.9 ← Suspicious
Two sections near maximum entropy means most of the file is encrypted — you'll need to unpack it before meaningful static analysis.
Tools for PE Analysis
PEStudio — Your First Tool
PEStudio (winitor.com) is a free static analysis tool specifically designed for PE files. It's the first tool most analysts open with a new sample because it provides a comprehensive overview of a file in a single interface.
Key features:
- Indicators panel: Automatically flags suspicious characteristics — packed indicators, suspicious imports, unusual section names, missing digital signature, imports commonly used by malware
- Import table: Every imported function with annotations indicating which ones are "interesting" from a security perspective
- Strings: Extracted strings with relevance scoring
- Sections: Section names, sizes, entropy, and permissions
- VirusTotal integration: Check the file hash against VirusTotal directly from the tool
- Version information: Product name, company, description (often spoofed by malware)
Workflow with PEStudio:
- Drag the sample file onto PEStudio
- Review the indicators panel first — red/yellow items need attention
- Check the sections for unusual names, entropy, or permissions
- Review the imports — what DLLs and functions are present
- Check strings for obvious URLs, paths, or suspicious content
- Note the compilation timestamp (verify if suspicious)
- Check for a digital signature (presence or absence, validity)
Detect-It-Easy (DIE) — Packer Detection
DIE (github.com/horsicq/Detect-It-Easy) identifies the tools used to create or modify a binary:
- Compiler (Visual C++, GCC, Delphi, Go, Rust)
- Packer (UPX, MPRESS, Themida, ASPack, etc.)
- Protector (VMProtect, Obsidium, Enigma)
- Linker
- Framework (.NET, AutoIT, NSIS installer)
Why it matters: The packer or compiler tells you what analysis approach to take. A UPX-packed binary can be unpacked with a single command. A Themida-protected binary requires a skilled debugger session. A .NET binary is analyzed with dnSpy rather than Ghidra.
Unpacking UPX directly:
UPX is the most common open-source packer. Many malware families use it. If DIE identifies UPX, you can unpack it in seconds:
# On REMnux or any system with UPX installed
upx -d packed_malware.exe -o unpacked_malware.exeNow analyze unpacked_malware.exe — the real import table and strings
are now visible.
PE-bear — Deep Structure Examination
PE-bear (github.com/hasherezade/pe-bear, free) provides the most detailed view of PE structure in a visual interface:
- Full hex view of headers with field-level annotation
- Section viewer with raw and virtual addresses
- Import and export table viewer
- Reconstruction tools for malformed PEs
PE-bear is particularly useful when a PE file has been manually crafted (as is common in shellcode loaders) or when you need to verify specific header fields that look suspicious.
HxD — Hex Editor
HxD (mh-nexus.de/en/hxd) is a free, straightforward hex editor.
Common hex analysis tasks:
- Verify file type from magic bytes (first 4–8 bytes)
- Search for embedded files (look for MZ headers, ZIP signatures, etc. in the middle of a file)
- Find encoded data (repeated byte patterns, blocks of high-entropy bytes)
- Manually examine suspicious bytes flagged by other tools
Professional alternative: 010 Editor (~$100) is the professional standard hex editor with binary templates that auto-parse file formats (PE, ELF, ZIP, etc.). ImHex (free) is a modern open-source alternative with similar template support.
Putting It All Together — A Static Analysis Workflow
Here's how to apply all the concepts in this part systematically:
Phase 1 — File Identification (2 minutes)
1. file sample.exe → Verify file type from magic bytes
2. Get-FileHash -SHA256 → Compute hash
3. VirusTotal hash lookup → Check detection status
4. exiftool sample.exe → Extract metadata
Phase 2 — Packer Detection (2 minutes)
5. Open in DIE → Identify compiler/packer
6. Check section entropy → High entropy = likely packed
7. Count imports → Few imports = likely packed
Decision point: If packed, go to Part 10 (debugging to unpack) before continuing. If not packed, proceed.
Phase 3 — PE Structure Analysis (10 minutes)
8. Open in PEStudio → Review all indicators
9. Examine imports → What DLLs and APIs are used?
10. Review sections → Unusual names, permissions, sizes?
11. Check compilation time → Real timestamp or 1970 epoch?
12. Digital signature? → Present? Valid? From whom?
Phase 4 — Strings Analysis (10 minutes)
13. PEStudio strings tab → Quick scan for obvious IOCs
14. FLOSS (next part) → Extract obfuscated strings
Phase 5 — Document Findings
15. Record all suspicious indicators
16. List all notable imports with what they suggest
17. Record any extracted IOCs (URLs, IPs, file paths)
18. Note whether unpacking is needed
19. Update analysis notes with plan for dynamic analysis
What You've Learned to Read
After completing this part, when you look at a PE file you can answer:
- What architecture is this? (x86, x64, ARM)
- Was it recently compiled? (or is the timestamp suspicious)
- Is it packed? (high entropy, few imports)
- What does it interact with? (network, filesystem, registry, processes)
- Is process injection likely? (VirtualAllocEx, WriteProcessMemory, CreateRemoteThread)
- Does it establish persistence? (registry run keys, service creation)
- Does it communicate on the network? (WS2_32 imports)
- Is it signed? (and by whom — legitimate or stolen certificate)
All of this before running a single tool beyond a hex editor and PEStudio — and without ever executing the malware.
Verification Checklist
- PEStudio installed and opens a sample file
- DIE installed and identifies file types
- HxD installed and you've viewed a file's raw bytes
- PE-bear installed
- You can identify the "MZ" header in a hex editor
- You understand what the import table reveals
- You can identify signs of packing from section entropy
- You've practiced reading imports and identifying suspicious API calls
- You can compute SHA-256 hashes from the command line
Previous: Part 4 — Safe Sample Acquisition, Triage, and Permissions Next: Part 6 — Static Analysis Part 2: Strings, YARA, and IOC Extraction