Module 3: PE Internals & Custom Sections
How FunctionPeekaboo embeds metadata and initialization code into the PE file using custom sections.
Module Objective
Understand the PE (Portable Executable) file format at the level needed to comprehend FunctionPeekaboo’s use of custom sections: .funcmeta for function metadata and .stub for initialization code. Learn how section alignment, permissions, and the entry point work together, and how modifyEP.py post-processes the binary.
1. PE File Format Overview
Every Windows executable (.exe, .dll) follows the Portable Executable format. The key structures relevant to FunctionPeekaboo are:
TextPE File Layout:
+-----------------------------+
| DOS Header (MZ) | ← Legacy header, contains e_lfanew pointer
+-----------------------------+
| PE Signature ("PE\0\0") | ← Identifies file as PE
+-----------------------------+
| COFF File Header | ← Machine type, number of sections, timestamp
+-----------------------------+
| Optional Header | ← Entry point RVA, image base, section alignment
| - Data Directories | ← Import/export tables, relocations, etc.
+-----------------------------+
| Section Headers | ← Array of IMAGE_SECTION_HEADER structures
| .text | .rdata | .data |
| .funcmeta | .stub | ← FunctionPeekaboo's custom sections
+-----------------------------+
| Section Bodies | ← Actual code, data, metadata bytes
| .text body |
| .rdata body |
| .data body |
| .funcmeta body | ← Function registration metadata
| .stub body | ← Initialization entry point
+-----------------------------+
2. Critical PE Header Fields
Several fields in the PE Optional Header are directly relevant to how FunctionPeekaboo operates:
| Field | Location | Relevance to FunctionPeekaboo |
|---|---|---|
| AddressOfEntryPoint | Optional Header | Modified by modifyEP.py to point to the .stub section instead of the original entry point |
| ImageBase | Optional Header | The handler uses this to locate the PE headers in memory for section traversal |
| SectionAlignment | Optional Header | Determines how sections are aligned in memory (typically 0x1000 = 4096 bytes) |
| FileAlignment | Optional Header | Determines how sections are aligned on disk (typically 0x200 = 512 bytes) |
| SizeOfImage | Optional Header | Total virtual size of the loaded PE; includes all sections |
| NumberOfSections | COFF Header | The handler iterates section headers to find .funcmeta |
3. Section Headers
Each section has an IMAGE_SECTION_HEADER that describes its name, size, location, and permissions:
C// IMAGE_SECTION_HEADER structure (40 bytes each)
typedef struct {
BYTE Name[8]; // Section name (".text", ".funcmeta", etc.)
DWORD VirtualSize; // Size in memory
DWORD VirtualAddress; // RVA (offset from ImageBase) when loaded
DWORD SizeOfRawData; // Size on disk
DWORD PointerToRawData; // File offset to section data
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // Flags: readable, writable, executable, etc.
} IMAGE_SECTION_HEADER;
The Characteristics field is a bitmask of flags that control the section’s memory permissions:
| Flag | Value | Meaning |
|---|---|---|
IMAGE_SCN_MEM_EXECUTE | 0x20000000 | Section contains executable code |
IMAGE_SCN_MEM_READ | 0x40000000 | Section is readable |
IMAGE_SCN_MEM_WRITE | 0x80000000 | Section is writable |
IMAGE_SCN_CNT_CODE | 0x00000020 | Section contains code |
IMAGE_SCN_CNT_INITIALIZED_DATA | 0x00000040 | Section contains initialized data |
4. The .funcmeta Custom Section
FunctionPeekaboo creates a custom PE section called .funcmeta that stores metadata about every registered (peekaboo-attributed) function. This section is the “registry” that the handler consults at runtime to know which regions to encrypt/decrypt.
.funcmeta Section Structure
The section contains an array of metadata entries, one per registered function:
C// Each entry in .funcmeta describes one registered function
typedef struct {
DWORD FunctionRVA; // RVA of the function start (after prologue stub)
DWORD FunctionSize; // Size of the function body in bytes
BYTE XorKey; // Single-byte XOR key for this function
BYTE IsEncrypted; // Current state: 1 = encrypted, 0 = decrypted
WORD Reserved; // Alignment padding
} FUNC_META_ENTRY;
// The section is a flat array:
// [entry_0][entry_1][entry_2]...[entry_N][null terminator]
The section permissions are READ | WRITE (not execute). It contains data only — the handler reads from it to find function locations and writes to it to update the IsEncrypted flag.
How the Handler Finds .funcmeta
At runtime, the handler walks the PE section headers (starting from the DOS header → PE signature → section headers array) and compares each section name against the string .funcmeta. When found, it uses the VirtualAddress field to locate the section’s data in memory. This is a standard PE parsing technique — no imports or API calls are needed.
5. The .stub Custom Section
The .stub section contains the initialization entry point for FunctionPeekaboo. The PE’s AddressOfEntryPoint is modified (by modifyEP.py) to point here instead of the original entry point.
.stub Execution Flow
- Windows loader calls
AddressOfEntryPoint, which now points to.stub - The stub initialization code runs: sets up TEB fields, XOR-encrypts all registered function bodies for the first time
- After initialization, the stub jumps to the original entry point (which was saved during post-processing)
- The program starts normally — but now every registered function is encrypted at rest
The .stub section has READ | EXECUTE permissions (it contains code). It is small — typically a few hundred bytes — containing just the initialization logic and a jump to the original entry point.
x86 Assembly; Simplified .stub pseudocode
_stub_entry:
; Save all registers (we're hijacking the entry point)
pushfq
push rax
push rcx
push rdx
; ... save all volatile registers ...
; Initialize TEB UserReserved fields
mov qword ptr gs:[0x1488], 0 ; UserReserved[0] = NULL (no active function)
mov qword ptr gs:[0x1490], 0 ; UserReserved[1] = flags
; Walk .funcmeta and XOR-encrypt each function body
call _initial_encrypt_all
; Restore all registers
; ... pop all registers ...
popfq
; Jump to original entry point
jmp original_entry_point
6. Section Alignment and Virtual Addresses
When the Windows PE loader maps a binary into memory, sections are aligned to SectionAlignment boundaries (typically 4096 bytes / one page). This means:
Alignment Matters for VirtualProtect
The handler uses VirtualProtect to change memory permissions (RX → RW for decryption, RW → RX after). VirtualProtect operates on page boundaries (4096-byte aligned). If a function spans a page boundary, the handler must protect multiple pages. If two registered functions share the same page, protecting one affects the other — the handler must account for this to avoid corrupting a function that’s currently executing.
The relationship between RVAs and virtual addresses:
C// Converting between RVA and Virtual Address
// RVA = Relative Virtual Address (offset from image base)
// VA = Virtual Address (actual memory address)
VA = ImageBase + RVA
// Example:
// ImageBase = 0x00400000 (loaded PE base)
// Function RVA = 0x00001234 (from .funcmeta entry)
// Function VA = 0x00401234 (actual address in memory)
7. The modifyEP.py Post-Processor
After compilation, the binary has the standard CRT entry point (e.g., mainCRTStartup). FunctionPeekaboo needs to intercept execution before the CRT runs, so it uses a Python script to modify the PE:
What modifyEP.py Does
- Reads the current entry point from the PE Optional Header’s
AddressOfEntryPoint - Finds the .stub section by walking section headers
- Patches the original entry point address into the .stub’s jump target (so .stub can redirect to it after initialization)
- Overwrites
AddressOfEntryPointwith the RVA of the .stub section - Recalculates PE checksums if needed
Python# Simplified modifyEP.py logic
import pefile
def modify_entry_point(pe_path):
pe = pefile.PE(pe_path)
# Save original entry point
original_ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
# Find .stub section
stub_section = None
for section in pe.sections:
if section.Name.rstrip(b'\x00') == b'.stub':
stub_section = section
break
# Write original EP into .stub's reserved field
stub_va = stub_section.VirtualAddress
# The .stub code has a placeholder for the original EP
pe.set_dword_at_rva(stub_va + ORIGINAL_EP_OFFSET, original_ep)
# Set new entry point to .stub
pe.OPTIONAL_HEADER.AddressOfEntryPoint = stub_va
pe.write(pe_path)
pe.close()
Order of Operations
The build pipeline must be: (1) compile with the modified Clang, (2) link normally, (3) run modifyEP.py. If you skip step 3, the binary will start at the normal entry point and no functions will be encrypted — the .stub and .funcmeta sections will exist but be unused.
8. Inspecting Custom Sections
You can verify FunctionPeekaboo’s sections using standard PE analysis tools:
Bash# Using dumpbin (Visual Studio)
dumpbin /headers implant.exe
# Look for:
# SECTION HEADER #5
# .funcmeta name
# ...
# 40000040 flags
# Initialized Data
# Read Write
#
# SECTION HEADER #6
# .stub name
# ...
# 60000020 flags
# Code
# Execute Read
# Using Python pefile
python3 -c "
import pefile
pe = pefile.PE('implant.exe')
for s in pe.sections:
print(f'{s.Name.decode().rstrip(chr(0)):10} VA=0x{s.VirtualAddress:08x} Size=0x{s.Misc_VirtualSize:08x} Chars=0x{s.Characteristics:08x}')
print(f'Entry Point: 0x{pe.OPTIONAL_HEADER.AddressOfEntryPoint:08x}')
"
9. Section Security Implications
| Section | Permissions | Detection Risk |
|---|---|---|
.text | Read + Execute | Normal — every executable has this |
.funcmeta | Read + Write | Unusual section name; EDR could flag binaries with non-standard sections |
.stub | Read + Execute | Unusual name; entry point not in .text is a potential red flag |
Mitigation: Section Name Randomization
A production deployment might rename .funcmeta and .stub to less suspicious names (e.g., .rsrc2, .tls2) or merge them into existing sections. The FunctionPeekaboo PoC uses descriptive names for clarity, but the concept does not depend on specific section names — the handler can search by section characteristics rather than names.
Knowledge Check
Q1: What does the .funcmeta section contain?
Q2: What is the purpose of modifyEP.py?
Q3: Why does VirtualProtect alignment matter for per-function masking?