mirror of
https://github.com/djdevin/recnet-plugin.git
synced 2026-09-08 06:31:29 -07:00
8.1 KiB
8.1 KiB
RecRoom Dumping Info
What each build era does to stop the metadata being read, and what it takes to get a dump out of it anyway.
Every build from 14 December 2018 – 19:12:52 UTC to 12 May 2026 – 18:46:14 UTC is dumpable. There are four separate protections stacked up over the years, added one on top of another, and each one needs its own answer.
Nothing here needs the game's servers, an account, or a debugger. The last two stages do need the game to actually run.
Stage 1 - The obfuscated metadata header
Added shortly after 21 June 2023, which is where the original dumps stopped.
- A normal
global-metadata.datstarts with the magic0xFAB11BAF, a version int, and then 31 pairs of{offset, size}in a fixed order, one per table. These builds shuffle which pair belongs to which table and overwrite the magic and version, so nothing can read it. - It is recoverable because the 31 tables exactly tile the file end to end. Solve for the chain through the header ints where
offset[i] + size[i] == offset[i+1], running from0x100to the end of the file, and you have the tables back in order. Two header slots are left over -- those are where the sanity and version fields used to be. - Tiling alone is not enough, and this is the single easiest thing to get wrong. More than one assignment can tile the file perfectly and still be wrong, so every table then has to be identified by its actual contents -- fixed entry sizes, count chains that have to agree with each other, and range checks like every
dataIndexlanding inside the blob it points into. A wrong-but-self-consistent tiling silently mis-pairs one table and the dump looks fine until Cpp2IL chokes on it. - Four metadata versions turn up across the range and each shifts the table layout:
v27,v29,v31andv31.1.v27tov29replacesattributesInfo(12 bytes an entry) withattributeData+attributeDataRange(8 bytes an entry, plus one sentinel entry whosestartOffsetis the total size of the blob).v29tov31growsIl2CppMethodDefinitionfrom 32 to 36 bytes, for the addedreturnParameterToken.v31tov31.1growsIl2CppCodeRegistrationfrom 15 to 17 qwords, from Unity2022.3.33onward.
- These builds also ship deliberately corrupted
<Module>type definitions -- thenameIndexandgenericContainerIndexare junk and have to be repaired or Cpp2IL falls over. It is a few hundred of them per build (346, 348 and 418 on the builds tested).
Stage 2 - The binary side
Il2CppCodeRegistrationinsideGameAssembly.dllgets its fields permuted the same way the metadata header does, and on top of that thecodeGenModulesarray is shuffled out of image order.- Cpp2IL indexes
codeGenModulespositionally, so the array has to be put back into image order or every method pointer ends up attached to the wrong assembly. The12 May 2026 – 18:46:14 UTCbuild has 419 modules. - The rebuilt struct will sometimes overlap the module array it points at, in which case it has to be written somewhere else in free space with the old pointer cleared.
- Cpp2IL itself needs patching for these builds.
BinarySearcher.cshas hardcoded scan limits (0xA_0000and0x70_000) and a 400 module cap that modern Rec Room blows straight past, andFindCodeRegistrationPost2019has to match the module count exactly instead of backtracking.
Stage 3 - The dummy metadata and the encrypted binary
Starts at 5 September 2024 – 03:21:53 UTC.
global-metadata.datis replaced with a dummy -- one repeated byte (0x52, ASCIIR) for the entire file. On12 May 2026 – 18:46:14 UTCthat is52,403,096bytes of nothing.- Deleting the dummy does not work. IL2CPP still opens it and the game dies with "Failed to initialize IL2CPP", so the fake file has to stay exactly where it is.
GameAssembly.dll's.textandil2cppsections are encrypted at rest as well -- entropy8.000on disk against about6.3once decrypted..rdataand.dataare left alone.- So there is nothing left on disk to repair and the game has to genuinely run. Both get decrypted during
il2cpp_init, in the first moments of startup, long before login, VR or networking, so it does not need to get far. It quits itself shortly after starting, so the dump has to win that race and suspend the process once it has a hit. - Only a fraction of the install is needed to get that far: every root level file, plus
RecRoom_Data'sglobalgamemanagers,boot.config,app.info, the.jsonmanifests, and theResources,UnitySubsystemsandil2cpp_datadirectories. Levels, sharedassets, resources.assets, the asset bundles, Plugins and EasyAntiCheat are all unnecessary. That is what takes the download from about5.4 GBdown to about325 MBa build. - The metadata keeps its permuted header in memory too, so there is no magic to scan for. Find it by its string table instead (
mscorlib.dllis a reliable anchor) and take the start of the allocation it lives in as the header. - Get the length by validation, never by guessing from the header. A wrong length can still tile cleanly and only falls apart at table identification, so candidate lengths have to be checked by actually identifying the tables. On the May 2026 build the obvious answer is
52,263,711and the correct one is52,403,096--139,385bytes out, which looks exactly like "the metadata has another encryption layer on it" when it is really just a bad carve. - For the binary, take only the code sections from memory and everything else from the untouched disk file. A straight memory dump is not usable, because
il2cpp_initrewrites.datain place -- type indices become live class pointers, field offsets get resolved -- and a static tool then reads pointers where it expects indices and dies. Base relocations inside the code have to be undone as well, since the module was loaded at an ASLR base, and the runtime only BSS tail grafted back on with its pointers rebased.
Stage 4 - The Referee anti-cheat
Everything up to 10 November 2025 – 04:35:41 UTC dumps with stage 3 alone. The builds from 2 December 2025 – 03:58:18 UTC onward need this as well.
- These builds add
Referee.dll, Themida packed, about 63 MB.GameAssembly.dll,UnityPlayer.dllandbaselib.dllall import from it and from nothing else -- every real import is resolved at runtime by the protector. - Referee is waiting on a global shared memory section named
KjMpQrStUvWxYzAb, created at exactly66,060,456bytes. The consumer walks it as three chunks of22,020,144after a small header, so the size has to be right. It builds the name as\BaseNamedObjects\..., an absolute path, so it has to be the global namespace and not the per session one. - If that section is not there, the session fails with status
-99903, the code then reads through the null pointer it was left with, andGameAssembly's DllMain never returns. Noil2cpp_init, no metadata, nothing to dump. That is the entire wall. - Creating the section is the whole fix. It can be all zeroes -- nothing verifies the contents, and there is no key or signature anywhere in Referee to forge. Then launch the game normally and it boots all the way through, into VR init, far past the point the metadata is decrypted.
- It has to be the real game executable. Loading
GameAssembly.dllyourself, or swappingReferee.dllfor a stub that exports its one function (jjiEVn), are both rejected before anything useful happens. - Creating a global section needs
SeCreateGlobalPrivilege, so this has to be run elevated. Without admin you silently get a per session section instead, the game never finds it, and it fails exactly as if there were none. - From there take a full memory dump of the process once
GameAssembly.dllis loaded, and carve the metadata and the binary out of it offline exactly as in stage 3. - The section name and its size are the only build specific values in any of this. If a later build changes either, it will crash before
il2cpp_initas though there were no section at all -- the name is a wide string inside that build'sReferee.dlland the size is the maximum size it passes when creating it.
- Automatically documented MD file, some details might be subtly wrong.