Unsafe Relocation Fixups in Asset Loaders

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Why asset relocations matter

Many legacy game engines (Granny 3D, Gamebryo, etc.) load complex assets by:

  1. Parsing a header and section table.
  2. Allocating one heap buffer per section.
  3. Building a SectionArray that stores the base pointer of every section.
  4. Applying relocation tables so that pointers embedded inside the section data get patched to the right target section + offset.

When the relocation handler blindly trusts attacker-controlled metadata, every relocation becomes a potential arbitrary read/write primitive. In Anno 1404: Venice, granny2.dll ships the following helper:

`GrannyGRNFixUp_0` (trimmed)
int *__cdecl GrannyGRNFixUp_0(DWORD RelocationCount,
                               Relocation *PointerFixupArray,
                               int *SectionArray,
                               char *destination)
{
  while (RelocationCount--) {
    int target_base = SectionArray[PointerFixupArray->SectionNumber]; // unchecked index
    int *patch_site = (int *)(destination + PointerFixupArray->SectionOffset); // unchecked offset
    *patch_site = target_base ;
    if (target_base)
      *patch_site = target_base + PointerFixupArray->Offset;
    ++PointerFixupArray;
  }
  return SectionArray;
}

SectionNumber is never range-checked and SectionOffset is never validated against the current section size. Crafting relocation entries with negative offsets or oversized indices lets you walk outside the section you control and stomp allocator metadata such as the section pointer array itself.

Stage 1 – Writing backwards into loader metadata

The goal is to make the relocation table of section 0 overwrite entries of SectionContentArray (which mirrors SectionArray and is stored right before the first section buffer). Because Granny’s custom allocator prepends 0x1F bytes and the NT heap adds its own 0x10-byte header plus alignment, an attacker can precalculate the distance between the start of the first section (destination) and the section-pointer array.

In the tested build, forcing the loader to allocate a GrannyFile structure that is exactly 0x4000 bytes makes the section-pointer arrays land right before the first section buffer. Solving

0x20 (header) + 0x20 (section descriptors)
+ n * 1 (section types) + n * 1 (flags)
+ n * 4 (pointer table) = 0x4000

gives n = 2720 sections. A relocation entry with SectionOffset = -0x3FF0 ( 0x4000 - 0x20 - 0x20 + 0x30 ) now resolves to SectionContentArray[1] even though the destination section thinks it is patching internal pointers.

Stage 2 – Deterministic heap layout on Windows 10

Windows 10 NT Heap routes allocations ≤ RtlpLargestLfhBlock (0x4000) to the randomized LFH and larger ones to the deterministic backend allocator. By keeping the GrannyFile metadata slightly above that threshold (using the 2720 sections trick) and preloading several malicious .gr2 assets, you can make:

  • Allocation #1 (metadata + section pointer arrays) land in a >0x4000 backend chunk.
  • Allocation #2 (section 0 contents) land immediately after allocation #1.
  • Allocation #3 (section 1 contents) land right after allocation #2, giving you a predictable target for subsequent relocations.

Process Monitor confirmed that assets are streamed on demand, so repeatedly requesting crafted units/buildings is enough to “prime” the heap layout without touching the executable image.

Stage 3 – Converting the primitive into RCE

  1. Corrupt SectionContentArray[1]. Section 0’s relocation table overwrites it by using the -0x3FF0 offset. Point it at any writable region you control (e.g., later section data).
  2. Recycle the corrupted pointer. Section 1’s relocation table now treats SectionNumber = 1 as whatever pointer you injected. The handler writes SectionArray[1] + Offset to destination + SectionOffset, giving you an arbitrary 4-byte write for every relocation entry.
  3. Hit reliable dispatchers. In Anno 1404 the target of choice was the granny2.dll allocator callbacks (no ASLR, DEP disabled). Overwriting the function pointer that granny2.dll uses for the next Malloc/Free call immediately diverts execution to attacker-controlled code loaded from the trojanized asset.

Because both granny2.dll and the injected .gr2 buffers reside at stable addresses when ASLR/DEP are disabled, the attack reduces to building a small ROP chain or raw shellcode and pointing the callback at it.

Practical checklist

  • Look for asset loaders that maintain SectionArray / relocation tables.
  • Diff relocation handlers for missing bounds on indices/offsets.
  • Measure the allocator headers added by both the game’s allocator wrapper and the underlying OS heap to compute backwards offsets precisely.
  • Force deterministic placement by:
    • inflating metadata (many empty sections) until allocation size > RtlpLargestLfhBlock;
    • repeatedly loading the malicious asset to fill backend holes.
  • Use a two-stage relocation table (first to retarget SectionArray, second to spray writes) and overwrite function pointers that will fire during normal rendering (allocator callbacks, virtual tables, animation dispatchers, etc.).

Once you gain an arbitrary file write (e.g., via the path traversal in the multiplayer save transfer), repackaging RDA archives with the crafted .gr2 gives you a clean delivery vector that is automatically decompressed by remote clients.

References

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks