Difficulty: Intermediate

Module 4: COFF Section Loading

Parsing section headers, allocating memory, and copying raw section data into executable pages.

Why This Module?

Before COFFLoader can resolve symbols or apply relocations, it must load the raw section data into memory. This module covers exactly how COFFLoader parses the COFF file header, locates the section table, allocates memory for each section, and copies the raw data. This is the first phase of the RunCOFF() function.

RunCOFF Entry Point

The main function that orchestrates everything is RunCOFF(). Its signature:

Cint RunCOFF(
    char*          functionname,   // entry function name (e.g., "go")
    unsigned char* coff_data,      // raw COFF file bytes in memory
    uint32_t       filesize,       // size of the COFF data
    unsigned char* argumentdata,   // packed argument buffer (or NULL)
    int            argumentSize    // size of argument buffer
);

The first thing RunCOFF() does is cast the raw buffer to the COFF header structure and extract the critical offsets:

C// Step 1: Parse the COFF file header
coff_file_header_t* coff_header = (coff_file_header_t*)coff_data;

// Step 2: Locate the section table (immediately after the file header)
coff_sect_t* sections = (coff_sect_t*)(coff_data + sizeof(coff_file_header_t));

// Step 3: Locate the symbol table
coff_sym_t* symbols = (coff_sym_t*)(coff_data + coff_header->PointerToSymbolTable);

// Step 4: Locate the string table (immediately after symbol table)
char* string_table = ((char*)symbols) + (coff_header->NumberOfSymbols * sizeof(coff_sym_t));

Pointer Setup in RunCOFF

coff_data
raw buffer start
coff_header
+0 (20 bytes)
sections[]
+20 (40 bytes each)
raw data
variable

Section Memory Allocation

COFFLoader allocates a separate memory region for each section using VirtualAlloc. The key detail: all sections are allocated with PAGE_EXECUTE_READWRITE permissions, regardless of whether they contain code or data.

C// COFFLoader allocates memory for each section
// sectionMapping[] stores the base address of each allocated region
char** sectionMapping = (char**)calloc(
    coff_header->NumberOfSections,
    sizeof(char*)
);

for (int i = 0; i < coff_header->NumberOfSections; i++) {
    sectionMapping[i] = (char*)VirtualAlloc(
        NULL,                         // let OS choose the address
        sections[i].SizeOfRawData,    // size of this section
        MEM_COMMIT | MEM_RESERVE,     // commit the pages immediately
        PAGE_EXECUTE_READWRITE        // RWX permissions
    );

    // Copy the raw section data from the COFF file into the new allocation
    if (sections[i].SizeOfRawData > 0) {
        memcpy(
            sectionMapping[i],
            coff_data + sections[i].PointerToRawData,
            sections[i].SizeOfRawData
        );
    }
}

Why RWX for Everything?

COFFLoader allocates all sections as PAGE_EXECUTE_READWRITE. This is a simplification: ideally, .text would be RX, .rdata would be R, and .data/.bss would be RW. Using RWX for everything avoids the complexity of tracking per-section permissions but creates a larger detection surface. RWX memory regions are uncommon in legitimate applications and are flagged by many EDR heuristics. Production-quality loaders (like bof-launcher) apply proper per-section protections after relocations are applied.

Handling Different Section Types

Different sections require different handling during the loading phase:

SectionSizeOfRawDataPointerToRawDataLoading Behavior
.textNon-zeroNon-zeroCopy raw bytes (compiled machine code)
.dataNon-zero or 0Non-zero or 0Copy initialized data, or allocate zero-filled
.rdataNon-zeroNon-zeroCopy read-only data (string literals, constants)
.bss00Allocate zero-filled memory (VirtualAlloc zero-initializes)
.xdataNon-zeroNon-zeroCopy exception unwind data
.pdataNon-zeroNon-zeroCopy function table entries

The .bss Section

The .bss section is special: it contains uninitialized data (global variables declared without an initial value). It has SizeOfRawData == 0 because there is nothing to store in the file -- all values are zero at startup. The section's VirtualSize field indicates how much memory to allocate. Since VirtualAlloc zero-initializes committed pages, the .bss data is automatically correct after allocation.

C// For .bss sections:
//   sections[i].SizeOfRawData == 0
//   sections[i].Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA
//
// COFFLoader checks: if SizeOfRawData > 0, copy data.
// Otherwise, the VirtualAlloc already provides zero-filled memory.
if (sections[i].SizeOfRawData > 0) {
    memcpy(sectionMapping[i],
           coff_data + sections[i].PointerToRawData,
           sections[i].SizeOfRawData);
}

Section Characteristics Deep Dive

The Characteristics field in each section header is a bitmask that describes the section's properties. COFFLoader defines these flags:

C// Content type flags
#define IMAGE_SCN_CNT_CODE               0x00000020  // contains executable code
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080  // contains uninitialized data

// Memory permission flags
#define IMAGE_SCN_MEM_EXECUTE            0x20000000  // can be executed
#define IMAGE_SCN_MEM_READ               0x40000000  // can be read
#define IMAGE_SCN_MEM_WRITE              0x80000000  // can be written

// Other flags
#define IMAGE_SCN_MEM_DISCARDABLE        0x02000000  // can be discarded after loading
#define IMAGE_SCN_ALIGN_16BYTES          0x00500000  // 16-byte alignment requirement

Determining Section Purpose from Characteristics

C// How to identify section types from characteristics:

// .text section: code + execute + read
// Characteristics = 0x60000020
//   IMAGE_SCN_CNT_CODE (0x20) | IMAGE_SCN_MEM_EXECUTE (0x20000000) | IMAGE_SCN_MEM_READ (0x40000000)

