Chapter 9

Base Relocations & ASLR

How Windows fixes up absolute addresses when a PE loads at a different base than expected — the .reloc section structure, relocation types, and Address Space Layout Randomization including entropy levels and bypass techniques

Scenario

A malware sample has no .reloc section and the DYNAMICBASE flag is absent from DllCharacteristics. This means the executable demands to load at a fixed address — 0x00400000 by default. An exploit targeting this binary doesn't need to leak an address; the location of every function is predictable across all machines. You need to explain to the vulnerability management team why this matters — and why modern Windows mitigations depend on relocation being present.

The Relocation Problem

When the compiler produces machine code that references a global variable or a function by absolute address, it embeds the actual virtual address into the instruction. For example:

; Compiler assumed ImageBase = 0x00400000
; Global variable g_count is at RVA 0x3000 → VA 0x00403000
mov  eax, [0x00403000]   ; load g_count
call 0x00401234          ; call function at VA 0x00401234

These hardcoded addresses work perfectly if the image loads at its preferred ImageBase. But if the image is loaded at a different address — because another DLL already occupies that range, or because ASLR randomized the location — those addresses are wrong. The code would read from and jump to wrong memory locations.

Base relocations solve this by: (1) recording every location in the image that contains a hardcoded absolute address, and (2) at load time, adjusting each of those addresses by the difference between the preferred and actual load address (delta).

The .reloc Section

Base relocations live in the .reloc section, pointed to by Data Directory entry 5 (IMAGE_DIRECTORY_ENTRY_BASERELOC). The section is organized as a sequence of relocation blocks, each covering a 4KB page of the image:


  .reloc Section Structure
  ──────────────────────────────────────────────────────────────────
  [ IMAGE_BASE_RELOCATION block 1 ]
  ┌────────────────────────────────────────────┐
  │ VirtualAddress: 0x00001000  (page RVA)     │
  │ SizeOfBlock:    0x0018      (bytes in block)│
  │ Entry[0]: 0x3018  → type=3, offset=0x018   │
  │ Entry[1]: 0x302C  → type=3, offset=0x02C   │
  │ Entry[2]: 0x3044  → type=3, offset=0x044   │
  │ Entry[3]: 0x0000  → type=0, padding        │
  └────────────────────────────────────────────┘

  [ IMAGE_BASE_RELOCATION block 2 ]
  ┌────────────────────────────────────────────┐
  │ VirtualAddress: 0x00002000                 │
  │ ...                                        │
  └────────────────────────────────────────────┘

  ... more blocks until all pages covered ...

  [ Terminator: VirtualAddress=0, SizeOfBlock=0 ]

  Each entry is a WORD (2 bytes):
  Bits 15-12: relocation type
  Bits 11-0:  byte offset within the page
  Target address = block.VirtualAddress + entry.offset

  

Relocation Types

TypeValueDescription
IMAGE_REL_BASED_ABSOLUTE0 No-op padding entry. Used to align the block to a DWORD boundary. The loader skips these.
IMAGE_REL_BASED_HIGHLOW3 Most common for 32-bit PE. The full 32-bit address at the target location is adjusted by adding the delta (actual base − preferred base).
IMAGE_REL_BASED_DIR6410 Used for 64-bit PE (PE32+). The full 64-bit value at the target location is adjusted by the delta. This is what you see in all modern 64-bit Windows PE files.
IMAGE_REL_BASED_HIGH1 Legacy. Only the high 16 bits of a 32-bit address are stored; adjust the high word.
IMAGE_REL_BASED_LOW2 Legacy. Only the low 16 bits of a 32-bit address.

How the Loader Applies Relocations

// Loader relocation algorithm (pseudo-code)
delta = actual_load_address - preferred_image_base;

if (delta == 0) return;  // loaded at preferred base, nothing to fix

