PE Sections
What lives in each PE section: .text, .data, .rdata, .bss, .rsrc, .reloc — their on-disk layout versus their in-memory representation, and how malware abuses section structure to hide payloads
A triage tool reports that a suspicious binary has a .text section with entropy 7.6, a .data section with size 0 on disk but 40 KB in memory, and a section named .ndata with RWX permissions. You know something is wrong — but you need to explain exactly what each of these means to the incident commander asking for a risk assessment. This chapter gives you those answers.
The Standard Section Lineup
A normal compiler-built Windows executable has a predictable set of sections. Understanding the expected layout lets you quickly identify anything anomalous.
| Section | Permissions | Contains | Normal Entropy |
|---|---|---|---|
.text |
Exec + Read | Compiled machine code — the actual program instructions | 5.5 – 6.5 |
.data |
Read + Write | Initialized global/static variables with non-zero values | 2.0 – 5.0 |
.rdata |
Read only | Read-only data: string literals, constants, import/export tables | 3.5 – 5.5 |
.bss |
Read + Write | Uninitialized globals (zero-initialized). Often VirtualSize >> SizeOfRawData (no disk content needed) | N/A (no data) |
.rsrc |
Read only | Resources: icons, version info, manifest, embedded files, dialogs | Variable (depends on content) |
.reloc |
Read only | Base relocation table for ASLR/rebasing | 4.0 – 5.5 |
.pdata |
Read only | Exception handler table (x64 SEH/unwind metadata) | 3.0 – 5.0 |
.tls |
Read + Write | Thread-local storage data and callbacks | Low |
The .text Section (Code)
The .text section holds the compiled machine code of the program. Its permissions are executable + readable, but not writable — legitimate code sections should never need to be modified at runtime. On x64 Windows, the entry point specified in OptionalHeader.AddressOfEntryPoint points into .text.
.text section layout (typical x64 PE) ──────────────────────────────────────────────────────────── VirtualAddress: 0x1000 (RVA where .text maps in memory) VirtualSize: 0x42A30 (actual code size) SizeOfRawData: 0x42C00 (file size, padded to FileAlignment) PointerToRawData: 0x400 (file offset on disk) Characteristics: 0x60000020 (exec | read | code) Normal content: 0x1000 Function prologs, call sequences, ret instructions 0x1234 Entry point (AddressOfEntryPoint = 0x1234) ... 0x42A30 Code ends here 0x42A31-0x42BFF Padding zeros (SizeOfRawData - VirtualSize)
Initialized Data: .data
The .data section holds global and static variables that have non-zero initial values. When the process loads, these values are copied from the file into the process's private pages (copy-on-write). Because the section is writable, each process gets its own private copy when the page is modified — the read-only file backing stays shared until first write.
The .bss Extension
The .bss section (Block Started by Symbol) holds zero-initialized globals. On disk, its SizeOfRawData = 0 (no content needed since it's all zeros), but its VirtualSize can be large. The loader creates this memory by allocating pages initialized to zero — no disk space needed. When analyzing a PE and you see a section where VirtualSize >> SizeOfRawData, this pattern is expected for .bss, but suspicious for any other named section.
When a section named .data or something similar has SizeOfRawData = 0 but a large VirtualSize, it could be legitimate (BSS-style zero init) or it could be a packed payload that arrives as zeros on disk and gets populated at runtime by a decryption stub. The distinction: if the section has executable permissions or if it's a non-standard section name, it's suspicious.
Read-Only Data: .rdata
The .rdata section is read-only and holds several distinct types of content that the compiler places together by permission class:
- String literals: hardcoded strings like
"cmd.exe","/c", URLs, registry paths - Constant arrays: lookup tables, vtables (virtual function tables for C++ objects)
- Import/Export tables: the IAT, ILT, and associated name strings (see next chapter)
- Debug directory: PDB path stored here
String extraction from .rdata (and .data) is one of the fastest ways to get intelligence from a PE before even running it. URLs, domain names, registry keys, and error messages in the read-only data section reveal intent.
import pefile
import re
def extract_strings(filepath, min_len=6):
with open(filepath, "rb") as f:
data = f.read()
# ASCII strings
ascii_strings = re.findall(rbb'[\x20-\x7e]{%d,}' % min_len, data)
# Unicode strings (UTF-16LE is common in Windows binaries)
unicode_strings = re.findall(
rbb'(?:[\x20-\x7e]\x00){%d,}' % min_len, data
)
results = [s.decode("ascii") for s in ascii_strings]
results += [s.decode("utf-16-le") for s in unicode_strings]
# Filter for interesting patterns
interesting = []
for s in results:
if re.search(r'https?://', s, re.I): interesting.append(("URL", s))
elif re.search(r'HKEY_|HKLM|HKCU', s): interesting.append(("REGISTRY", s))
elif re.search(r'\\.exe|\\.dll|\\.bat|\\.ps1', s, re.I): interesting.append(("FILE", s))
elif re.search(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', s): interesting.append(("IP", s))
return interesting
Resources: .rsrc
The .rsrc section holds the application's resources — non-code data the program uses at runtime. It's organized as a tree: resource type (icon, dialog, string table, manifest, etc.) → resource ID → language variants → actual data. Common resource types:
| Type ID | Name | Description |
|---|---|---|
| 1 | RT_CURSOR | Cursor images |
| 2 | RT_BITMAP | Bitmap images |
| 3 | RT_ICON | Icon images (executable's icon) |
| 4 | RT_MENU | Menu resources |
| 14 | RT_GROUP_ICON | Group of icons for different sizes |
| 16 | RT_VERSION | Version information (file version, company, description) |
| 24 | RT_MANIFEST | Application manifest (UAC level, DPI awareness, SxS) |
| Custom | Named | Arbitrary embedded data — used by malware to hide payloads |
The RT_VERSION resource contains the file metadata shown in Windows Explorer's properties dialog. Malware frequently copies version resources from legitimate Windows system files (e.g., mimicking svchost.exe's version info) while containing malicious code. Comparing the version info claims with the actual code characteristics is a useful detection technique.
The .rsrc section is commonly used to store encrypted or compressed payloads because: (1) resource data is easily accessible via FindResource() / LoadResource() — no custom file I/O needed, (2) resources are not executable memory, so static analysis tools don't immediately flag them as code, (3) the resource type can be a custom name (e.g., "BINARY", "DATA", "ICON" with wrong format) that camouflages the payload. When you see a large resource with high entropy and no legitimate icon/manifest content, it's almost certainly a hidden payload.
Base Relocations: .reloc
When a PE is loaded at an address different from its preferred ImageBase, absolute virtual addresses hardcoded by the compiler are wrong. The .reloc section contains a list of these addresses so the loader can fix them up (add the delta between preferred and actual load address).
Base Relocation Mechanics ────────────────────────────────────────────────────────────────── Preferred ImageBase: 0x00400000 (what the linker expected) Actual load address: 0x00700000 (where ASLR loaded it) Delta: 0x00300000 .reloc entry says: "fix address at RVA 0x2A18" On-disk value at that location: 0x00402A18 (assumed ImageBase) After relocation: 0x00702A18 (actual load + offset) .reloc structure (IMAGE_BASE_RELOCATION blocks): [VirtualAddress: 0x1000][SizeOfBlock: 0x18] [0x0018 = type:3, offset:0x018] type 3 = HIGHLOW (32-bit fixup) [0x002C = type:3, offset:0x02C] [0x0000 = padding]
An executable without a .reloc section cannot be rebased — it must load at its preferred ImageBase. This means ASLR cannot randomize its base. The DYNAMICBASE DllCharacteristics flag requires a valid .reloc section to work. Malware sometimes deliberately strips the .reloc section to force loading at a fixed address, making it easier to hardcode absolute addresses in shellcode or payloads.
Raw Size vs Virtual Size: The Critical Gap
Every section has two size fields. Understanding their difference is essential for both analysis and exploitation:
| Field | Meaning |
|---|---|
SizeOfRawData |
How many bytes are stored in the file for this section. Aligned to FileAlignment (typically 512 bytes). This is the actual file space consumed. |
VirtualSize |
How many bytes the section occupies in memory after loading. Not aligned — this is the true content size. Can be larger than SizeOfRawData (the remainder is zero-padded in memory) or smaller. |
Common size relationship patterns:
─────────────────────────────────────────────────────────────────
Case 1: VirtualSize ≈ SizeOfRawData (normal code/data section)
.text: Virtual=0x42A30 Raw=0x42C00 — raw is aligned-up virtual
Case 2: VirtualSize > SizeOfRawData (common for packed malware)
A packer puts a tiny stub in SizeOfRawData,
but allocates a large VirtualSize for the unpacked payload
.UPX0: Virtual=0x100000 Raw=0x000 — all zeros on disk!
Case 3: VirtualSize < SizeOfRawData (unusual — suggests overlay data)
Raw size has extra data at end not mapped to memory
This tail data (the "overlay") can hide payloads
Case 4: SizeOfRawData = 0 (BSS-style section, expected for .bss)
.bss: Virtual=0x5000 Raw=0 — memory allocated, no disk data
Malware Section Tricks
UPX Packing (.upx0, .upx1)
UPX is a popular open-source packer. A UPX-packed PE has section names .upx0 and .upx1. The .upx0 section has SizeOfRawData = 0 but a large VirtualSize — this is where the decompressed payload lands at runtime. The .upx1 section holds the compressed payload. Detection: trivial to detect by section names, which is why sophisticated malware uses custom packers with arbitrary names.
Injected Section
A binary patched by adding a new section (e.g., by a crypter tool) will show a non-standard section name, often with RWX permissions. The section contains shellcode or an encrypted payload. The entry point is typically redirected to point into this new section.
Section Name Spoofing
Malware uses names that look legitimate but carry wrong permissions: a section named .text that is writable, or named .data that is executable. The OS doesn't enforce any meaning on section names — they're just labels. The permissions in Characteristics are what actually matter.
Overlay Payloads
Data appended after the last section of a PE file is called an overlay. The loader maps sections but ignores any trailing data. Malware uses overlays to store a second-stage payload, configuration data, or an encrypted DLL. When the total file size exceeds SizeOfHeaders + sum(all sections' SizeOfRawData), there's an overlay.
def find_overlay(filepath):
import pefile, os
pe = pefile.PE(filepath)
last_section = max(pe.sections, key=lambda s: s.PointerToRawData)
last_offset = last_section.PointerToRawData + last_section.SizeOfRawData
file_size = os.path.getsize(filepath)
overlay_size = file_size - last_offset
if overlay_size > 0:
print(f"OVERLAY DETECTED: {overlay_size} bytes at offset {hex(last_offset)}")
with open(filepath, "rb") as f:
f.seek(last_offset)
overlay_data = f.read(min(32, overlay_size))
print(f"Overlay header: {overlay_data.hex()}")
# If overlay starts with 4D5A: it's another PE file
# If it starts with 78 9C or 1F 8B: compressed data (zlib/gzip)
pe.close()
Q & A
Why is the .text section exec+read but never writable?
Code should not modify itself at runtime in a well-behaved executable. Making .text read-only enables two security properties: (1) Data Execution Prevention (DEP/NX): the CPU enforces that writable pages cannot be executable and vice versa. If .text were writable, an attacker who overwrites code could execute their shellcode directly in the code section. (2) Code integrity: a read-only code section cannot be patched in memory by injected code. Some edge cases exist: (1) JIT compilers need writable+executable memory, but they typically use dynamically allocated pages (via VirtualAlloc with PAGE_EXECUTE_READWRITE), not the static .text section. (2) Hot-patching support on older Windows used a read+write+execute .text section, but this is deprecated. Any PE you encounter today with a writable .text section should be considered highly suspicious.
Can a PE have sections in any order? Does the loader care?
By convention, sections are ordered by their VirtualAddress (lowest first), but the PE specification doesn't strictly require this. The loader maps each section to its declared VirtualAddress regardless of section table ordering. However, tools and parsers may behave unexpectedly with out-of-order sections, and the PE spec notes that "sections should be ordered by virtual address." Some packers and protectors deliberately create unusual section orderings to confuse parsing tools. Additionally, sections don't need to be contiguous in the file — a PE with gaps between sections in the file (PointerToRawData values that leave holes) is unusual but loadable. The Windows PE loader maps each section independently using its VirtualAddress and SizeOfRawData. Gap areas in the file are simply not mapped.
What's the difference between .rdata and .data? Can't everything read-only just go in .text?
The separation exists for page permission management. .text is executable+read. .rdata is read-only but not executable. Putting constant data in .text would mean those constants occupy executable pages — wasting permission budget and creating a potential exploit surface. Keeping data and code in separate sections means: (1) Read-only constants in .rdata can be shared across processes (same physical page mapped copy-on-read). (2) Accidental execution of data causes an access violation rather than running garbage instructions. Compilers also put the import table in .rdata because after the loader resolves imports at process start, those function pointers should be read-only. If the IAT is in writable memory, it becomes a target for IAT hooking. Modern compilers can enable /GUARD:CF (Control Flow Guard) which requires the IAT in a non-writable section.