Difficulty: Beginner

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:

FieldLocationRelevance to FunctionPeekaboo
AddressOfEntryPointOptional HeaderModified by modifyEP.py to point to the .stub section instead of the original entry point
ImageBaseOptional HeaderThe handler uses this to locate the PE headers in memory for section traversal
SectionAlignmentOptional HeaderDetermines how sections are aligned in memory (typically 0x1000 = 4096 bytes)
FileAlignmentOptional HeaderDetermines how sections are aligned on disk (typically 0x200 = 512 bytes)
SizeOfImageOptional HeaderTotal virtual size of the loaded PE; includes all sections
NumberOfSectionsCOFF HeaderThe 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:

FlagValueMeaning
IMAGE_SCN_MEM_EXECUTE0x20000000Section contains executable code
IMAGE_SCN_MEM_READ0x40000000Section is readable
IMAGE_SCN_MEM_WRITE0x80000000Section is writable
IMAGE_SCN_CNT_CODE0x00000020Section contains code
IMAGE_SCN_CNT_INITIALIZED_DATA0x00000040Section 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

  1. Windows loader calls AddressOfEntryPoint, which now points to .stub
  2. The stub initialization code runs: sets up TEB fields, XOR-encrypts all registered function bodies for the first time
  3. After initialization, the stub jumps to the original entry point (which was saved during post-processing)
  4. 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

  1. Reads the current entry point from the PE Optional Header’s AddressOfEntryPoint
  2. Finds the .stub section by walking section headers
  3. Patches the original entry point address into the .stub’s jump target (so .stub can redirect to it after initialization)
  4. Overwrites AddressOfEntryPoint with the RVA of the .stub section
  5. 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

SectionPermissionsDetection Risk
.textRead + ExecuteNormal — every executable has this
.funcmetaRead + WriteUnusual section name; EDR could flag binaries with non-standard sections
.stubRead + ExecuteUnusual 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?

A) The handler's executable code
B) Metadata entries describing each registered function (RVA, size, XOR key, state)
C) The original unmodified entry point code
D) Encrypted copies of all function bodies

Q2: What is the purpose of modifyEP.py?

A) It changes the PE entry point to the .stub section so initialization runs before the program starts
B) It encrypts the entire binary for distribution
C) It removes debug symbols from the PE
D) It signs the binary with a code signing certificate

Q3: Why does VirtualProtect alignment matter for per-function masking?

A) Functions must be exactly 4096 bytes
B) Alignment determines the XOR key length
C) VirtualProtect operates on page boundaries, so functions sharing a page can affect each other
D) Alignment is only needed for 32-bit binaries