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
raw buffer start
+0 (20 bytes)
+20 (40 bytes each)
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:
| Section | SizeOfRawData | PointerToRawData | Loading Behavior |
|---|---|---|---|
| .text | Non-zero | Non-zero | Copy raw bytes (compiled machine code) |
| .data | Non-zero or 0 | Non-zero or 0 | Copy initialized data, or allocate zero-filled |
| .rdata | Non-zero | Non-zero | Copy read-only data (string literals, constants) |
| .bss | 0 | 0 | Allocate zero-filled memory (VirtualAlloc zero-initializes) |
| .xdata | Non-zero | Non-zero | Copy exception unwind data |
| .pdata | Non-zero | Non-zero | Copy 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?
Q2: How does COFFLoader handle .bss sections that have SizeOfRawData == 0?
Q3: What is the purpose of the functionMapping buffer?