Module 6: Relocation Processing
Patching live code: turning placeholder offsets into valid addresses with ADDR64, REL32, and ADDR32NB fixups.
Why This Module?
After sections are loaded and symbols are resolved, the BOF's machine code still contains placeholder values. A CALL instruction might reference offset 0x00000000 where the real target is at 0x00007FFA1A2B3C4D. Relocation entries tell COFFLoader exactly which bytes to patch and how to compute the correct value. This is the final step before the code becomes executable.
The Relocation Processing Loop
COFFLoader iterates over every section, and for each section, processes its relocation entries. Each relocation identifies a location in the section that needs patching, the target symbol, and the type of fixup to apply:
C// Relocation processing: for each section, apply all relocations
for (int secIdx = 0; secIdx < coff_header->NumberOfSections; secIdx++) {
// Get the relocation table for this section
coff_reloc_t* relocs = (coff_reloc_t*)(
coff_data + sections[secIdx].PointerToRelocations
);
for (int relIdx = 0; relIdx < sections[secIdx].NumberOfRelocations; relIdx++) {
// 1. Where to patch: base of loaded section + VirtualAddress
char* fixupAddress = sectionMapping[secIdx] + relocs[relIdx].VirtualAddress;
// 2. What symbol is referenced
int symIdx = relocs[relIdx].SymbolTableIndex;
// 3. Resolve the symbol to an address (internal, Beacon, or DLL)
void* symbolAddress = resolve_symbol(symIdx, ...);
// 4. Apply the fixup based on relocation Type
apply_relocation(relocs[relIdx].Type, fixupAddress, symbolAddress);
}
}
Resolving the Target Address
The target address depends on the symbol category. COFFLoader determines this during the relocation loop:
C// Determine the target address for a relocation
void* symbolAddress;
int symIdx = relocs[relIdx].SymbolTableIndex;
if (coff_symbol_is_defined(&symbols[symIdx])) {
// Internal symbol: address is section base + symbol value
int targetSection = symbols[symIdx].SectionNumber - 1;
symbolAddress = sectionMapping[targetSection] + symbols[symIdx].Value;
}
else if (/* symbol starts with __imp_ */) {
// External symbol with __imp_ prefix:
// The address points to the functionMapping SLOT (indirect reference)
// functionMappingCount is a sequential counter, NOT the symbol index
symbolAddress = functionMapping + (functionMappingCount * sizeof(uint64_t));
// Store the resolved address in this slot
*(uint64_t*)symbolAddress = (uint64_t)process_symbol(symbolName);
functionMappingCount++; // advance to next slot
// The slot now contains the resolved function pointer
}
else {
// External symbol WITHOUT __imp_ prefix (direct reference):
// Use the resolved address directly from process_symbol()
symbolAddress = process_symbol(symbolName);
}
Internal vs. External: Where the Address Points
For internal symbols, the address points directly into loaded section memory (e.g., a string in .rdata or a helper function in .text). For external __imp_ symbols, the address points to a slot in functionMapping that contains the real address. The BOF code dereferences this slot at runtime via an indirect CALL. This distinction is critical: patching an indirect call target with a direct address (or vice versa) will crash.
AMD64 Relocation Types
COFFLoader handles the following AMD64 relocation types. These are the most common types found in x64 BOFs:
IMAGE_REL_AMD64_ADDR64 (Type 0x0001)
A 64-bit absolute address. The fixup location receives the full 8-byte address of the target symbol. This is used for data pointers, function pointer tables, and any reference that needs a full virtual address.
Ccase IMAGE_REL_AMD64_ADDR64:
// Write the full 64-bit address of the symbol at the fixup location
*(uint64_t*)fixupAddress = (uint64_t)symbolAddress;
break;
TEXTExample: ADDR64 relocation
Before: fixupAddress contains 0x0000000000000000 (placeholder)
Symbol resolves to: 0x00007FFA1A2B3C4D
After: fixupAddress contains 0x00007FFA1A2B3C4D (absolute 64-bit address)
Use case: Global function pointer variable
void (*fnPtr)(void) = SomeFunction;
// The compiler emits an ADDR64 relocation for the initializer
IMAGE_REL_AMD64_ADDR32NB (Type 0x0003)
A 32-bit relative virtual address (RVA) -- the address of the target minus the image base. In COFFLoader's context (no image base), this is used for references that need a 32-bit address without a base adjustment. It is commonly seen in exception handling data (.pdata/.xdata).
Ccase IMAGE_REL_AMD64_ADDR32NB:
// Write a 32-bit address (no base, typically for exception tables)
*(uint32_t*)fixupAddress = (uint32_t)(
(uint64_t)symbolAddress - (uint64_t)sectionMapping[0]
);
break;
IMAGE_REL_AMD64_REL32 (Type 0x0004)
The most common relocation type in x64 code. A 32-bit RIP-relative offset. The x64 instruction set uses RIP-relative addressing extensively. The fixup computes the signed 32-bit distance from the end of the instruction to the target:
Ccase IMAGE_REL_AMD64_REL32:
// 32-bit relative offset: target - (fixup_location + 4)
// The +4 accounts for the 4-byte fixup field itself
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4)
);
break;
TEXTExample: REL32 relocation for a CALL instruction
Instruction: E8 00 00 00 00 (CALL with placeholder offset)
fixupAddress points to the 00 00 00 00 bytes (offset field of CALL)
symbolAddress = 0x00007FF8A1230100 (target function)
fixupAddress = 0x00007FF8A1230050 (location of the offset bytes)
Calculation:
offset = symbolAddress - (fixupAddress + 4)
= 0x00007FF8A1230100 - (0x00007FF8A1230050 + 4)
= 0x00007FF8A1230100 - 0x00007FF8A1230054
= 0xAC
After patching: E8 AC 00 00 00 (CALL +0xAC)
When executed at fixupAddress-1, RIP after fetching = fixupAddress+4,
so target = RIP + 0xAC = fixupAddress + 4 + 0xAC = symbolAddress. Correct!
IMAGE_REL_AMD64_REL32_1 through REL32_5 (Types 0x0005-0x0009)
Variants of REL32 with additional displacement. These handle instructions where the 32-bit relocation field is not at the end of the instruction. The _N suffix means there are N more bytes of instruction after the relocation field:
Ccase IMAGE_REL_AMD64_REL32_1:
// REL32 + 1: the instruction has 1 extra byte after the relocation field
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 1)
);
break;
case IMAGE_REL_AMD64_REL32_2:
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 2)
);
break;
case IMAGE_REL_AMD64_REL32_3:
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 3)
);
break;
case IMAGE_REL_AMD64_REL32_4:
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 4)
);
break;
case IMAGE_REL_AMD64_REL32_5:
*(int32_t*)fixupAddress = (int32_t)(
(uint64_t)symbolAddress - ((uint64_t)fixupAddress + 4 + 5)
);
break;
When Do REL32_N Variants Appear?
The REL32_1 variant commonly appears with MOV instructions that have a ModR/M byte after the 32-bit displacement, or with LEA instructions that use RIP-relative addressing with an additional immediate byte. For example, mov [rip+disp32], imm8 would use REL32_1 because the 1-byte immediate follows the relocation field. REL32_2 through REL32_5 are progressively rarer but handle instructions with larger trailing data.
i386 Relocation Types
COFFLoader also supports 32-bit x86 relocations for x86 BOFs:
Ccase IMAGE_REL_I386_DIR32: // 0x0006
// 32-bit absolute address (direct)
*(uint32_t*)fixupAddress += (uint32_t)(uintptr_t)symbolAddress;
break;
case IMAGE_REL_I386_REL32: // 0x0014
// 32-bit relative offset
*(int32_t*)fixupAddress = (int32_t)(
(uint32_t)(uintptr_t)symbolAddress -
((uint32_t)(uintptr_t)fixupAddress + 4)
);
break;
Complete Relocation Example
Let us trace through a complete relocation for a BOF that calls KERNEL32$GetCurrentProcessId:
TEXTStep-by-step: Resolving a CALL to KERNEL32$GetCurrentProcessId
1. Section .text is loaded at sectionMapping[0] = 0x1A0000
2. Symbol table entry #7: "__imp_KERNEL32$GetCurrentProcessId"
- SectionNumber = 0 (undefined/external)
- StorageClass = 2 (EXTERNAL)
3. Relocation entry in .text:
- VirtualAddress = 0x1C (offset within .text)
- SymbolTableIndex = 7
- Type = IMAGE_REL_AMD64_REL32 (0x0004)
4. Symbol resolution:
- process_symbol("__imp_KERNEL32$GetCurrentProcessId")
- Strip __imp_ -> "KERNEL32$GetCurrentProcessId"
- Not in InternalFunctions table
- Split on $ -> library="KERNEL32", function="GetCurrentProcessId"
- LoadLibraryA("KERNEL32") -> hKernel32
- GetProcAddress(hKernel32, "GetCurrentProcessId") -> 0x7FFA1A2B0000
5. Store in functionMapping (using sequential counter, not symbol index):
- Suppose this is the 3rd external function relocation processed
- functionMapping[3 * 8] = 0x7FFA1A2B0000
- The symbolAddress for relocation = &functionMapping[3*8] = 0x2A0018
6. Apply REL32 fixup:
- fixupAddress = sectionMapping[0] + 0x1C = 0x1A001C
- offset = 0x2A0018 - (0x1A001C + 4) = 0x0FFFF8
- *(int32_t*)0x1A001C = 0x000FFFF8
7. Machine code at 0x1A001A:
Before: FF 15 00 00 00 00 CALL [rip + 0x0]
After: FF 15 F8 FF 0F 00 CALL [rip + 0x0FFFF8]
When executed: RIP = 0x1A0020, target = 0x1A0020 + 0x0FFFF8 = 0x2A0018
At 0x2A0018: the 8-byte value 0x7FFA1A2B0000 (the real function address)
CPU reads the pointer -> calls GetCurrentProcessId at 0x7FFA1A2B0000
The Two-Level Indirection
For __imp_ symbols, there are two levels of indirection. The REL32 relocation patches the code to point at a functionMapping slot. That slot contains the actual function address. The CPU's CALL [rip+offset] instruction dereferences the pointer automatically. This is identical to how the PE loader handles DLL imports via the Import Address Table (IAT) -- the functionMapping buffer is COFFLoader's equivalent of the IAT.
Error Handling in Relocations
COFFLoader prints a debug message for unrecognized relocation types but does not abort. It also handles the case where process_symbol() returns NULL (unresolvable symbol) by logging the error. In practice, an unresolved symbol usually means the BOF references a DLL function from a library that is not present on the system.
C// COFFLoader handles unknown relocation types with a debug message
default:
printf("ERROR: Unhandled relocation type: 0x%x\n", relocs[relIdx].Type);
break;
Pop Quiz: Relocation Processing
Q1: An IMAGE_REL_AMD64_REL32 relocation has fixupAddress=0x5000, symbolAddress=0x6100. What 32-bit value is written?
Q2: What is the difference between IMAGE_REL_AMD64_ADDR64 and IMAGE_REL_AMD64_REL32?
Q3: IMAGE_REL_AMD64_REL32_2 differs from REL32 how?