// .data section: initialized + read + write
// Characteristics = 0xC0000040
//   IMAGE_SCN_CNT_INITIALIZED_DATA (0x40) | IMAGE_SCN_MEM_READ (0x40000000) | IMAGE_SCN_MEM_WRITE (0x80000000)

// .rdata section: initialized + read only
// Characteristics = 0x40000040
//   IMAGE_SCN_CNT_INITIALIZED_DATA (0x40) | IMAGE_SCN_MEM_READ (0x40000000)

// .bss section: uninitialized + read + write
// Characteristics = 0xC0000080
//   IMAGE_SCN_CNT_UNINITIALIZED_DATA (0x80) | IMAGE_SCN_MEM_READ (0x40000000) | IMAGE_SCN_MEM_WRITE (0x80000000)

The sectionMapping Array

After loading, the sectionMapping array is the bridge between the COFF file's section indices (0-based) and the actual memory addresses where those sections live. Every subsequent operation (symbol resolution, relocation application) uses this array to convert section references to real pointers.

TEXTsectionMapping[] after loading a typical BOF:

  Index  Section    Base Address          Size
  [0]    .text      0x00007FF8A1230000    0x15C  (348 bytes of code)
  [1]    .rdata     0x00007FF8A1240000    0x02A  (42 bytes of strings)
  [2]    .data      0x00007FF8A1250000    0x010  (16 bytes of globals)
  [3]    .xdata     0x00007FF8A1260000    0x00C  (12 bytes of unwind info)
  [4]    .pdata     0x00007FF8A1270000    0x00C  (12 bytes of func table)

Each address is independent (VirtualAlloc does not guarantee contiguous pages).
Relocations must account for the actual distance between sections.

Why Separate Allocations?

COFFLoader allocates each section independently rather than as one contiguous block. This means sections can be at arbitrary addresses, potentially far apart in the virtual address space. This is important for relocation processing: REL32 relocations encode a 32-bit signed offset, which has a range of +/- 2 GB. If sections are more than 2 GB apart, REL32 relocations will fail. In practice, VirtualAlloc typically returns addresses in the same region, but a production loader should allocate a single contiguous block and partition it into sections to guarantee proximity.

Function Pointer Table Allocation

After loading sections, COFFLoader allocates a function pointer table -- a block of memory that stores resolved addresses for external function call relocations. This is where the __imp_ prefix convention connects: when the BOF code references __imp_KERNEL32$GetProcAddress, it expects to read a function pointer from a known memory location. COFFLoader allocates this table based on the total number of relocations across all sections and fills it during relocation processing (Module 6).

C// Count total relocations across all sections
int totalRelocations = 0;
for (int i = 0; i < coff_header->NumberOfSections; i++) {
    totalRelocations += sections[i].NumberOfRelocations;
}

// Allocate the function pointer table
// One 8-byte slot per relocation (on x64)
char* functionMapping = (char*)VirtualAlloc(
    NULL,
    totalRelocations * sizeof(uint64_t),  // 8 bytes per relocation
    MEM_COMMIT | MEM_RESERVE,
    PAGE_EXECUTE_READWRITE  // needs to be readable for pointer dereference
);

The functionMapping buffer is indexed by a sequential counter that increments each time an external function call relocation is processed. When the N-th external function relocation is resolved to address 0x00007FFA12340000, that address is stored at functionMapping[N * 8]. The relocation is then patched to reference this slot. Note: the index is NOT the symbol table index -- it is a counter that tracks how many external function relocations have been processed so far.

Complete Loading Phase Summary

TEXTSection Loading Phase (RunCOFF steps 1-5):

1. Cast coff_data to coff_file_header_t*
2. Compute pointers: sections[], symbols[], string_table
3. Allocate sectionMapping[NumberOfSections] array
4. For each section i:
   a. VirtualAlloc(NULL, SizeOfRawData, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
   b. if SizeOfRawData > 0: memcpy(sectionMapping[i], coff_data + PointerToRawData, SizeOfRawData)
5. Count total relocations across all sections (relocationCount)
6. Allocate functionMapping[relocationCount * 8] for resolved function pointers

After this phase:
  - All section data is in RWX memory
  - sectionMapping[] maps section index -> memory address
  - functionMapping is allocated but empty (filled during relocation processing in Module 6)
  - No relocations applied yet (code contains placeholder offsets)

Pop Quiz: Section Loading

Q1: What memory protection does COFFLoader use when allocating section memory?

COFFLoader simplifies memory management by allocating all sections with PAGE_EXECUTE_READWRITE. While this works correctly, it creates detectable RWX memory regions. A hardened loader would apply proper per-section protections after relocations are complete.

Q2: How does COFFLoader handle .bss sections that have SizeOfRawData == 0?

VirtualAlloc with MEM_COMMIT always returns zero-initialized pages. Since .bss contains uninitialized (zero) data and SizeOfRawData is 0, COFFLoader simply does not copy anything -- the allocation is already correct. The check "if SizeOfRawData > 0" naturally skips the memcpy for .bss sections.

Q3: What is the purpose of the functionMapping buffer?

The functionMapping buffer is a table of function pointers, one slot per external function call relocation processed. When an external symbol like __imp_KERNEL32$GetProcAddress is resolved during relocation processing, the resolved address is stored in the next available functionMapping slot (indexed by a sequential counter, not by symbol table index). The relocation then points the BOF's code at this table slot so the indirect call reaches the correct function.