// Walk each IMAGE_BASE_RELOCATION block
for each block in .reloc:
    page_base = actual_load_address + block.VirtualAddress;
    num_entries = (block.SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;
    for i = 0; i < num_entries; i++:
        entry = block.entries[i];
        type   = entry >> 12;
        offset = entry & 0xFFF;
        if (type == IMAGE_REL_BASED_HIGHLOW):     // 3
            *(DWORD*)(page_base + offset) += (DWORD)delta;
        else if (type == IMAGE_REL_BASED_DIR64):  // 10
            *(QWORD*)(page_base + offset) += delta;
        // type 0 = skip (padding)

Address Space Layout Randomization

ASLR was introduced in Windows Vista (2007) as a defense against exploits that rely on knowing the exact address of code and data. Without ASLR, every run of a program on every machine loaded libraries at the same predictable address. With ASLR, the base addresses of the executable, its DLLs, the stack, and the heap are randomized each load — requiring an attacker to leak an address before executing a ROP chain or shellcode.

ASLR applies only to images that opt in by setting the DYNAMICBASE flag (0x0040) in OptionalHeader.DllCharacteristics. Images without this flag are loaded at their preferred ImageBase — predictable, but the loader will try to avoid conflicts with other images that are already loaded.


  ASLR Address Randomization on 64-bit Windows
  ──────────────────────────────────────────────────────────────────
  Image type         Randomization       Bits of entropy
  ─────────────────────────────────────────────────────
  EXE (low-entropy) 64 KB granularity    8 bits  (256 positions)
  EXE (HEASLR)      64 KB granularity    17 bits (131,072 positions)
  DLL                64 KB granularity    8 bits  (256 positions)
  ntdll / kernel32   System-wide once    8 bits  (shared across processes)
  Stack              Per-thread          17 bits
  Heap               Per-process         5 bits

  64-bit address space: 128 TB user space
  Even 8-bit ASLR on a 64-bit system spreads images across 256 slots ×
  64 KB = 16 MB range, within a much larger address space.

  

ASLR Entropy on 32-bit vs 64-bit

32-bit Windows has only 2 GB of user address space. Fitting all DLLs, the stack, the heap, and the executable into that space while randomizing leaves only a small number of possible positions. This is why 32-bit ASLR is considered weak — an attacker can brute-force position in 256–512 attempts, and a crash-restart loop might succeed within seconds.

64-bit Windows has 128 TB of user virtual address space. Even modest randomization (8 bits = 256 positions) is harder to brute-force because each crash kills the process and requires a restart. With High-Entropy ASLR (HIGHENTROPYVA, 0x0020 in DllCharacteristics), executables can be randomized with 17 bits of entropy (131,072 possible positions), making brute-force infeasible.

ASLR Bypass Techniques

TechniqueMechanismMitigated By
Information leak / KASLR bypass Use a memory disclosure vulnerability to read a pointer to known code (e.g., a return address on the stack or a vtable pointer in a heap object) and calculate the base address from the known offset Fixing the info-leak vulnerability; Control Flow Guard; stack cookies
Non-ASLR DLL as gadget base Find a loaded DLL that doesn't have DYNAMICBASE — its base is fixed and known. Use ROP gadgets from that DLL to construct exploits. Ensure all loaded DLLs have DYNAMICBASE. Process Mitigation Policies can block loading of non-ASLR DLLs.
Heap spray Spray heap with NOP sleds + shellcode until a predictable address range is covered with controlled content. Jump to an address in the sprayed range without needing exact position. ASLR for heap (makes spray target unpredictable); safe unlinking; SegHeap
Partial address overwrite Overwrite only the low bytes of a return address. ASLR still randomizes the page, but within a page, offsets are fixed. A partial overwrite within the same page region bypasses full ASLR. Stack canaries; shadow stacks (CET)
JIT spraying Use a JIT engine (JavaScript engine, Flash) to spray predictable code patterns into executable memory across a large address range. JIT hardening; disabling JIT (in browser sandboxes)

High-Entropy ASLR (HEASLR)

On 64-bit Windows, executables can opt into High-Entropy ASLR by setting both DYNAMICBASE (0x0040) and HIGHENTROPYVA (0x0020) in DllCharacteristics. With HEASLR enabled, the loader randomizes the image base using a 17-bit random offset at a 64 KB granularity, placing the image anywhere in a 128 TB-wide window rather than the 16 MB window of standard ASLR.

import pefile

def check_aslr(filepath):
    pe = pefile.PE(filepath)
    chars = pe.OPTIONAL_HEADER.DllCharacteristics
    dynamicbase = bool(chars & 0x0040)
    highentropyva = bool(chars & 0x0020)
    nxcompat = bool(chars & 0x0100)
    cfg = bool(chars & 0x4000)
    has_reloc = any(s.Name.startswith(b'.reloc') for s in pe.sections)
    machine = pe.FILE_HEADER.Machine
    is_64bit = (machine == 0x8664)

    print(f"DYNAMICBASE (ASLR opt-in): {dynamicbase}")
    print(f"HIGHENTROPYVA (HEASLR):    {highentropyva}")
    print(f"NXCOMPAT (DEP opt-in):     {nxcompat}")
    print(f"CFG Guard:                 {cfg}")
    print(f"Has .reloc section:        {has_reloc}")
    print(f"64-bit:                    {is_64bit}")

    if dynamicbase and not has_reloc:
        print("WARNING: DYNAMICBASE set but no .reloc section — ASLR won't work!")
    if not dynamicbase:
        print("RISK: No ASLR — image loads at fixed preferred base")
    pe.close()

Q & A

If two DLLs want the same preferred ImageBase, what happens?

The Windows loader resolves conflicts by rebasing the DLL that arrives second. When loading a DLL, the loader checks if the preferred base is available. If another DLL already occupies that range, the loader picks a different available address and applies base relocations to fix up all the absolute addresses. For this to work, the DLL being rebased must have a .reloc section. If it doesn't have one, the loader either fails to load it or (in some cases) loads it anyway with incorrect absolute addresses — resulting in crashes. This conflict was common before ASLR: many third-party DLLs used the same default base (0x10000000). Modern practice is to either give DLLs a unique preferred base or rely on ASLR to find available space. When ASLR is active, the entire address randomization process effectively rebases all images anyway, so the preferred base is just a nominal default.

Can you strip the .reloc section to prevent ASLR?

Yes, and this is a known malware technique. By removing the .reloc section and clearing the DYNAMICBASE flag, a PE is forced to load at its fixed preferred base (0x00400000 for a default EXE). Windows will attempt to honor this — if the address range is free, it loads there; if not, it either fails or loads at an alternate location with broken absolute addresses. Malware does this because: (1) it hardcodes absolute addresses in its shellcode or data that must match the actual load address, (2) it wants to avoid the overhead of relocation at load time, (3) it slightly complicates analysis (tools may not auto-apply relocations). Detection: check for absence of .reloc section AND absence of DYNAMICBASE in a PE that appears to be an EXE or DLL (not a shellcode blob). Legitimate modern Windows binaries almost universally have both.

Does ASLR protect against kernel-level exploits?

ASLR in user mode does not protect kernel addresses. Kernel Address Space Layout Randomization (KASLR) is a separate mechanism. On Windows, KASLR was significantly strengthened over versions: (1) The kernel image (ntoskrnl.exe) and drivers are randomized at boot time. (2) The kernel's base address is not accessible from user mode since Windows 8 — NtQuerySystemInformation with SystemModuleInformation used to return kernel base addresses but this was restricted. (3) Even the kernel page table entries were randomized in recent Windows to prevent kernel ASLR bypass via page-walk attacks. The distinction matters for vulnerability research: a user-mode exploit needs a user-space info leak; a kernel exploit needs a kernel-space info leak. KASLR and user-mode ASLR are independent and each requires its own bypass.