|
| 1 | +# Unsafe Relocation Fixups in Asset Loaders |
| 2 | + |
| 3 | +{{#include ../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +## Why asset relocations matter |
| 6 | + |
| 7 | +Many legacy game engines (Granny 3D, Gamebryo, etc.) load complex assets by: |
| 8 | + |
| 9 | +1. Parsing a header and section table. |
| 10 | +2. Allocating one heap buffer per section. |
| 11 | +3. Building a `SectionArray` that stores the base pointer of every section. |
| 12 | +4. Applying relocation tables so that pointers embedded inside the section data get patched to the right target section + offset. |
| 13 | + |
| 14 | +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: |
| 15 | + |
| 16 | +<details> |
| 17 | +<summary>`GrannyGRNFixUp_0` (trimmed)</summary> |
| 18 | + |
| 19 | +```c |
| 20 | +int *__cdecl GrannyGRNFixUp_0(DWORD RelocationCount, |
| 21 | + Relocation *PointerFixupArray, |
| 22 | + int *SectionArray, |
| 23 | + char *destination) |
| 24 | +{ |
| 25 | + while (RelocationCount--) { |
| 26 | + int target_base = SectionArray[PointerFixupArray->SectionNumber]; // unchecked index |
| 27 | + int *patch_site = (int *)(destination + PointerFixupArray->SectionOffset); // unchecked offset |
| 28 | + *patch_site = target_base ; |
| 29 | + if (target_base) |
| 30 | + *patch_site = target_base + PointerFixupArray->Offset; |
| 31 | + ++PointerFixupArray; |
| 32 | + } |
| 33 | + return SectionArray; |
| 34 | +} |
| 35 | +``` |
| 36 | +
|
| 37 | +</details> |
| 38 | +
|
| 39 | +`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. |
| 40 | +
|
| 41 | +## Stage 1 – Writing backwards into loader metadata |
| 42 | +
|
| 43 | +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. |
| 44 | +
|
| 45 | +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 |
| 46 | +
|
| 47 | +``` |
| 48 | +0x20 (header) + 0x20 (section descriptors) |
| 49 | ++ n * 1 (section types) + n * 1 (flags) |
| 50 | ++ n * 4 (pointer table) = 0x4000 |
| 51 | +``` |
| 52 | +
|
| 53 | +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. |
| 54 | +
|
| 55 | +## Stage 2 – Deterministic heap layout on Windows 10 |
| 56 | +
|
| 57 | +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: |
| 58 | +
|
| 59 | +- Allocation #1 (metadata + section pointer arrays) land in a >0x4000 backend chunk. |
| 60 | +- Allocation #2 (section 0 contents) land immediately after allocation #1. |
| 61 | +- Allocation #3 (section 1 contents) land right after allocation #2, giving you a predictable target for subsequent relocations. |
| 62 | +
|
| 63 | +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. |
| 64 | +
|
| 65 | +## Stage 3 – Converting the primitive into RCE |
| 66 | +
|
| 67 | +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). |
| 68 | +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. |
| 69 | +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. |
| 70 | +
|
| 71 | +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. |
| 72 | +
|
| 73 | +## Practical checklist |
| 74 | +
|
| 75 | +- Look for asset loaders that maintain `SectionArray` / relocation tables. |
| 76 | +- Diff relocation handlers for missing bounds on indices/offsets. |
| 77 | +- Measure the allocator headers added by both the game’s allocator wrapper and the underlying OS heap to compute backwards offsets precisely. |
| 78 | +- Force deterministic placement by: |
| 79 | + - inflating metadata (many empty sections) until allocation size > `RtlpLargestLfhBlock`; |
| 80 | + - repeatedly loading the malicious asset to fill backend holes. |
| 81 | +- 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.). |
| 82 | +
|
| 83 | +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. |
| 84 | +
|
| 85 | +## References |
| 86 | +
|
| 87 | +- [Synacktiv – Exploiting Anno 1404](https://www.synacktiv.com/publications/exploiting-anno-1404.html) |
| 88 | +- [W. Yason – Windows 10 Segment Heap Internals (BlackHat USA 2016)](https://blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segment-Heap-Internals-wp.pdf) |
| 89 | +
|
| 90 | +{{#include ../banners/hacktricks-training.md}} |
0 commit comments