mirror of
https://github.com/djdevin/recnet-plugin.git
synced 2026-09-08 06:31:29 -07:00
unstable patch for 202312+
This commit is contained in:
+20
-3
@@ -1,3 +1,20 @@
|
||||
GamePath.props
|
||||
obj
|
||||
bin
|
||||
build
|
||||
CMakeLists.txt.user
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
CMakeScripts
|
||||
Testing
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
CTestTestfile.cmake
|
||||
_deps
|
||||
CMakeUserPresets.json
|
||||
|
||||
# CLion
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#cmake-build-*
|
||||
|
||||
@@ -1,309 +1,189 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Guidance for working in this repo. This is a **BepInEx 6 (IL2CPP)** Harmony plugin that points the
|
||||
Rec Room client at a self-hosted server. Read the README for the user-facing overview; this file is
|
||||
the stuff you only learn by getting burned.
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
This is the **native (C/Win32) redirector** that points the Rec Room client at a self-hosted server
|
||||
**without any managed mod loader**. It replaces the sibling `../recnet-patcher` project (a BepInEx/
|
||||
MelonLoader Harmony plugin) — both loaders fail on current Rec Room builds (BepInEx crashes in
|
||||
`il2cpp_init`; the loader trips the anti-cheat memory-integrity scan). This build is loaded as a
|
||||
`version.dll` proxy and hooks Winsock + il2cpp methods directly in native code. Read `README.md` for
|
||||
the user-facing overview; this file is the stuff you only learn by getting burned.
|
||||
|
||||
## Build & deploy
|
||||
|
||||
```sh
|
||||
dotnet build -c Debug -p:GamePath="C:\Games\depots\471711\23191908"
|
||||
Rec Room / `GameAssembly.dll` is **64-bit — you must build x64**. A 32-bit DLL silently fails to load.
|
||||
The default VS dev shell is x86, and the PowerShell tool **does not persist env vars between calls**,
|
||||
so the amd64 env import and the cmake/build must run in the **same** call, else you get an x86 DLL:
|
||||
|
||||
```powershell
|
||||
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat"
|
||||
cmd /c "`"$vcvars`" amd64 >nul 2>&1 && set" | ForEach-Object { if ($_ -match '^([^=]+)=(.*)$') { Set-Item -Path "Env:$($matches[1])" -Value $matches[2] } }
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release # add -DGAME_DIR="C:\Games\recflare-client-unstable" to deploy
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
- `GamePath` points at the Rec Room install root. It's normally set in the gitignored
|
||||
`GamePath.props` (see `GamePath.props.example`); the `-p:GamePath=...` override is handy for one-offs.
|
||||
- The project references ~300 interop DLLs from `$(GamePath)\BepInEx\interop`. Those are generated by
|
||||
Il2CppInterop the first time the game runs under BepInEx — if they're missing, launch the game once.
|
||||
- A post-build `DeployPlugin` target copies `RecNetPlugin.dll` into `$(GamePath)\BepInEx\plugins\`.
|
||||
**The copy fails while Rec Room is running** (the DLL is locked) — that's an MSB3027 error, not a
|
||||
compile error. Close the game and rebuild. The DLL is also always left in `bin/Debug/net6.0/`.
|
||||
- **Verify arch** from the PE header after building: machine word at `(int32 @0x3C)+4` must be **0x8664**,
|
||||
not 0x014C. `cmake` refuses to configure a non-64-bit toolchain (guard in `CMakeLists.txt`).
|
||||
- Toolchain: VS 2022 Community; `cmake`/`ninja` ship with it. Installed Windows SDK is **10.0.19041.0**;
|
||||
the VS-generator MSBuild probe can't find it without the VC env — another reason to use Ninja inside
|
||||
the imported amd64 env.
|
||||
- Build output is **`build\version.dll`** (`OUTPUT_NAME version`, `PREFIX ""`) and that is the *only*
|
||||
artifact — it is self-contained. It used to ship alongside a `version_orig.dll` (a copy of the system
|
||||
DLL) that its exports statically forwarded to; that dependency was removed in f3eb296 in favour of
|
||||
runtime forwarding, so **do not** re-add a copy step for it. `-DGAME_DIR=...` copies `version.dll`
|
||||
into the game folder. **The copy fails while Rec Room is running** (DLL locked) — close the game first.
|
||||
|
||||
## Hard-won gotchas (read before patching anything)
|
||||
## Architecture
|
||||
|
||||
1. **Interop assemblies are stubs. The real code is native.** The DLLs under `BepInEx/interop` are
|
||||
Il2CppInterop proxies — method bodies just marshal into `GameAssembly.dll`. dnSpy / managed
|
||||
decompilation of the interop shows *no real logic*. You cannot read the actual algorithms
|
||||
statically; you learn behavior by patching + logging at runtime.
|
||||
`DllMain` (`src/dllmain.c`) spawns one background thread — `HookThread` in
|
||||
`src/hooks/hook_manager.c` — which gates on `IsGameProcess()`, installs the ws2_32 hooks inline, then
|
||||
fans each il2cpp patch out onto **its own thread**, since they each wait independently on the runtime
|
||||
coming up. Order matters only for the memcheck patch (started first, see 5). The pipeline:
|
||||
|
||||
2. **Obfuscated names differ per game build.** Rec Room's type/method names are obfuscated
|
||||
(`PGECJHKNIEN`, `MDBMGOBECDJ`, `cm_did_ppk`, etc.). A dnSpy dump from *some* build will not
|
||||
necessarily match the interop you compile against. Real example from the DUID work: a dnSpy dump
|
||||
called the method `CheckForMismatch` and the pref field `DBAIOPIEJNC`, but in our interop the
|
||||
method is `CheckForDUIDMismatch` and neither `DBAIOPIEJNC` nor `MDBMGOBECDJ` exist at all. **Always
|
||||
resolve members against the interop DLLs you actually build against** (see the Cecil snippet below),
|
||||
never against a dump from an unknown build.
|
||||
1. **Loader vector — `version.dll` proxy** (`src/proxy/version_proxy.c`). `RecRoom.exe` imports
|
||||
`VERSION.dll` by name, and the loader searches the app dir before System32, so our `version.dll`
|
||||
loads very early (before `UnityPlayer.dll`). All 17 real exports are satisfied by local `my_*`
|
||||
wrappers, aliased to the real names via `#pragma comment(linker, "/export:NAME=my_NAME")` (aliases,
|
||||
**not** PE forwarders — no dot in the target). Each wrapper lazily `LoadLibraryW`s the genuine
|
||||
`%SYSTEM32%\version.dll` **by absolute path** (so the app-dir search can't loop back into us) and
|
||||
calls through. `DllMain` starts the hook thread. To retarget at `winhttp.dll` instead, swap this
|
||||
file's export list for winhttp's.
|
||||
|
||||
3. **Patch the concrete class, not the IL2CPP "interface".** Il2CppInterop renders IL2CPP interfaces
|
||||
as abstract classes deriving from `Il2CppObjectBase`. Harmony will happily patch an abstract method
|
||||
and throw no error, but the prefix **never runs** because the game dispatches to the concrete
|
||||
implementation. This cost us a whole "shipped fix" that did nothing. Concrete impls live in
|
||||
`Assembly-CSharp.dll`. Example: patch `CheatManager.CheckForDUIDMismatch`, *not* the interface
|
||||
`PGECJHKNIEN.CheckForDUIDMismatch`. Verify with Cecil that `IsAbstract == false` before trusting a
|
||||
patch.
|
||||
2. **DNS host rewrite** (`src/hooks/dns_hook.c`, detours `ws2_32!getaddrinfo`). On an **exact-match**
|
||||
lookup it swaps the hostname (`ns.rec.net` → `ns.recflare.net`) and delegates to the real
|
||||
`getaddrinfo`, so the client reaches the target's *current* IP. This alone is **not sufficient** —
|
||||
it only changes DNS resolution; SNI and the HTTP `Host:` header still say `ns.rec.net`. Kept as a
|
||||
safety net.
|
||||
|
||||
4. **Obfuscated members live in the global namespace** and are referenced unqualified in this codebase
|
||||
(e.g. `typeof(LEALBOODIEE)`, `PGECJHKNIEN`). No `using` needed.
|
||||
3. **HTTP host rewrite** (`src/unity/http_rewrite.c`) — the real fix. The alternate backend
|
||||
(`ns.recflare.net`) serves its own vhost/cert, so requests must carry that host in URL + SNI + Host.
|
||||
This replicates the managed `SendRequestPatch`: it waits for the il2cpp runtime, resolves the
|
||||
concrete static `BestHTTP.HTTPManager.SendRequest(HTTPRequest)`, reads `req.get_Uri().get_AbsoluteUri()`,
|
||||
swaps the host, and `req.set_Uri(new System.Uri(...))` before forwarding. **This is a call-through
|
||||
hook** (must run the original) so it depends on the trampoline in `detour.c`.
|
||||
|
||||
5. **Harmony prefix conventions here:** force a value + `return false` to skip the original (see
|
||||
`Patches/EACPatches.cs`). **Bind parameters positionally (`__0`, `__1`, …), never by their
|
||||
obfuscated name** — obfuscated *parameter* names change per build just like type names, and binding
|
||||
by name fails at load with `Parameter "XXX" not found in method ...`. For out-params take
|
||||
`ref string __0` plus `ref bool __result`.
|
||||
4. **TLS pinning bypass** (`src/unity/ssl_patch.c`). Redirecting to a mismatched cert fails the
|
||||
handshake; this resolves the concrete `Org.BouncyCastle.Crypto.Tls.LegacyTlsAuthentication`
|
||||
`.NotifyServerCertificate` and detours it to an accept-all no-op. **Replace-only** hook (never calls
|
||||
the original).
|
||||
|
||||
6. **Method names in `[HarmonyPatch]` are strings — the compiler does not check them.** A renamed
|
||||
*type* is a build error; a renamed *method* builds fine and only shows up as a HarmonyX error in
|
||||
`LogOutput.log` at load, or (worse) as a patch that silently never runs. After a game upgrade,
|
||||
re-verify every string method name with Cecil, don't just trust a green build.
|
||||
5. **Memory-integrity scan neutralizer** (`src/unity/memcheck_patch.c`) — **the reason 3/4/6 are
|
||||
possible at all.** A background scan hashes `GameAssembly.dll` code against baked-in hashes; our
|
||||
inline patches change that memory, so boot dies *"Launch validation failed. Is Rec Room installed
|
||||
correctly?"*. The scanner class is obfuscated and **rotates every build**, so it is matched by
|
||||
*shape*: the class in `Assembly-CSharp.dll` carrying both a `System.Threading.Thread` and a
|
||||
`CancellationTokenSource` field. Its public/instance/0-param/non-void method is the scan entry; its
|
||||
return type is the promise the boot step awaits. We detour that entry (replace-only) to return an
|
||||
already-resolved promise, obtained by `il2cpp_runtime_invoke` on the promise type's static
|
||||
special-name 0-param `Resolved` getter (found by sweeping every image for a getter returning that
|
||||
exact class, skipping `_k__BackingField`). The getter is test-invoked **before** committing the
|
||||
detour — if it returns null or throws we skip the hook rather than hand boot a null promise.
|
||||
`PatchMemoryIntegrityCheck` is started **first** among the il2cpp patches: the boot step that awaits
|
||||
the scan can fire early and the reflection sweep takes ~700 ms, so it needs the head start.
|
||||
|
||||
### Surviving a game-version upgrade
|
||||
6. **EAC neutralizer** (`src/unity/eac_patch.c`). Two replace-only hooks on the literal
|
||||
`RecRoom.AntiCheat.EACManager` (namespace+class are *not* obfuscated; the methods are):
|
||||
- **Readiness → true.** The real check needs EasyAntiCheat services that no longer exist. Matched by
|
||||
shape: the sole static, 0-param, `bool`-returning, **non**-special-name method (excluding
|
||||
special-name is what keeps property getters out). Hook returns 1 — note the native signature is
|
||||
`(void *methodInfo)`, since a static il2cpp method still gets `MethodInfo*` in RCX.
|
||||
- **`GenerateChallengeResponse(string)` → `base64(challenge)`**, `base64("nothing")` for null/empty.
|
||||
Unobfuscated name, resolved directly. `g_gcr_static` is read from the method flags because it
|
||||
decides whether the string arg arrives in RCX or RDX — get that wrong and you base64 a `this`
|
||||
pointer.
|
||||
|
||||
Obfuscated names are re-rolled every build. Unobfuscated names (`CheckForDUIDMismatch`, `WriteDUIDs`,
|
||||
`ClearDUIDs`, `SendRequest`, `NotifyServerCertificate`, `GenerateChallengeResponse`) have been stable
|
||||
across upgrades so far; everything else must be re-resolved. Don't guess from the old name — **search
|
||||
the new interop by signature**, which is what actually identifies the target:
|
||||
Both are only safe because 5 has already neutralized the hash check.
|
||||
|
||||
| Patch | Target | Signature that identifies it |
|
||||
| --- | --- | --- |
|
||||
| `PhotonPatches` | `HPEENKELKDJ.MGKINLFMJLB` | only instance, 0-param method returning `Photon.Realtime.AppSettings` in `Assembly-CSharp` (the static/2-param `PUNNetworkManager` sibling also returns it — exclude it) |
|
||||
| `EACPatches` (is-ready) | `EACManager.MCFIOBHCFBB` | only static, 0-param `bool` on `EACManager` that isn't a property getter; type lives in `RecRoom.Rranticheat.Runtime.dll` |
|
||||
`src/core/` = logger + JSON-ish config + process info (incl. `IsGameProcess`). `src/utils/strings.c` = host match/rewrite.
|
||||
`src/debug/` and `connect_hook.c` are logging stubs / the disabled connect hook. Config is
|
||||
`redirector.json` next to `RecRoom.exe` (sample `.example`), parsed by a **flat key-scan, not real
|
||||
JSON** — keep it flat, one object per rewrite.
|
||||
|
||||
> **Il2CppInterop regenerates on launch and obfuscation can differ between generations.** Only scan the
|
||||
> interop the game *actually loaded* — check that its mtime is *after* the last game launch, and treat a
|
||||
> clean HarmonyX load (no "Could not find method") as the real proof. A stale/mismatched interop
|
||||
> generation once produced a whole different name set (`IHODDIDPEOD.JEGOHKJDPFH`, `EACManager.KOIFGPGJGKB`)
|
||||
> that got overwritten on the next launch back to the names below — patching against it failed at load.
|
||||
## Hard-won gotchas (read before touching hooks)
|
||||
|
||||
Renames observed in the **20230414 build** (`C:\Games\recflare-client`, Steam manifest
|
||||
`6426603215211043630`):
|
||||
1. **The detour engine has two modes; picking wrong corrupts code** (`src/memory/detour.c`).
|
||||
`InstallDetour(target, hook, backup, outTrampoline)`:
|
||||
- `outTrampoline != NULL` → **call-through**: it length-decodes the prologue (`decode`/`steal_len`),
|
||||
copies **whole instructions** (≥14 bytes) into a trampoline with relocation fixups
|
||||
(`RelocateInto`), and NOP-pads. Use when the hook calls the original (DNS, HTTP).
|
||||
- `outTrampoline == NULL` → **replace-only**: a blind 14-byte overwrite. Only safe when the hook
|
||||
never calls through (SSL), because we jump away immediately so a torn trailing instruction is never
|
||||
executed.
|
||||
The original crash bug was a *blind* 14-byte copy on a call-through target: `ws2_32!getaddrinfo`'s
|
||||
prologue has instruction boundaries at 3/7/11/**15**, so 14 bytes tore the 4th `mov` and the
|
||||
trampoline ran garbage → whichever thread called the original died. The symptom was subtle: the game
|
||||
booted (the first lookup runs on a Unity *background* thread that died silently) but the client's real
|
||||
API lookup later hit the same broken trampoline and its thread died before any request left the process.
|
||||
|
||||
- `LEALBOODIEE.GBNKOFMAJPA` → `HPEENKELKDJ.MGKINLFMJLB`
|
||||
- `EACManager.IMMGELPFGCK` → `EACManager.MCFIOBHCFBB`
|
||||
- `CheckForDUIDMismatch` out-param → `BPOGCIINKBB` (still bound as `__0`, no source change)
|
||||
2. **The length decoder relocates two cases and bails on the rest.** `decode()` classifies each
|
||||
instruction: `RK_RIPREL` (rip-relative disp32, `mod=00,rm=101`) and `RK_REL32` (`E8`/`E9`) are
|
||||
**relocated** — `AllocNear` places the trampoline within ±2 GB of the target so the rewritten
|
||||
displacements still fit in int32, and each fixup is range-checked. rip is computed from the *end of
|
||||
the whole instruction*, past any trailing immediate. `decode` returns 0 on a two-byte (`0x0F`)
|
||||
opcode or anything it doesn't model, and `rel8` branches are measured but flagged `RK_UNSUPPORTED`
|
||||
(they'd need a rel8→rel32 rewrite); in all those cases `InstallDetour` **refuses the hook** (logs
|
||||
it) rather than corrupt code. This is what unblocked the call-through hook on
|
||||
`HTTPManager.SendRequest`, whose prologue is the usual il2cpp class-init check `cmp byte [rip+disp],
|
||||
0` + `jne` — it now steals 16 bytes and relocates cleanly. ws2_32 stubs are position-independent and
|
||||
hook fine as-is (`getaddrinfo` steals 15).
|
||||
|
||||
Renames observed in the **07-21 build** (`C:\Games\recflare-client`), for reference:
|
||||
3. **il2cpp method resolution.** Resolve types by literal namespace+name across all loaded assemblies
|
||||
(`il2cpp_domain_get_assemblies` → `il2cpp_assembly_get_image` → `il2cpp_class_from_name` — the last
|
||||
only searches the image you give it, so sweep). Get methods with `il2cpp_class_get_method_from_name`
|
||||
(argc counts declared params only). **There is no `il2cpp_method_get_pointer` export** — read the
|
||||
compiled entry from `MethodInfo` offset 0 (`methodPointer`, `*(void**)method`). Wait for
|
||||
`il2cpp_domain_get()` to be non-NULL (runtime init) before resolving, and `il2cpp_thread_attach` your
|
||||
native thread before any metadata call. Interop DLLs name the type `Il2CppSystem.Uri`, but the runtime
|
||||
metadata namespace is plain `System`/`Uri`.
|
||||
|
||||
- `CheckForDUIDMismatch` out-param `ALOMDLLNIMD` → `LICOPEEMHHG` (now bound as `__0`)
|
||||
- `GenerateChallengeResponse` param `PGCINMIEBJP` → `__0`
|
||||
- `GPFPFDBGCEK.AMOHMPKKGHL` → `LEALBOODIEE.GBNKOFMAJPA`
|
||||
- `EACManager.FJLMLEPOKGE` → `EACManager.IMMGELPFGCK`
|
||||
- `JAPJPGNBMNM`, `HPHDJAFFHCN<>`, `HAAHJPGNIMD` — **gone** from `Assembly-CSharp`. These were the
|
||||
image-signing `PromisePatch`, now deleted and replaced by `ImageSigningPatch` (see below), which
|
||||
hooks framework types instead and so has no obfuscated names left to break.
|
||||
4. **Framework names are stable; patch the concrete class.** `SendRequest`, `get_Uri`/`set_Uri`,
|
||||
`AbsoluteUri`, `NotifyServerCertificate`, `HTTPManager`, `LegacyTlsAuthentication` are unobfuscated and
|
||||
have survived build changes — this is *why* these are the hook points. As in the managed project, hook
|
||||
the **concrete** impl, never an il2cpp interface. Verify signatures with Mono.Cecil against the interop
|
||||
in `../recflare-client/BepInEx/interop` when they drift (see the sibling `../recnet-patcher/CLAUDE.md`
|
||||
for the Cecil load snippet; that project's interop has the same types).
|
||||
|
||||
## Image signature verification
|
||||
5. **version.dll loads into multiple processes — log per-PID.** Our DLL loads into the game, the
|
||||
EasyAntiCheat launcher/bootstrap, and the crash handler. They previously shared `redirector.log` opened
|
||||
with `"w"` and truncated each other (the EAC process, stuck forever in `WaitForUnity` because
|
||||
`UnityPlayer.dll` never loads there, buried the game's diagnostics under module dumps). The logger now
|
||||
writes **`redirector_<pid>.log`**, and `WaitForUnity` is time-bounded. `HookThread` now returns
|
||||
immediately unless `IsGameProcess()` (`src/core/process.c`, basename == `RecRoom.exe`) — that check
|
||||
sits **before** `InitConsole`/`InitLogger`, because `AllocConsole` in the crash-handler process was
|
||||
opening a second debug window on every launch. So only `RecRoom.exe` writes a log at all now; if you
|
||||
need diagnostics from a sibling process, move the gate to wrap `InitConsole` alone.
|
||||
|
||||
The client verifies images against an RSA public key whose modulus is a string literal in
|
||||
`global-metadata.dat`. Patching that literal is fragile; **don't**. Hook the framework instead.
|
||||
6. **The connect hook is intentionally disabled.** `src/hooks/connect_hook.c` blindly redirects *all*
|
||||
:443 traffic (would break Photon/CDN/telemetry). `getaddrinfo` covers the il2cpp DNS path surgically.
|
||||
`gethostbyname` also has a latent self-recursion bug (calls `real_gethostbyname`, not a trampoline) if
|
||||
ever installed.
|
||||
|
||||
The decisive observation: the literal base64-decodes to **exactly 256 bytes and does not start with
|
||||
`0x30`**, so it is a *raw* 2048-bit modulus, not a DER/SPKI blob. The client base64-decodes it and
|
||||
hand-builds `RSAParameters`, then verifies with `mscorlib` RSA — plain unobfuscated names. Rather than
|
||||
touch the modulus, we just force the verify to succeed. Confirmed working at runtime: images load with
|
||||
no signing check.
|
||||
|
||||
`Patches/ImageSigningPatch.cs` hooks, all in `Il2Cppmscorlib.dll`:
|
||||
|
||||
| Hook | Purpose |
|
||||
| --- | --- |
|
||||
| `RSACryptoServiceProvider.VerifyData` / `VerifyHash` ×2 | **the fix:** forces the verify to succeed |
|
||||
|
||||
Config lives in `[Signing]`, a single knob: `Disable Signature Verification` defaults **true** —
|
||||
that's the shipped behaviour, since this setup doesn't use image signing. The patch only *removes* the
|
||||
check (forces verify-true); it does not swap in a replacement key.
|
||||
|
||||
Two things to remember:
|
||||
|
||||
- **Blast radius is the point, not a wart.** Forcing verify-true affects *all* mscorlib RSA
|
||||
verification, not just images. That breadth is load-bearing — see the warning below. BestHTTP's TLS
|
||||
uses its own bundled BouncyCastle, so cert validation appears unaffected — inferred from assembly
|
||||
layout, not proven. Keep it behind the config knob.
|
||||
- **If it ever stops working:** images failing to load with the knob on means verification moved off
|
||||
mscorlib RSA onto `BestHTTP.SecureProtocol.Org.BouncyCastle`; the equivalent hooks there are the
|
||||
**concrete** `RsaDigestSigner`/`PssSigner.VerifySignature` (not the abstract `ISigner` — gotcha 3).
|
||||
|
||||
## Analytics / telemetry
|
||||
|
||||
**One knob**, `[Analytics] Disable Telemetry`, default **true**, gating all three patch files below.
|
||||
One switch is the deliberate choice: nobody wants Amplitude gone but the collector alive, and per-vendor
|
||||
knobs are just more ways to end up half-configured.
|
||||
|
||||
**But they are not one mechanism.** Each vendor sends over a different stack, and *that*, not the
|
||||
hostname, is what decides how you block it — a host blocklist would be dead code for two of these four:
|
||||
|
||||
| Vendor | Patch file | Stack it sends over | How it's blocked |
|
||||
| --- | --- | --- | --- |
|
||||
| Amplitude | `AmplitudePatch.cs` | BestHTTP | `Log*` prefixes + host block on `amplitude.com` |
|
||||
| Data collector | `AmplitudePatch.cs` | BestHTTP | host block on first label `datacollection*` |
|
||||
| Backtrace | `BacktracePatch.cs` | **UnityWebRequest** | `BacktraceHttpClient.Post` ×4 |
|
||||
| Unity Analytics | `UnityTelemetryPatch.cs` | **native (neither)** | Unity's own opt-out properties — **doesn't work, see below** |
|
||||
|
||||
The collector is matched on the hostname's *first label*, not a fixed domain, so it stays right across
|
||||
deployments (`…recflare.net`, `…rec.net`). It's a first-party endpoint, hence its own knob — you may
|
||||
want Amplitude gone but the collector alive, or the reverse.
|
||||
|
||||
The collector is matched on the hostname's *first label*, not a fixed domain, so it stays right across
|
||||
deployments (`…recflare.net`, `…rec.net`). It's a first-party endpoint, hence its own knob — you may
|
||||
want Amplitude gone but the collector alive, or the reverse.
|
||||
|
||||
**Blocking the `Log*` entrypoints is not sufficient, and this is the trap.** `LogEventAsync`,
|
||||
`LogSerializedEventAsync`, `LogIdentifyAsync` etc. only *queue*; the queue is persisted to the
|
||||
`pending_room_stats` PlayerPref and drained later by the client's own flush coroutines. So a batch
|
||||
queued before the plugin existed (or during a session where a `Log*` door we didn't cover was used)
|
||||
still ships on the next launch, and mitmproxy keeps showing `api2.amplitude.com` even though
|
||||
LogOutput.log says `[AMPLITUDE] ... blocked LogEventAsync`. **Symptom-to-cause: prefixes visibly
|
||||
firing + traffic still leaving means you blocked the producer, not the sender.**
|
||||
|
||||
The send path, for the record: `AmplitudeAnalyticsClient` → transport interface `FOMPBHDLPDO`
|
||||
(`MAJANJBIDMF` / `KBECOPLKGHL`) → concrete impl **`MHBPNDOGOLG` in `RecNet.Runtime.dll`** → BestHTTP.
|
||||
Note `RecRoom.Analytics.Runtime.dll` has *no* assembly reference to BestHTTP/UnityWebRequest — the
|
||||
transport is injected, so grepping the analytics assembly for an HTTP type finds nothing.
|
||||
|
||||
So the actual block is at the BestHTTP layer: a second prefix on
|
||||
`HTTPManager.SendRequest(HTTPRequest)` (the same method `SendRequestPatch` hooks) that drops any
|
||||
request to a blocked host. Two properties worth keeping:
|
||||
|
||||
- **No obfuscated names.** `HTTPManager`/`HTTPRequest`/`HTTPResponse` are BestHTTP's own types, so
|
||||
this survives a game upgrade even though every name in the analytics path above will re-roll.
|
||||
- **It fakes a 200, not a failure.** We build an `HTTPResponse` (status 200, Amplitude's real body
|
||||
shapes: `success` for `/identify`, the `{"code":200,...}` envelope otherwise; `{"success":true}` for
|
||||
the collector, whose shape we don't know — an empty body would trip the RecNet wrapper's "Response
|
||||
was empty"), set `State = Finished`, and invoke `request.Callback` inline. The transport resolves its
|
||||
promise, the client considers the batch delivered and clears `pending_room_stats`. Failing the
|
||||
request instead would leave the batch queued and retried every session forever.
|
||||
|
||||
RudderStack (`get_OutOfSessionRudderStackKey`) and gamesight are *not* covered — add their hosts to
|
||||
`AmplitudeDomains` / `CollectorLabelPrefixes` if they need to go too.
|
||||
|
||||
### Backtrace (`submit.backtrace.io`) — UnityWebRequest, not BestHTTP
|
||||
|
||||
`Backtrace.Unity.dll` references `UnityEngine.UnityWebRequestModule` and nothing else HTTP-shaped, so
|
||||
the BestHTTP host block never sees it. Two things make this one easy: the SDK is a third-party package
|
||||
and therefore **unobfuscated** (no per-build churn to survive), and every submission it makes — crash
|
||||
reports, minidumps, metrics — funnels through the four **concrete** `BacktraceHttpClient.Post`
|
||||
overloads (`IBacktraceHttpClient` is the interface — gotcha 3).
|
||||
|
||||
The overloads split by *who sends the request*, which decides the patch shape:
|
||||
|
||||
- `void Post(url, jObject, onComplete)` — the SDK sends internally. Prefix + skip, then invoke
|
||||
`onComplete(200, false, "{}")` ourselves. Answering it matters: the metrics queue holds the batch
|
||||
until the callback reports success, so a silent skip means it retries forever.
|
||||
- `UnityWebRequest Post(…)` ×3 — builds the request and hands it back; **the caller** sends it
|
||||
(`yield return request.SendWebRequest()`). A prefix returning null gets dereferenced, so instead a
|
||||
*postfix* repoints the finished request at `http://127.0.0.1:1/` — nothing listens, so it fails
|
||||
connection-refused in microseconds with no packet leaving the box, and the SDK takes its ordinary
|
||||
offline path. Repointing beats rebuilding: the SDK keeps its own handlers and headers, so there's
|
||||
nothing to guess about what the caller dereferences next.
|
||||
|
||||
Not covered: `RecRoomNativeClient` installs a native crash handler, and a minidump it uploads on the
|
||||
launch after a hard crash never passes through managed code. A multipart minidump POST to
|
||||
submit.backtrace.io with the hooks visibly firing is that path.
|
||||
|
||||
### Unity Analytics / Performance Reporting (`perf-events.cloud.unity3d.com`) — ⚠️ UNSOLVED, and fine
|
||||
|
||||
**Confirmed not working on the 20230414 build; accepted as-is — don't re-litigate it.** The setters are
|
||||
refused, `enabled` reads back `True` on every attempt, and the uploads keep flowing. The other three
|
||||
`[Analytics]` knobs all confirmed dropping at runtime, and they account for the bulk of the traffic, so
|
||||
this one is noise-floor. If it ever has to go for real it needs a hosts-file/DNS block or a native
|
||||
hook — there is no managed lever. What follows is why, kept because the *approach* is right even
|
||||
though this build refuses it.
|
||||
|
||||
|
||||
`UnityEngine.Analytics.Analytics` and `PerformanceReporting` are thin managed shims over native engine
|
||||
code; the uploads happen inside the player, on no managed send path and over neither HTTP stack. What
|
||||
they do have is a documented opt-out, so `UnityTelemetryPatch` just sets it: `PerformanceReporting.
|
||||
enabled = false`, `Analytics.enabled = false`, `deviceStatsEnabled = false`, `limitUserTracking = true`.
|
||||
|
||||
Two traps, both handled there: these are native setters that can be **silently refused**, so the patch
|
||||
*reads the properties back* and only believes it worked if they read false — the `[UNITY-TELEMETRY]`
|
||||
log line is the proof, the assignment isn't. (That read-back is what caught this build refusing them;
|
||||
without it the knob would have looked like it worked.) And since "not initialised yet" was the leading
|
||||
theory for the refusal, it retries from `OnSceneLoaded`, capped at 5 attempts — which ruled that theory
|
||||
out, because all five read back `True`.
|
||||
7. **Rec Room's own names are obfuscated and rotate every build — match by shape, never by name.**
|
||||
Gotcha 4's framework names are the exception; anything in `Assembly-CSharp` is an 11-char scramble
|
||||
(`NAEMGPMOPED`, `JMCKLNABHHJ`) that differs next build, so **never hard-code one you saw in a log**.
|
||||
The two patches that need such a target (5, 6) locate it through the il2cpp reflection API by
|
||||
structure instead — field types, static-ness, param count, return type, special-name flag. Rules
|
||||
that follow from getting this wrong:
|
||||
- **Log every candidate; warn on >1.** The *method* searches count matches and log
|
||||
`WARNING N candidates` when ambiguous (scan-start in 5, readiness in 6). The **scanner class**
|
||||
search in 5 does not — it logs each `[MEMCHECK] scanner candidate` and silently keeps the last.
|
||||
Real logs already show **two**, so that field signature is *not* unique and the patch is riding on
|
||||
ordering. It works today; treat it as the most fragile thing here, and read those lines before
|
||||
trusting a boot.
|
||||
- **Never fall back to "close enough".** Every resolver bails with a log line rather than hooking a
|
||||
guess — a wrong detour on a rotating target corrupts an unrelated method.
|
||||
- **Verify before committing an irreversible detour** where you can (5 test-invokes the `Resolved`
|
||||
getter first).
|
||||
|
||||
## Inspecting the game
|
||||
|
||||
Use **Mono.Cecil** for static metadata/signature checks (accessibility, abstract-ness, exact param
|
||||
names, which assembly a concrete impl lives in). Mark-of-the-web will block loading `Mono.Cecil.dll`
|
||||
directly — copy it somewhere local, `Unblock-File`, then load via bytes:
|
||||
|
||||
```powershell
|
||||
$interop = "$env:GamePath\BepInEx\interop"
|
||||
$dst = "$scratch\Mono.Cecil.dll"
|
||||
# NOTE: Mono.Cecil.dll ships in BepInEx/core, NOT in BepInEx/interop.
|
||||
Copy-Item "$env:GamePath\BepInEx\core\Mono.Cecil.dll" $dst; Unblock-File $dst
|
||||
[System.Reflection.Assembly]::Load([System.IO.File]::ReadAllBytes($dst)) | Out-Null
|
||||
$asm = [Mono.Cecil.AssemblyDefinition]::ReadAssembly("$interop\Assembly-CSharp.dll")
|
||||
# then walk $asm.MainModule.GetTypes(), inspect .Methods / .Fields / .IsAbstract / .IsStatic ...
|
||||
```
|
||||
|
||||
Not every target is in `Assembly-CSharp` — `EACManager` is in `RecRoom.Rranticheat.Runtime.dll`,
|
||||
`HTTPManager`/`LegacyTlsAuthentication` in `RecNet.Runtime.dll`. When a lookup comes up empty, sweep
|
||||
all ~305 DLLs in `interop/` (`ReadAssembly` each, `.Dispose()` after) before concluding it's gone.
|
||||
|
||||
Notes:
|
||||
- Windows PowerShell 5.1 has **no** `?.` null-conditional operator — use explicit `$x -eq $null` checks.
|
||||
- String literals (pref keys, endpoints, GUIDs) are in `RecRoom_Data/il2cpp_data/Metadata/global-metadata.dat`.
|
||||
`grep -a -o -E '[ -~]{4,}' global-metadata.dat | grep -i <thing>` extracts them.
|
||||
- The BepInEx runtime log is `$(GamePath)/BepInEx/LogOutput.log`. Our plugin logs under the
|
||||
`RecNet Plugin` source. `[HTTP]`, `[DUID]`, `[DUID-PROBE]`, `[DEVICEID]`, `[CORRUPT]` are our tags.
|
||||
- PlayerPrefs on Windows live in the registry at `HKCU\Software\Against Gravity\Rec Room`, value names
|
||||
are `<key>_h<unityHash>`, values are `REG_BINARY`. CodeStage AntiCheat stores strings *obscured*
|
||||
(XOR-encrypted), so a stored id will not appear as plaintext in registry or files.
|
||||
|
||||
## Case study: the Create Account / DUID hang
|
||||
|
||||
The gnarliest bug so far; the diagnostic tooling for it still lives in the repo. Summary:
|
||||
|
||||
- **Symptom:** on some machines Create Account hangs. Log shows a `PlayerReporting/v1/deviceId` POST
|
||||
returning `200 {"success":true}`, after which the client never calls the `create_account` OAuth and
|
||||
never persists the id (`WriteDUIDs` never runs).
|
||||
- **Trigger:** a device-id **mismatch**. `CheatManager.CheckForDUIDMismatch(out string)` returns true
|
||||
when the stored id differs from `SystemInfo.deviceUniqueIdentifier`. True → migration path → POST →
|
||||
hang. Machines whose stored id matches never take the path.
|
||||
- **Stored id:** PlayerPrefs key `cm_did_ppk` (registry `cm_did_ppk_h3478365449`), CodeStage
|
||||
ObscuredString-encoded. `CheatManager.WriteDUIDs()` writes it, `ClearDUIDs()` deletes it. In our
|
||||
interop these are **instance** methods (a dnSpy dump showed them static — build difference again).
|
||||
- **Two surprises:**
|
||||
1. Deleting the registry value did **not** change the `oldDeviceId` in the POST, and the probe
|
||||
showed no `cm_did_ppk` read that session — i.e. the "old" id is **not sourced from local
|
||||
PlayerPrefs** on the failing path. Consequence: **a registry-reset script does not fix it.**
|
||||
2. `game callback attached = True` on that request → the client is genuinely waiting on the
|
||||
response. But the real server already returns `{"success":true}` at 200 and it still hangs, so the
|
||||
accepting response shape (if one exists) is something more specific.
|
||||
|
||||
> ### ⚠️ OPEN QUESTION — where does the old DUID actually live?
|
||||
> We have **not** found the source of the `oldDeviceId` value. It survives a full delete of the
|
||||
> `HKCU\Software\Against Gravity\Rec Room` registry key, it does not appear as plaintext anywhere in
|
||||
> `AppData/LocalLow/Against Gravity/Rec Room`, and on the failing run the `cm_did_ppk` PlayerPref is
|
||||
> never read. So `cm_did_ppk` is *a* copy but not the one that seeds the migration POST. Leading (but
|
||||
> unconfirmed) theory: it's held server-side by the archival server, which recorded it from prior
|
||||
> POSTs, and/or cached in memory from a server response. **Until this is found, the only reliable fix
|
||||
> is `Suppress` (client) or correcting the value server-side — not clearing local storage.** Next
|
||||
> steps to try: inspect what the recflare backend stores/returns for the account's device id; dump the
|
||||
> `HTTPCache` entries decoded (not plaintext); trace who sets the field the POST body reads from.
|
||||
- **Working client-side workaround:** force `CheckForDUIDMismatch` → false (skips the migration path
|
||||
entirely). Config: `Suppress DUID Mismatch = true`.
|
||||
- **Proper root-cause fix:** server-side — make the endpoint stop reporting a stale `old` id (so
|
||||
`old == new`, no mismatch) or return whatever the client needs to proceed.
|
||||
|
||||
### Diagnostic knobs (all in `[Advanced]`)
|
||||
|
||||
`Suppress DUID Mismatch` defaults **true** (it's the shipped fix). Everything else defaults **false** —
|
||||
those are investigation tools, not normal config. See the patch files for details.
|
||||
|
||||
| Config key | Patch file | What it does |
|
||||
| --- | --- | --- |
|
||||
| `Suppress DUID Mismatch` | `DUIDMismatchPatch.cs` | **The fix (default true).** Force `CheckForDUIDMismatch` → false. |
|
||||
| `Simulate DUID Mismatch` | `DUIDMismatchPatch.cs` | Force it → true. Reproduce the hang without a corrupt value. |
|
||||
| `Corrupt Stored DUID` | `CorruptDUIDPatch.cs` | One-shot: write a truncated id via `WriteDUIDs` (spoofing `SystemInfo.deviceUniqueIdentifier`) to create a *genuinely* corrupt stored value. |
|
||||
| `Restore Stored DUID` | `CorruptDUIDPatch.cs` | One-shot undo: `WriteDUIDs` with the real id. |
|
||||
| `DeviceId Response Override` / `Status` | `DeviceIdResponsePatch.cs` | Rewrite the `deviceId` response body/status in-flight to probe what shape the client will accept. |
|
||||
| (probe) | `DUIDProbePatch.cs` | Logs every PlayerPrefs get/set and the `WriteDUIDs`/`ClearDUIDs` calls — `WriteDUIDs() called` is the "client accepted the response" signal. |
|
||||
|
||||
`DUIDMismatchPatch` has three modes: `Simulate` → force true, `Suppress` → force false, neither →
|
||||
pass through to the real check (needed to observe a genuinely corrupt stored value).
|
||||
|
||||
**The diagnostic patches ship** — the DUID hang is still unsolved, so the tooling stays in the build
|
||||
where affected users can turn it on. Before cutting a release, confirm their knobs still default
|
||||
false (`Simulate`, `Corrupt`, `Restore`, `DeviceId Response Override`); an accidentally-true default
|
||||
would break normal play. `Suppress DUID Mismatch` is the real fix and defaults **true**, so it stays on.
|
||||
- **Runtime**: the per-PID log is the source of truth. Our tags: `[STATUS] [HOOK] [DETOUR] [DNS ...]`
|
||||
`[REWRITE] [REDIRECT] [SSL] [HTTP] [MEMCHECK] [EAC]`. Success = `[HTTP] host rewrite installed on
|
||||
SendRequest` then `[HTTP] https://ns.rec.net/... -> https://ns.recflare.net/...` per request. A
|
||||
`[DETOUR] ... refusing hook` line means gotcha 2 — the decoder hit a prologue it won't relocate
|
||||
(`0x0F` opcode, `rel8` branch, or an unmodelled opcode), and **that patch is not active**.
|
||||
- **Static il2cpp**: dump prologue bytes from `GameAssembly.dll` by converting the logged runtime `code=`
|
||||
address to an RVA (subtract the logged `GameAssembly.dll Base`) and mapping RVA→file offset via the PE
|
||||
section headers. Method/field signatures: Mono.Cecil over the interop DLLs (see gotcha 4).
|
||||
- PowerShell here is Windows PowerShell 5.1 — no `?.`; use explicit `$x -eq $null`. Avoid `2>&1` on native
|
||||
exes.
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(RRRedirector C)
|
||||
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
|
||||
# Rec Room is 64-bit; a 32-bit build silently fails to load. Guard against an x86 toolchain.
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(FATAL_ERROR "Must build 64-bit. Import the amd64 VC env (vcvarsall.bat amd64) before cmake.")
|
||||
endif()
|
||||
|
||||
# Where to deploy the built proxy. Point at your Rec Room install root; the deploy step below copies
|
||||
# version.dll there. That single file is the whole payload -- see the note above the deploy step.
|
||||
set(GAME_DIR "" CACHE PATH "Rec Room install root to deploy version.dll into")
|
||||
|
||||
|
||||
add_library(redirector SHARED
|
||||
|
||||
src/dllmain.c
|
||||
|
||||
|
||||
# Proxy loader (exports wrap the real system version.dll; DllMain starts the hook thread)
|
||||
src/proxy/version_proxy.c
|
||||
|
||||
|
||||
# Core
|
||||
src/core/logger.c
|
||||
src/core/config.c
|
||||
src/core/process.c
|
||||
|
||||
|
||||
# Hooks
|
||||
src/hooks/dns_hook.c
|
||||
src/hooks/connect_hook.c
|
||||
src/hooks/hook_manager.c
|
||||
|
||||
|
||||
# Memory
|
||||
src/memory/detour.c
|
||||
|
||||
|
||||
# Unity
|
||||
src/unity/module_watch.c
|
||||
src/unity/ssl_patch.c
|
||||
src/unity/http_rewrite.c
|
||||
src/unity/memcheck_patch.c
|
||||
src/unity/eac_patch.c
|
||||
|
||||
|
||||
# Utils
|
||||
src/utils/strings.c
|
||||
|
||||
|
||||
# Debug
|
||||
src/debug/api_logger.c
|
||||
src/debug/tls_logger.c
|
||||
src/debug/packet_logger.c
|
||||
)
|
||||
|
||||
|
||||
target_include_directories(
|
||||
redirector PRIVATE
|
||||
include
|
||||
)
|
||||
|
||||
|
||||
|
||||
target_link_libraries(
|
||||
redirector
|
||||
|
||||
ws2_32
|
||||
dbghelp
|
||||
psapi
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Output must be named version.dll so RecRoom.exe's VERSION.dll import resolves to us.
|
||||
set_target_properties(
|
||||
redirector PROPERTIES
|
||||
|
||||
OUTPUT_NAME "version"
|
||||
PREFIX ""
|
||||
)
|
||||
|
||||
|
||||
# The proxy loads the real system version.dll at runtime (see src/proxy/version_proxy.c), so there is
|
||||
# nothing extra to materialize or ship -- version.dll is fully self-contained.
|
||||
|
||||
|
||||
# Optional one-step deploy: -DGAME_DIR=... to copy version.dll into the game folder after build.
|
||||
# The copy fails while Rec Room is running (DLL locked) -- close the game and rebuild.
|
||||
if(GAME_DIR)
|
||||
add_custom_command(TARGET redirector POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:redirector>" "${GAME_DIR}/version.dll"
|
||||
COMMENT "Deploying version.dll to ${GAME_DIR}"
|
||||
)
|
||||
endif()
|
||||
@@ -1,10 +0,0 @@
|
||||
<Project>
|
||||
<!-- Copy this file to "GamePath.props" (same folder) and set GamePath to your local
|
||||
Rec Room install. GamePath.props is gitignored, so your local path stays out of the repo.
|
||||
|
||||
The install must have been launched once under BepInEx 6 (IL2CPP) so that
|
||||
BepInEx/interop/ is populated with the proxy assemblies this project references. -->
|
||||
<PropertyGroup>
|
||||
<GamePath>C:\Path\To\RecRoom</GamePath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 djdevin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,283 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using BestHTTP;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Amplitude analytics: the client ships telemetry to Amplitude, which a self-hosted setup has no use
|
||||
// for (and which leaks play data off-box). Prefix every event-logging entrypoint and swallow the call
|
||||
// so nothing is ever queued, batched or sent.
|
||||
//
|
||||
// One knob for all telemetry, `[Analytics] Disable Telemetry`, default true — it gates this file plus
|
||||
// BacktracePatch and UnityTelemetryPatch. Deliberately not split per vendor: nobody wants Amplitude
|
||||
// gone but the collector alive, and four switches for one intention is four ways to be half-configured.
|
||||
//
|
||||
// Target resolution: AmplitudeAnalytics.AmplitudeAnalyticsClient in RecRoom.Analytics.Runtime.dll,
|
||||
// concrete (it derives from SingletonMonoBehaviour<T>, so there is no abstract-interface dispatch
|
||||
// trap here — see gotcha 3 in CLAUDE.md). All five Log* names are UNobfuscated in the 20230414 build.
|
||||
// They are still strings, so a rename shows up only as a HarmonyX "Could not find method" in
|
||||
// LogOutput.log, not as a build error — the per-method [AMPLITUDE] blocked log line below is the real
|
||||
// proof a hook is live.
|
||||
//
|
||||
// Why all five: blocking LogEventAsync alone was not enough — a session's room_stats/perf_stats
|
||||
// events still went out, and the LogEventAsync prefix never logged at all, so that entrypoint was
|
||||
// never even called. Those events are the pre-serialized batch the client parks in the
|
||||
// `pending_room_stats` PlayerPref (visible in the DUID-PROBE log), which points at
|
||||
// LogSerializedEventAsync rather than LogEventAsync.
|
||||
//
|
||||
// Why the Log* prefixes alone STILL are not enough — and why `BlockAnalyticsUploadPatch` below is the
|
||||
// part that actually stops the traffic: uploads to api2.amplitude.com kept showing up in mitmproxy
|
||||
// with LogEventAsync/LogIdentifyAsync visibly blocked in LogOutput.log. The Log* methods only *queue*;
|
||||
// the queue is persisted (`pending_room_stats`) and drained later by the client's own flush coroutines
|
||||
// (Flush / AMEAMPDLJPN / PPOCFIHNKPP), which reach the network through the transport interface
|
||||
// `FOMPBHDLPDO` — concrete impl `MHBPNDOGOLG` in RecNet.Runtime.dll, i.e. BestHTTP. So a batch queued
|
||||
// in an earlier session ships on the next launch no matter what we do to the Log* doors. Blocking at
|
||||
// the BestHTTP layer catches every path, present and future, and costs no obfuscated names.
|
||||
[HarmonyPatch]
|
||||
public static class AmplitudePatch
|
||||
{
|
||||
private static readonly HashSet<string> _loggedBlocked = new();
|
||||
|
||||
// Returns false to skip the original. Logs once per entrypoint so LogOutput.log shows which door
|
||||
// the client actually used.
|
||||
private static bool Block(string entrypoint)
|
||||
{
|
||||
if (_loggedBlocked.Add(entrypoint))
|
||||
Plugin.Log.LogInfo($"[AMPLITUDE] analytics disabled — blocked {entrypoint}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogEventAsync")]
|
||||
private static bool LogEventAsyncPrefix() =>
|
||||
Plugin.DisableTelemetry.Value && Block("LogEventAsync");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogPrevSessionEventAsync")]
|
||||
private static bool LogPrevSessionEventAsyncPrefix() =>
|
||||
Plugin.DisableTelemetry.Value && Block("LogPrevSessionEventAsync");
|
||||
|
||||
// The likely culprit for the room_stats/perf_stats batch — takes the already-serialized
|
||||
// Dictionary<string, object> that gets parked in `pending_room_stats`.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogSerializedEventAsync")]
|
||||
private static bool LogSerializedEventAsyncPrefix() =>
|
||||
Plugin.DisableTelemetry.Value && Block("LogSerializedEventAsync");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogIdentifyAsync")]
|
||||
private static bool LogIdentifyAsyncPrefix() =>
|
||||
Plugin.DisableTelemetry.Value && Block("LogIdentifyAsync");
|
||||
|
||||
// The odd one out: static, and it returns a promise instead of void. Skipping it with a null
|
||||
// __result would hand the caller something it will chain .Then() on, so we substitute an
|
||||
// already-resolved promise — the call looks like it succeeded instantly. If we cannot build one,
|
||||
// we let the original run rather than risk a null-deref at quit time.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogOutOfSessionEvent")]
|
||||
private static bool LogOutOfSessionEventPrefix(ref LAHBDKNMNHN __result)
|
||||
{
|
||||
if (!Plugin.DisableTelemetry.Value)
|
||||
return true;
|
||||
|
||||
var resolved = ResolvedPromise();
|
||||
if (resolved == null)
|
||||
return true;
|
||||
|
||||
__result = resolved;
|
||||
return Block("LogOutOfSessionEvent");
|
||||
}
|
||||
|
||||
private static bool _promiseResolved;
|
||||
private static MethodInfo _resolvedPromiseGetter;
|
||||
|
||||
// Finds the concrete Promise class's static `Resolved` property getter without hardcoding its
|
||||
// obfuscated name. Among the static, 0-param property getters in RecRoom.Promises.Runtime that
|
||||
// return the promise interface there are exactly two: the real (obfuscated) property getter and
|
||||
// the compiler-generated `get_<Name>_k__BackingField`. The backing field may be null if the
|
||||
// property initialises lazily, so we drop it by its compiler-generated name — which the
|
||||
// obfuscator leaves alone — and keep the other one.
|
||||
private static LAHBDKNMNHN ResolvedPromise()
|
||||
{
|
||||
if (!_promiseResolved)
|
||||
{
|
||||
_promiseResolved = true;
|
||||
|
||||
var candidates = typeof(LAHBDKNMNHN).Assembly.GetTypes()
|
||||
.SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
|
||||
.Where(m => m.ReturnType == typeof(LAHBDKNMNHN)
|
||||
&& m.GetParameters().Length == 0
|
||||
&& m.IsSpecialName
|
||||
&& !m.Name.EndsWith("_k__BackingField"))
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 1)
|
||||
_resolvedPromiseGetter = candidates[0];
|
||||
else
|
||||
Plugin.Log.LogWarning(
|
||||
$"[AMPLITUDE] expected exactly one resolved-promise getter, found {candidates.Count} " +
|
||||
"— LogOutOfSessionEvent will NOT be blocked");
|
||||
}
|
||||
|
||||
if (_resolvedPromiseGetter == null)
|
||||
return null;
|
||||
|
||||
return _resolvedPromiseGetter.Invoke(null, null) as LAHBDKNMNHN;
|
||||
}
|
||||
|
||||
// Analytics hosts we refuse to talk to, matched two different ways because they identify
|
||||
// themselves two different ways:
|
||||
//
|
||||
// AmplitudeDomains — a registrable domain plus everything under it, so api2.amplitude.com
|
||||
// and api.eu.amplitude.com are both covered.
|
||||
// CollectorLabelPrefixes — the *first* hostname label, for the telemetry collector that lives
|
||||
// on whatever domain the deployment uses (datacollection.recflare.net,
|
||||
// datacollection.rec.net, datacollection-eu.…). The domain varies, the
|
||||
// label doesn't, so match on the label and stay deployment-agnostic.
|
||||
//
|
||||
// Split into separate lists because they're *matched* differently, not configured differently —
|
||||
// one knob covers the lot.
|
||||
private static readonly string[] AmplitudeDomains = { "amplitude.com" };
|
||||
private static readonly string[] CollectorLabelPrefixes = { "datacollection" };
|
||||
|
||||
// Backtrace and Unity's perf-events don't normally reach BestHTTP at all — Backtrace goes through
|
||||
// UnityWebRequest (see BacktracePatch) and Unity's is native (see UnityTelemetryPatch). They're
|
||||
// listed here anyway because it costs a string comparison to be right if that ever changes, and
|
||||
// because "the host block covers every host we don't want talked to" is easier to reason about
|
||||
// than a list with holes in it. Only perf-events is named, not all of cloud.unity3d.com — the
|
||||
// client uses other Unity services.
|
||||
private static readonly string[] BacktraceDomains = { "backtrace.io" };
|
||||
private static readonly string[] UnityTelemetryHosts = { "perf-events.cloud.unity3d.com" };
|
||||
|
||||
private static bool IsUnderAnyDomain(string host, string[] domains) =>
|
||||
domains.Any(d => host.Equals(d, StringComparison.OrdinalIgnoreCase)
|
||||
|| host.EndsWith("." + d, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool IsAmplitudeHost(string host) => IsUnderAnyDomain(host, AmplitudeDomains);
|
||||
|
||||
private static bool IsCollectorHost(string host)
|
||||
{
|
||||
var dot = host.IndexOf('.');
|
||||
var firstLabel = dot < 0 ? host : host.Substring(0, dot);
|
||||
|
||||
return CollectorLabelPrefixes.Any(p => firstLabel.StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// One knob for the lot. The vendors are split into separate lists above because they're matched
|
||||
// differently, not because they're configured differently.
|
||||
private static bool IsBlockedHost(string host)
|
||||
{
|
||||
if (string.IsNullOrEmpty(host) || !Plugin.DisableTelemetry.Value)
|
||||
return false;
|
||||
|
||||
return IsAmplitudeHost(host)
|
||||
|| IsCollectorHost(host)
|
||||
|| IsUnderAnyDomain(host, BacktraceDomains)
|
||||
|| UnityTelemetryHosts.Any(h => host.Equals(h, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// The part that actually stops the traffic — see the note at the top of the file. Prefixes the
|
||||
// same BestHTTP entrypoint SendRequestPatch hooks (HTTPManager is BestHTTP's own type, so no
|
||||
// obfuscated names are involved and this survives game upgrades) and, for analytics hosts, hands
|
||||
// the caller a synthetic 200 instead of sending anything.
|
||||
//
|
||||
// Faking success rather than failure is deliberate: the transport resolves its promise, the flush
|
||||
// coroutine considers the batch delivered, and the client clears `pending_room_stats` — so nothing
|
||||
// accumulates and nothing retries. Failing the request instead would leave the batch queued and
|
||||
// re-attempted every session.
|
||||
[HarmonyPatch(typeof(HTTPManager), "SendRequest", [typeof(HTTPRequest)])]
|
||||
public static class BlockAnalyticsUploadPatch
|
||||
{
|
||||
private static bool Prefix(HTTPRequest request, ref HTTPRequest __result)
|
||||
{
|
||||
// SendRequest returns the request it was handed; callers chain off it, so hand it back
|
||||
// even though we never send it.
|
||||
__result = request;
|
||||
|
||||
return !Drop(request);
|
||||
}
|
||||
|
||||
// Second net, one layer down. Every SendRequest overload funnels into SendRequestImpl, and
|
||||
// IL2CPP is free to inline the tiny SendRequest(HTTPRequest) body into its callers — a hook on
|
||||
// it then never fires for those call sites (gotcha: a Harmony patch that loads clean can still
|
||||
// never run). SendRequestImpl is the last managed-visible chokepoint before the connection, so
|
||||
// anything that slipped past the hook above is caught here.
|
||||
[HarmonyPatch(typeof(HTTPManager), "SendRequestImpl", [typeof(HTTPRequest)])]
|
||||
public static class ImplPatch
|
||||
{
|
||||
private static bool Prefix(HTTPRequest request) => !Drop(request);
|
||||
}
|
||||
|
||||
// True when the request was blocked (caller should skip the original).
|
||||
private static bool Drop(HTTPRequest request)
|
||||
{
|
||||
if (!IsBlockedHost(request.Uri.Host))
|
||||
return false;
|
||||
|
||||
// Once per host normally; every request under [Advanced] Debug, since "did this specific
|
||||
// upload get dropped or did it slip past?" is exactly the question a mitmproxy trace
|
||||
// raises, and a deduped line can't answer it.
|
||||
if (Plugin.Debug.Value)
|
||||
Plugin.Log.LogInfo($"[ANALYTICS] dropped {request.MethodType} {request.Uri.AbsoluteUri}");
|
||||
else if (_loggedBlocked.Add("upload:" + request.Uri.Host))
|
||||
Plugin.Log.LogInfo($"[ANALYTICS] dropping uploads to {request.Uri.Host}");
|
||||
|
||||
try
|
||||
{
|
||||
CompleteWithFakeSuccess(request);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Couldn't synthesize the response — still don't send. The request's callback never
|
||||
// fires, so whatever promise the transport made stays pending; that's a stalled flush
|
||||
// coroutine at worst, versus telemetry leaving the box.
|
||||
Plugin.Log.LogWarning($"[ANALYTICS] blocked {request.Uri.Host} but could not fake a response: {e.Message}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void CompleteWithFakeSuccess(HTTPRequest request)
|
||||
{
|
||||
var body = FakeBodyFor(request);
|
||||
|
||||
var response = new HTTPResponse(request, new Il2CppSystem.IO.MemoryStream(), false, false)
|
||||
{
|
||||
StatusCode = 200,
|
||||
Message = "OK",
|
||||
Data = new Il2CppStructArray<byte>(Encoding.UTF8.GetBytes(body)),
|
||||
};
|
||||
|
||||
request.Response = response;
|
||||
request.State = HTTPRequestStates.Finished;
|
||||
|
||||
// BestHTTP would normally fire this from HTTPManager's update loop a frame or more later.
|
||||
// Firing it inline is safe here because the callback is assigned before SendRequest is
|
||||
// called, and the promise it resolves already exists by then.
|
||||
request.Callback?.Invoke(request, response);
|
||||
}
|
||||
|
||||
// Whatever the endpoint would have said on a good day. Amplitude's real shapes are known:
|
||||
// /identify answers with the literal "success", the v2 batch endpoint with a small JSON
|
||||
// envelope. The collector's shape isn't known, so we fall back to `{"success":true}` — the
|
||||
// envelope every first-party RecNet endpoint uses (it's what the real deviceId endpoint
|
||||
// returns, see the DUID case study in CLAUDE.md) and a far better guess than an empty body,
|
||||
// which the RecNet HTTP wrapper rejects outright with "Response was empty".
|
||||
private static string FakeBodyFor(HTTPRequest request)
|
||||
{
|
||||
if (!IsAmplitudeHost(request.Uri.Host))
|
||||
return "{\"success\":true}";
|
||||
|
||||
return request.Uri.AbsoluteUri.Contains("/identify", StringComparison.OrdinalIgnoreCase)
|
||||
? "success"
|
||||
: "{\"code\":200,\"events_ingested\":0,\"payload_size_bytes\":0,\"server_upload_time\":"
|
||||
+ DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Backtrace.Unity.Json;
|
||||
using Backtrace.Unity.Model;
|
||||
using HarmonyLib;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Backtrace crash reporting — the uploads to submit.backtrace.io.
|
||||
//
|
||||
// This one does not go through BestHTTP, so `AmplitudePatch`'s host block never sees it:
|
||||
// `Backtrace.Unity.dll` references `UnityEngine.UnityWebRequestModule` and nothing else HTTP-shaped.
|
||||
// The whole SDK is unobfuscated (it's a third-party package, so no per-build name churn to survive),
|
||||
// and every submission it makes — crash reports, minidumps, metrics — funnels through the four
|
||||
// `BacktraceHttpClient.Post` overloads. That's the concrete class; `IBacktraceHttpClient` is the
|
||||
// interface and patching it would silently never run (gotcha 3 in CLAUDE.md).
|
||||
//
|
||||
// The two overload shapes need different treatment, because of who owns the send:
|
||||
//
|
||||
// void Post(url, jObject, onComplete) - fire-and-forget, the SDK sends internally. We skip it and
|
||||
// invoke the callback with a 200 ourselves.
|
||||
// UnityWebRequest Post(...) x3 - builds the request and hands it back; *the caller* sends it
|
||||
// (`yield return request.SendWebRequest()`). Skipping the
|
||||
// original would hand the caller a null to dereference, so
|
||||
// instead we let it build whatever it likes and repoint the
|
||||
// finished request at a black hole.
|
||||
//
|
||||
// Not covered: `RecRoomNativeClient` installs a native crash handler, and a minidump uploaded from
|
||||
// native code on the next launch never passes through here. If submit.backtrace.io still shows a
|
||||
// multipart minidump POST with everything below firing, that's the path it took.
|
||||
[HarmonyPatch]
|
||||
public static class BacktracePatch
|
||||
{
|
||||
// Loopback port 1: nothing listens there, so the send fails with connection-refused in
|
||||
// microseconds without a packet leaving the machine, and the SDK takes its ordinary offline path.
|
||||
private const string BlackHoleUrl = "http://127.0.0.1:1/blocked-by-recnet-plugin";
|
||||
|
||||
private static readonly HashSet<string> _loggedBlocked = new();
|
||||
|
||||
// Fire-and-forget path (metrics). The SDK sends this one itself, so skipping the original is
|
||||
// enough — but the callback has to be answered or the submission queue keeps the batch it just
|
||||
// handed us and retries it forever. (statusCode, isError, response): a 200 with no error is what
|
||||
// it waits for before clearing the batch.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(BacktraceHttpClient), nameof(BacktraceHttpClient.Post),
|
||||
typeof(string), typeof(BacktraceJObject), typeof(Il2CppSystem.Action<long, bool, string>))]
|
||||
private static bool PostWithCallbackPrefix(string __0, Il2CppSystem.Action<long, bool, string> __2)
|
||||
{
|
||||
if (!Plugin.DisableTelemetry.Value)
|
||||
return true;
|
||||
|
||||
LogBlocked(__0);
|
||||
__2?.Invoke(200, false, "{}");
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(BacktraceHttpClient), nameof(BacktraceHttpClient.Post),
|
||||
typeof(string), typeof(BacktraceJObject))]
|
||||
private static void PostJObjectPostfix(string __0, UnityWebRequest __result) => Neuter(__0, __result);
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(BacktraceHttpClient), nameof(BacktraceHttpClient.Post),
|
||||
typeof(string), typeof(string),
|
||||
typeof(Il2CppSystem.Collections.Generic.IEnumerable<string>),
|
||||
typeof(Il2CppSystem.Collections.Generic.IDictionary<string, string>))]
|
||||
private static void PostJsonPostfix(string __0, UnityWebRequest __result) => Neuter(__0, __result);
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(BacktraceHttpClient), nameof(BacktraceHttpClient.Post),
|
||||
typeof(string), typeof(Il2CppSystem.Collections.Generic.List<IMultipartFormSection>))]
|
||||
private static void PostFormPostfix(string __0, UnityWebRequest __result) => Neuter(__0, __result);
|
||||
|
||||
// Leave the request the SDK built exactly as it is — handlers, headers, body — and change only
|
||||
// where it points. Rebuilding it ourselves would mean guessing which handlers the caller goes on
|
||||
// to dereference; this way the coroutine keeps its shape and just gets an error back.
|
||||
private static void Neuter(string url, UnityWebRequest request)
|
||||
{
|
||||
if (!Plugin.DisableTelemetry.Value || request == null)
|
||||
return;
|
||||
|
||||
LogBlocked(url);
|
||||
request.url = BlackHoleUrl;
|
||||
}
|
||||
|
||||
// Once per host normally, every submission under [Advanced] Debug — same rule as the analytics
|
||||
// host block, and for the same reason: a deduped line can't answer "did *this* upload get
|
||||
// dropped?" when you're staring at a proxy trace.
|
||||
private static void LogBlocked(string url)
|
||||
{
|
||||
if (Plugin.Debug.Value)
|
||||
{
|
||||
Plugin.Log.LogInfo($"[BACKTRACE] dropped submission to {url}");
|
||||
return;
|
||||
}
|
||||
|
||||
var host = HostOf(url);
|
||||
if (_loggedBlocked.Add(host))
|
||||
Plugin.Log.LogInfo($"[BACKTRACE] telemetry disabled — dropping submissions to {host}");
|
||||
}
|
||||
|
||||
private static string HostOf(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Uri(url).Host;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Test tool: persist a genuinely corrupt STORED device id on this machine, matching the friend's
|
||||
// condition (stored id truncated, current id healthy).
|
||||
//
|
||||
// We can't hand-craft the stored value: it lives in PlayerPrefs under an obfuscated key, encoded as a
|
||||
// CodeStage ObscuredString, and both the key and the encode method are renamed per game build. So we
|
||||
// let the game write it: WriteDUIDs() stores ObscuredString(SystemInfo.deviceUniqueIdentifier) under
|
||||
// the right key. We temporarily spoof deviceUniqueIdentifier to a truncated value around that one
|
||||
// call, so the game encrypts+stores a bad id with its own (unknown-to-us) key. Afterwards the spoof
|
||||
// is off, so the current id reads healthy again -> stored != current -> real mismatch on next launch.
|
||||
[HarmonyPatch]
|
||||
public static class CorruptDUIDPatch
|
||||
{
|
||||
// Only true for the duration of the WriteDUIDs() call below, so the SystemInfo getter is spoofed
|
||||
// exactly there and nowhere else.
|
||||
private static bool _spoofActive;
|
||||
private static string _spoofValue = "";
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(SystemInfo), "get_deviceUniqueIdentifier")]
|
||||
private static bool DeviceIdGetterPrefix(ref string __result)
|
||||
{
|
||||
if (!_spoofActive)
|
||||
return true;
|
||||
__result = _spoofValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if the corruption was written (so the caller marks it done and won't repeat).
|
||||
public static bool CorruptStored(GameObject cheatMgrGo)
|
||||
{
|
||||
var cm = cheatMgrGo.GetComponent<CheatManager>() ?? cheatMgrGo.GetComponentInChildren<CheatManager>();
|
||||
if (cm == null)
|
||||
{
|
||||
Plugin.Log.LogError("[CORRUPT] could not find CheatManager component; nothing written");
|
||||
return false;
|
||||
}
|
||||
|
||||
var real = SystemInfo.deviceUniqueIdentifier; // spoof off -> real id
|
||||
var bad = real is { Length: >= 7 } ? real.Substring(0, 7) : "badduid";
|
||||
|
||||
_spoofValue = bad;
|
||||
_spoofActive = true;
|
||||
try
|
||||
{
|
||||
cm.WriteDUIDs(); // encodes+stores ObscuredString(bad) under the real key
|
||||
}
|
||||
finally
|
||||
{
|
||||
_spoofActive = false;
|
||||
}
|
||||
|
||||
Plugin.Log.LogWarning($"[CORRUPT] wrote truncated stored DUID = \"{bad}\" (real id = \"{real}\"). " +
|
||||
"Set 'Corrupt Stored DUID' back to false and relaunch to drive the real mismatch path.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Undo: overwrite the stored value with the real id by calling WriteDUIDs with the spoof off.
|
||||
public static bool RestoreStored(GameObject cheatMgrGo)
|
||||
{
|
||||
var cm = cheatMgrGo.GetComponent<CheatManager>() ?? cheatMgrGo.GetComponentInChildren<CheatManager>();
|
||||
if (cm == null)
|
||||
{
|
||||
Plugin.Log.LogError("[CORRUPT] could not find CheatManager component; nothing restored");
|
||||
return false;
|
||||
}
|
||||
|
||||
cm.WriteDUIDs(); // spoof off -> stores ObscuredString(real deviceUniqueIdentifier)
|
||||
Plugin.Log.LogWarning($"[CORRUPT] restored stored DUID to real id = \"{SystemInfo.deviceUniqueIdentifier}\". " +
|
||||
"Set 'Restore Stored DUID' back to false.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using HarmonyLib;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Controls CheatManager.CheckForDUIDMismatch, which returns true when the machine's stored device id
|
||||
// differs from the freshly-derived one. A true result sends the client down the migration path that
|
||||
// POSTs PlayerReporting/v1/deviceId and then stalls on Create Account.
|
||||
//
|
||||
// Three modes, chosen by config:
|
||||
// Simulate = true -> force TRUE (fake a mismatch to reproduce the hang without a corrupt value)
|
||||
// Suppress = true -> force FALSE (the workaround fix: never migrate, never hang)
|
||||
// both false -> pass through, let the REAL check run against the actual stored value
|
||||
// (needed to observe a genuinely corrupt stored id, e.g. after Corrupt Stored DUID)
|
||||
//
|
||||
// Patch the concrete CheatManager method, NOT the abstract PGECJHKNIEN interface, or the prefix
|
||||
// never runs.
|
||||
[HarmonyPatch]
|
||||
public static class DUIDMismatchPatch
|
||||
{
|
||||
private const string SimulatedStoredDeviceId = "491e8b9";
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CheatManager), "CheckForDUIDMismatch")]
|
||||
// __0 = the out-param (positional). Its obfuscated name changes every game build, so binding it
|
||||
// by name throws "Parameter ... not found" on upgrade.
|
||||
private static bool Prefix(ref string __0, ref bool __result)
|
||||
{
|
||||
if (Plugin.SimulateDUIDMismatch.Value)
|
||||
{
|
||||
__0 = SimulatedStoredDeviceId;
|
||||
__result = true;
|
||||
Plugin.Log.LogWarning($"[DUID] simulating mismatch, stored id = {SimulatedStoredDeviceId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Plugin.SuppressDUIDMismatch.Value)
|
||||
{
|
||||
__0 = string.Empty;
|
||||
__result = false;
|
||||
Plugin.Log.LogInfo("[DUID] mismatch check forced to false (suppressed)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pass through to the real check.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Diagnostic only. Two jobs:
|
||||
// 1. Show where the stored device id lives, by logging PlayerPrefs reads/writes.
|
||||
// 2. Show how far the DUID migration branch gets, by logging CheatManager's other DUID methods.
|
||||
// If WriteDUIDs() never fires after the deviceId POST, the flow stalls before it.
|
||||
[HarmonyPatch]
|
||||
public static class DUIDProbePatch
|
||||
{
|
||||
// PlayerPrefs.GetString is called constantly, so log each key only once — except device/DUID
|
||||
// keys, which we always log so we can watch them change across the migration.
|
||||
private static readonly HashSet<string> SeenKeys = new();
|
||||
|
||||
private static bool IsInteresting(string key) =>
|
||||
key != null && (key.Contains("DUID") || key.Contains("Duid") || key.Contains("duid")
|
||||
|| key.Contains("Device") || key.Contains("device")
|
||||
|| key.Contains("Anon") || key.Contains("anon"));
|
||||
|
||||
private static void Note(string op, string key, string value)
|
||||
{
|
||||
if (IsInteresting(key))
|
||||
Plugin.Log.LogWarning($"[DUID-PROBE] {op} {key} = \"{value}\"");
|
||||
else if (SeenKeys.Add($"{op}:{key}"))
|
||||
Plugin.Log.LogInfo($"[DUID-PROBE] {op} {key} = \"{value}\"");
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(PlayerPrefs), nameof(PlayerPrefs.GetString), [typeof(string)])]
|
||||
private static void GetStringPostfix(string key, string __result) => Note("get", key, __result);
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(PlayerPrefs), nameof(PlayerPrefs.GetString), [typeof(string), typeof(string)])]
|
||||
private static void GetStringDefaultPostfix(string key, string __result) => Note("get", key, __result);
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlayerPrefs), nameof(PlayerPrefs.SetString))]
|
||||
private static void SetStringPrefix(string key, string value) => Note("SET", key, value);
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlayerPrefs), nameof(PlayerPrefs.DeleteKey))]
|
||||
private static void DeleteKeyPrefix(string key) => Note("DEL", key, "<deleted>");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CheatManager), "WriteDUIDs")]
|
||||
private static void WriteDUIDsPrefix() => Plugin.Log.LogWarning("[DUID-PROBE] WriteDUIDs() called");
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CheatManager), "WriteDUIDs")]
|
||||
private static void WriteDUIDsPostfix() => Plugin.Log.LogWarning("[DUID-PROBE] WriteDUIDs() returned");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CheatManager), "ClearDUIDs")]
|
||||
private static void ClearDUIDsPrefix() => Plugin.Log.LogWarning("[DUID-PROBE] ClearDUIDs() called");
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using BestHTTP;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Experiment harness for the Create Account hang.
|
||||
//
|
||||
// On a device-id mismatch the client POSTs PlayerReporting/v1/deviceId, the server answers
|
||||
// 200 {"success":true}, and then the client stops: CheatManager.WriteDUIDs() is never called, so the
|
||||
// new id is never persisted and the flow never reaches create_account. That means the client can't
|
||||
// proceed on what it got back.
|
||||
//
|
||||
// This rewrites that one response body before the game sees it, so response shapes can be tried
|
||||
// without redeploying the server. WriteDUIDs() appearing in the log (see DUIDProbePatch) is the
|
||||
// pass signal: it means the client accepted the response and resumed the migration.
|
||||
[HarmonyPatch]
|
||||
public static class DeviceIdResponsePatch
|
||||
{
|
||||
private const string Endpoint = "/PlayerReporting/v1/deviceId";
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(HTTPManager), "SendRequest", [typeof(HTTPRequest)])]
|
||||
private static void Prefix(HTTPRequest request)
|
||||
{
|
||||
if (!request.Uri.AbsoluteUri.Contains(Endpoint, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
var original = request.Callback;
|
||||
|
||||
// Whether the game attached a completion callback at all. If this logs False, the client is
|
||||
// not waiting on this request through the callback API and the "stuck on the response" model
|
||||
// is wrong -- that would be worth knowing before chasing response shapes any further.
|
||||
Plugin.Log.LogWarning($"[DEVICEID] request seen; game callback attached = {original != null}");
|
||||
|
||||
var body = Plugin.DeviceIdResponseOverride.Value;
|
||||
if (string.IsNullOrEmpty(body))
|
||||
return;
|
||||
|
||||
var status = Plugin.DeviceIdResponseStatus.Value;
|
||||
|
||||
request.Callback = DelegateSupport.ConvertDelegate<OnRequestFinishedDelegate>(
|
||||
(Action<HTTPRequest, HTTPResponse>)((req, resp) =>
|
||||
{
|
||||
if (resp != null)
|
||||
{
|
||||
// Set both: DataAsText is computed from Data but cached in dataAsText once read,
|
||||
// and our own HTTP logger may already have read it.
|
||||
resp.Data = new Il2CppStructArray<byte>(Encoding.UTF8.GetBytes(body));
|
||||
resp.dataAsText = body;
|
||||
resp.StatusCode = status;
|
||||
Plugin.Log.LogWarning($"[DEVICEID] response overridden -> {status} {body}");
|
||||
}
|
||||
|
||||
original?.Invoke(req, resp);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using HarmonyLib;
|
||||
using Org.BouncyCastle.Crypto.Tls;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
/**
|
||||
Disables TLS certificate pinning. Even though we connect over SSL it seems some certificates
|
||||
might be pinned.
|
||||
*/
|
||||
public class DisableTLSPinning
|
||||
{
|
||||
[HarmonyPatch(typeof(LegacyTlsAuthentication), "NotifyServerCertificate")]
|
||||
public class TlsPatch
|
||||
{
|
||||
private static bool Prefix()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using HarmonyLib;
|
||||
using RecRoom.AntiCheat;
|
||||
using System.Text;
|
||||
using Il2CppSystem;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class EACPatches
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
// The "is ready" check: the only static, 0-param bool method on EACManager that isn't a property
|
||||
// getter. 20230414 build: MCFIOBHCFBB (was IMMGELPFGCK, was FJLMLEPOKGE). Method names here are
|
||||
// strings, so a rename is not a compile error — it shows up as a HarmonyX "method not found" at load.
|
||||
[HarmonyPatch(typeof(EACManager), "MCFIOBHCFBB")]
|
||||
private static bool IsReadyPatch(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(EACManager), "GenerateChallengeResponse")]
|
||||
// __0 = the challenge string (positional); obfuscated param names shift between game builds.
|
||||
private static bool GenerateChallengeResponsePatch(string __0, ref string __result)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(__0))
|
||||
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes(__0));
|
||||
else
|
||||
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes("nothing"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
using Il2CppSystem.Security.Cryptography;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Image signing: the client verifies images against an RSA public key whose modulus is a string
|
||||
// literal in global-metadata.dat. Patching that literal is fragile, so we intervene at the framework
|
||||
// level instead, by forcing the mscorlib RSA verify to succeed.
|
||||
//
|
||||
// One knob, see [Signing] in the .cfg:
|
||||
// Disable Signature Verification -> THE FIX (default true). Forces the RSA verify to succeed, so
|
||||
// the modulus never has to match and unsigned images load. This
|
||||
// self-hosted setup does not use image signing.
|
||||
//
|
||||
// If images ever stop loading with this on, the client has moved verification off mscorlib RSA onto
|
||||
// BestHTTP.SecureProtocol.Org.BouncyCastle; the equivalent hooks there are the concrete
|
||||
// RsaDigestSigner/PssSigner.VerifySignature (NOT the abstract ISigner "interface", which never
|
||||
// dispatches).
|
||||
[HarmonyPatch]
|
||||
public static class ImageSigningPatch
|
||||
{
|
||||
private static bool _loggedForced;
|
||||
|
||||
// Forces EVERY mscorlib RSA verification to succeed, not just image signatures. BestHTTP's TLS
|
||||
// uses its own bundled BouncyCastle rather than mscorlib RSA, so this should not touch
|
||||
// certificate validation — but it is a blunt instrument, so it stays behind a config knob rather
|
||||
// than being unconditional.
|
||||
private static bool ForceVerifyTrue(ref bool __result)
|
||||
{
|
||||
if (!Plugin.DisableSignatureVerification.Value)
|
||||
return true;
|
||||
|
||||
if (!_loggedForced)
|
||||
{
|
||||
_loggedForced = true;
|
||||
Plugin.Log.LogWarning("[SIG] signature verification disabled — RSA verify forced to true");
|
||||
}
|
||||
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RSACryptoServiceProvider), nameof(RSACryptoServiceProvider.VerifyData))]
|
||||
private static bool VerifyDataPrefix(ref bool __result) => ForceVerifyTrue(ref __result);
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RSACryptoServiceProvider), nameof(RSACryptoServiceProvider.VerifyHash),
|
||||
[typeof(Il2CppStructArray<byte>), typeof(int), typeof(Il2CppStructArray<byte>)])]
|
||||
private static bool VerifyHashPrefix(ref bool __result) => ForceVerifyTrue(ref __result);
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RSACryptoServiceProvider), nameof(RSACryptoServiceProvider.VerifyHash),
|
||||
[typeof(Il2CppStructArray<byte>), typeof(Il2CppStructArray<byte>), typeof(HashAlgorithmName),
|
||||
typeof(RSASignaturePadding)])]
|
||||
private static bool VerifyHashPaddingPrefix(ref bool __result) => ForceVerifyTrue(ref __result);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using ExitGames.Client.Photon;
|
||||
using HarmonyLib;
|
||||
using Photon.Realtime;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
/**
|
||||
Patches Photon to use the App IDs and server hostname/port specified in the plugin config.
|
||||
*/
|
||||
// Obfuscated names shift every game build. Re-resolve by signature: the target is the only
|
||||
// instance, 0-param method returning Photon.Realtime.AppSettings in Assembly-CSharp.
|
||||
// 20230414 build: HPEENKELKDJ.MGKINLFMJLB (was LEALBOODIEE.GBNKOFMAJPA, was GPFPFDBGCEK.AMOHMPKKGHL).
|
||||
[HarmonyPatch(typeof(HPEENKELKDJ), "MGKINLFMJLB")]
|
||||
public class PhotonPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
private static void Postfix(ref AppSettings __result)
|
||||
{
|
||||
if (__result != null)
|
||||
{
|
||||
__result.AppIdRealtime = Plugin.AppIdRT.Value;
|
||||
__result.AppIdVoice = Plugin.AppIdVoice.Value;
|
||||
__result.AppIdChat = Plugin.AppIdChat.Value;
|
||||
__result.FixedRegion = "us";
|
||||
__result.UseNameServer = true;
|
||||
__result.Protocol = ConnectionProtocol.Udp;
|
||||
|
||||
if (Plugin.EnableAdvancedSettings.Value)
|
||||
{
|
||||
__result.Server = Plugin.PhotonHostname.Value;
|
||||
__result.Port = Plugin.PhotonPort.Value == 0
|
||||
? 4533
|
||||
: Plugin.PhotonPort.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
using System;
|
||||
using BestHTTP;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
/**
|
||||
Intercept a variety of HTTP requests and rewrite them to point to our own custom server.
|
||||
*/
|
||||
public class SendRequestPatch
|
||||
{
|
||||
// Official name server host to redirect away from, swapped for the custom server.
|
||||
private const string OfficialNameServer = "ns.rec.net";
|
||||
|
||||
// Skip when HTTP-logging so we don't spam the logs.
|
||||
private static readonly string[] LogIgnoreSubstrings =
|
||||
{
|
||||
"/api/gamesight/event",
|
||||
"/data/heartbeat",
|
||||
"/identify",
|
||||
"/httpapi",
|
||||
"/data/event",
|
||||
};
|
||||
|
||||
private static bool IsIgnoredForLogging(string url)
|
||||
{
|
||||
foreach (var s in LogIgnoreSubstrings)
|
||||
if (url.Contains(s, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cap logged bodies so a large response/request doesn't flood the log.
|
||||
private const int MaxLoggedBodyLength = 10000;
|
||||
|
||||
private static string Truncate(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s) || s.Length <= MaxLoggedBodyLength)
|
||||
return s;
|
||||
return s.Substring(0, MaxLoggedBodyLength) + $"... <truncated {s.Length - MaxLoggedBodyLength} chars>";
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(HTTPManager), "SendRequest", [typeof(HTTPRequest)])]
|
||||
public class ConnectToRecNetPatch
|
||||
{
|
||||
private static void Prefix(ref HTTPRequest request)
|
||||
{
|
||||
var debug = Plugin.Debug.Value && !IsIgnoredForLogging(request.Uri.AbsoluteUri);
|
||||
|
||||
if (debug)
|
||||
{
|
||||
var entityBody = request.GetEntityBody();
|
||||
string body;
|
||||
if (entityBody == null)
|
||||
body = "<none>";
|
||||
else if (IsBinaryContentType(request.GetFirstHeaderValue("content-type")) || LooksBinary(entityBody))
|
||||
body = BinaryPreview(entityBody);
|
||||
else
|
||||
body = System.Text.Encoding.UTF8.GetString(entityBody);
|
||||
Plugin.Log.LogInfo($"[HTTP] {request.MethodType} {request.Uri.AbsoluteUri} body={Truncate(body)}");
|
||||
}
|
||||
|
||||
var host = request.Uri.Host;
|
||||
if (host == OfficialNameServer)
|
||||
{
|
||||
// Redirect the nameserver lookup to the custom server, swapping only the host.
|
||||
var newHost = new System.Uri(Plugin.ServerHostname.Value).Host;
|
||||
var builder = new Il2CppSystem.UriBuilder(request.Uri) { Host = newHost };
|
||||
request.Uri = builder.Uri;
|
||||
|
||||
if (debug)
|
||||
Plugin.Log.LogInfo($"[HTTP] intercepted {host} -> {newHost}");
|
||||
}
|
||||
|
||||
if (debug)
|
||||
LogResponseWhenDone(request);
|
||||
}
|
||||
}
|
||||
|
||||
// Wraps the request's completion callback so we log the response (status + body) when it
|
||||
// finishes, then forwards to the game's original callback. This is how we see *which*
|
||||
// request comes back empty (RecNet throws "Response was empty" on a blank body).
|
||||
private static void LogResponseWhenDone(HTTPRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var original = request.Callback;
|
||||
var url = request.Uri.AbsoluteUri;
|
||||
|
||||
request.Callback = DelegateSupport.ConvertDelegate<OnRequestFinishedDelegate>(
|
||||
(Action<HTTPRequest, HTTPResponse>)((req, resp) =>
|
||||
{
|
||||
if (resp == null)
|
||||
Plugin.Log.LogWarning($"[HTTP] <- {url} NO RESPONSE (state={req.State})");
|
||||
else
|
||||
{
|
||||
string text;
|
||||
if (IsBinaryContentType(resp.GetFirstHeaderValue("content-type")))
|
||||
text = "<binary>";
|
||||
else
|
||||
{
|
||||
text = resp.DataAsText;
|
||||
if (string.IsNullOrEmpty(text)) text = "<empty>";
|
||||
}
|
||||
var msg = $"[HTTP] <- {resp.StatusCode} {url} body={Truncate(text)}";
|
||||
if (resp.StatusCode is >= 200 and < 300)
|
||||
Plugin.Log.LogInfo(msg);
|
||||
else
|
||||
Plugin.Log.LogError(msg);
|
||||
}
|
||||
|
||||
original?.Invoke(req, resp);
|
||||
}));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.LogError($"[HTTP] failed to attach response logger: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Content-Type prefixes/keywords we treat as textual; anything else is logged as <binary> so we
|
||||
// don't dump image/asset bytes into the log.
|
||||
private static readonly string[] TextContentTypes =
|
||||
{
|
||||
"text/", "application/json", "application/xml", "application/javascript",
|
||||
"application/x-www-form-urlencoded", "+json", "+xml",
|
||||
};
|
||||
|
||||
// True if the body is (probably) binary and shouldn't be logged as text. Defaults to text when
|
||||
// there's no Content-Type, so we err toward logging rather than hiding.
|
||||
private static bool IsBinaryContentType(string contentType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentType)) return false;
|
||||
|
||||
foreach (var t in TextContentTypes)
|
||||
if (contentType.Contains(t, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Render the leading bytes of a binary body as text so structured framing (e.g. multipart form
|
||||
// boundaries and part headers) stays readable, while raw bytes are shown as \xNN escapes. Capped
|
||||
// at MaxLoggedBodyLength since the interesting framing is at the front.
|
||||
private static string BinaryPreview(byte[] data)
|
||||
{
|
||||
if (data.Length == 0) return "<binary empty>";
|
||||
|
||||
var sb = new System.Text.StringBuilder(MaxLoggedBodyLength + 32);
|
||||
sb.Append("<binary ").Append(data.Length).Append(" bytes> ");
|
||||
var i = 0;
|
||||
// Cap on rendered length, not byte count: escapes expand a byte to 4 chars, so this keeps the
|
||||
// preview near MaxLoggedBodyLength and avoids a second pass by Truncate at the log site.
|
||||
for (; i < data.Length && sb.Length < MaxLoggedBodyLength; i++)
|
||||
{
|
||||
var b = data[i];
|
||||
if (b == 0x09 || b == 0x0A || b == 0x0D || (b >= 0x20 && b < 0x7F))
|
||||
sb.Append((char)b);
|
||||
else
|
||||
sb.Append("\\x").Append(b.ToString("x2"));
|
||||
}
|
||||
if (i < data.Length)
|
||||
sb.Append($"... <truncated {data.Length - i} bytes>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Content sniff for raw request bytes — the Content-Type header isn't reliably set at
|
||||
// SendRequest time (e.g. multipart form bodies set it lazily, and the body still embeds the
|
||||
// raw image), so look at the bytes: a NUL byte, or a high ratio of non-text control bytes in
|
||||
// the first chunk, means it's binary (or binary-mixed like a multipart upload).
|
||||
private static bool LooksBinary(byte[] data)
|
||||
{
|
||||
if (data.Length == 0) return false;
|
||||
|
||||
var sample = Math.Min(data.Length, 4096);
|
||||
var nonText = 0;
|
||||
for (var i = 0; i < sample; i++)
|
||||
{
|
||||
var b = data[i];
|
||||
if (b == 0) return true;
|
||||
// Control chars other than tab/newline/carriage-return.
|
||||
if (b < 0x20 && b != 0x09 && b != 0x0A && b != 0x0D) nonText++;
|
||||
}
|
||||
return nonText * 100 / sample > 10;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Unity's own telemetry — the uploads to perf-events.cloud.unity3d.com.
|
||||
//
|
||||
// !! THIS DOES NOT WORK on the 20230414 build, and that is a known, accepted limitation — don't spend
|
||||
// another afternoon on it. Confirmed at runtime: the setters below are refused, `enabled` reads back
|
||||
// True on all five attempts, and perf-events uploads keep flowing. It's left in because it costs
|
||||
// nothing, is the correct thing to do if a future build stops refusing, and the read-back logs the
|
||||
// truth either way rather than pretending. The parts of `Disable Telemetry` that carry the actual win
|
||||
// are Amplitude, the collector and Backtrace — all confirmed dropping — and they take out the bulk of
|
||||
// the noise. If perf-events ever has to go for real, it needs a hosts-file/DNS block or a native hook;
|
||||
// there is no managed lever.
|
||||
//
|
||||
// There is nothing to hook here, and that's the point: `UnityEngine.Analytics.Analytics` and
|
||||
// `PerformanceReporting` are thin managed shims over native engine code, and the uploads happen inside
|
||||
// the player, not on any managed send path a Harmony prefix could sit on. Blocking this one at the HTTP
|
||||
// layer is equally hopeless — it never touches BestHTTP or UnityWebRequest. What it *does* have is a
|
||||
// documented opt-out, so we flip the switches at startup and read them back.
|
||||
//
|
||||
// Performance Reporting is the exception/crash reporter, Analytics is the event stream; both feed
|
||||
// perf-events, so both go off. `limitUserTracking` and `deviceStatsEnabled` cover the case where
|
||||
// something re-enables the event stream behind our back — with those set, what it can collect is
|
||||
// nothing worth sending.
|
||||
internal static class UnityTelemetryPatch
|
||||
{
|
||||
// Applied from Plugin.Load and again on each scene load until it sticks — these are native
|
||||
// properties whose setters can be refused (service not initialised yet, build flags), so
|
||||
// "set it once at load and assume" is exactly how this silently does nothing.
|
||||
private const int MaxAttempts = 5;
|
||||
|
||||
private static bool _done;
|
||||
private static int _attempts;
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
if (_done || !Plugin.DisableTelemetry.Value || _attempts >= MaxAttempts)
|
||||
return;
|
||||
|
||||
_attempts++;
|
||||
|
||||
try
|
||||
{
|
||||
UnityEngine.Analytics.PerformanceReporting.enabled = false;
|
||||
UnityEngine.Analytics.Analytics.enabled = false;
|
||||
UnityEngine.Analytics.Analytics.deviceStatsEnabled = false;
|
||||
UnityEngine.Analytics.Analytics.limitUserTracking = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// No _done: a later scene load gets another go, up to MaxAttempts.
|
||||
if (_attempts >= MaxAttempts)
|
||||
Plugin.Log.LogWarning($"[UNITY-TELEMETRY] gave up flipping the opt-out switches after {_attempts} attempts: {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
// The read-back is the proof, not the assignment above. A refused setter is silent.
|
||||
var perf = UnityEngine.Analytics.PerformanceReporting.enabled;
|
||||
var analytics = UnityEngine.Analytics.Analytics.enabled;
|
||||
|
||||
_done = !perf && !analytics;
|
||||
|
||||
if (_done)
|
||||
Plugin.Log.LogInfo(
|
||||
$"[UNITY-TELEMETRY] disabled — PerformanceReporting.enabled={perf} Analytics.enabled={analytics} " +
|
||||
$"deviceStats={UnityEngine.Analytics.Analytics.deviceStatsEnabled} " +
|
||||
$"limitUserTracking={UnityEngine.Analytics.Analytics.limitUserTracking}");
|
||||
else if (_attempts >= MaxAttempts)
|
||||
// Info, not a warning: this is the known outcome on this build (see the header), not a
|
||||
// fault to go chasing. It stays logged so a build that *does* accept the switches is
|
||||
// visible as a change rather than a surprise.
|
||||
Plugin.Log.LogInfo(
|
||||
$"[UNITY-TELEMETRY] switches refused after {_attempts} attempts — " +
|
||||
$"PerformanceReporting.enabled={perf} Analytics.enabled={analytics}. " +
|
||||
"Known limitation: perf-events.cloud.unity3d.com uploads continue. The rest of Disable Telemetry is unaffected.");
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using System;
|
||||
using BepInEx;
|
||||
using BepInEx.Configuration;
|
||||
using BepInEx.Logging;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace RecNetPlugin;
|
||||
|
||||
[BepInPlugin("net.rec.plugin", "RecNet Plugin", "1.0.0")]
|
||||
public class Plugin : BasePlugin
|
||||
{
|
||||
internal static new ManualLogSource Log;
|
||||
|
||||
public static ConfigEntry<string> AppIdRT { get; private set; }
|
||||
public static ConfigEntry<string> AppIdVoice { get; private set; }
|
||||
public static ConfigEntry<string> AppIdChat { get; private set; }
|
||||
public static ConfigEntry<string> ServerHostname { get; private set; }
|
||||
public static ConfigEntry<bool> EnableAdvancedSettings { get; private set; }
|
||||
public static ConfigEntry<string> PhotonHostname { get; private set; }
|
||||
public static ConfigEntry<int> PhotonPort { get; private set; }
|
||||
public static ConfigEntry<bool> Debug { get; private set; }
|
||||
public static ConfigEntry<bool> SimulateDUIDMismatch { get; private set; }
|
||||
public static ConfigEntry<bool> SuppressDUIDMismatch { get; private set; }
|
||||
public static ConfigEntry<bool> CorruptStoredDUID { get; private set; }
|
||||
public static ConfigEntry<bool> RestoreStoredDUID { get; private set; }
|
||||
public static ConfigEntry<string> DeviceIdResponseOverride { get; private set; }
|
||||
public static ConfigEntry<int> DeviceIdResponseStatus { get; private set; }
|
||||
public static ConfigEntry<bool> DisableSignatureVerification { get; private set; }
|
||||
public static ConfigEntry<bool> DisableTelemetry { get; private set; }
|
||||
|
||||
private static bool _corruptDone;
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
Log = base.Log;
|
||||
|
||||
AppIdRT = Config.Bind("Photon", "App Id Realtime", "", "Photon Realtime App ID");
|
||||
AppIdVoice = Config.Bind("Photon", "App Id Voice", "", "Photon Voice App ID");
|
||||
AppIdChat = Config.Bind("Photon", "App Id Chat", "", "Photon Chat App ID");
|
||||
EnableAdvancedSettings = Config.Bind("Advanced", "Enabled Advanced Settings", false, "Allows other fields below in the advanced section to be modified.");
|
||||
PhotonHostname = Config.Bind("Advanced", "Photon NameServer", "", "Custom Photon NameServer");
|
||||
PhotonPort = Config.Bind("Advanced", "Photon NameServer Port", 0, "Custom Photon NameServer Port (if 0, it will be default)");
|
||||
ServerHostname = Config.Bind("Server", "RecNet NameServer Host", "https://ns.rec.net", "Host for the RecNet NameServer.");
|
||||
Debug = Config.Bind("Advanced", "Debug", false, "Show debug logs (HTTP tracing, etc. WARNING: will include sensitive information such as passwords and auth tokens in the logs, be careful when sharing them!)");
|
||||
SimulateDUIDMismatch = Config.Bind("Advanced", "Simulate DUID Mismatch", false, "Force CheckForDUIDMismatch to return TRUE (fakes the comparison only). Reproduces the hang path but does not corrupt any stored value. Leave false for normal play.");
|
||||
SuppressDUIDMismatch = Config.Bind("Advanced", "Suppress DUID Mismatch", true, "Force CheckForDUIDMismatch to return FALSE (the workaround fix, ON by default): the client never migrates and never takes the Create Account hang path. No-op on healthy machines (the real check returns false anyway); on mismatched machines it skips the hang. Set false only to observe the real mismatch behavior for debugging.");
|
||||
CorruptStoredDUID = Config.Bind("Advanced", "Corrupt Stored DUID", false, "ONE-SHOT TEST: on next launch, write a truncated device id into the DUID pref via the game's own WriteDUIDs, producing a genuinely corrupt STORED value (real current id) — exactly the friend's condition. After it logs '[CORRUPT] wrote', set this back to false and relaunch to drive the real mismatch path. Use 'Restore Stored DUID' to undo.");
|
||||
RestoreStoredDUID = Config.Bind("Advanced", "Restore Stored DUID", false, "ONE-SHOT UNDO: on next launch, call WriteDUIDs with the real device id, overwriting any corrupt stored value with a good one. Set back to false after it logs '[CORRUPT] restored'.");
|
||||
DeviceIdResponseOverride = Config.Bind("Advanced", "DeviceId Response Override", "", "Replace the body of the PlayerReporting/v1/deviceId response with this text, to test what shape the client will accept. Empty = leave the server's response alone.");
|
||||
DeviceIdResponseStatus = Config.Bind("Advanced", "DeviceId Response Status", 200, "HTTP status to force on the PlayerReporting/v1/deviceId response. Only applies when the override body is set.");
|
||||
|
||||
DisableSignatureVerification = Config.Bind("Signing", "Disable Signature Verification", true, "Force RSA signature verification to succeed (ON by default), so the client stops checking that images are signed with Rec Room's private key. This is what lets a self-hosted server serve its own images without the baked-in modulus matching. NOTE: this forces ALL mscorlib RSA verification to pass, not just image signatures — that breadth is deliberate, see CLAUDE.md.");
|
||||
|
||||
DisableTelemetry = Config.Bind("Analytics", "Disable Telemetry", true, "Stop the client reporting to third-party telemetry services (ON by default). Covers: Amplitude analytics (every AmplitudeAnalyticsClient.Log* call plus any upload to amplitude.com, so batches queued in earlier sessions can't be flushed later); the data-collection endpoint (any host whose name starts with 'datacollection', e.g. datacollection.recflare.net); and Backtrace crash reports, minidumps and metrics to submit.backtrace.io. Blocked uploads get a synthetic 200 so the client carries on as if they had been accepted. It also asks Unity's own Analytics and Performance Reporting to switch off, but that part is KNOWN NOT TO WORK on this game build — those send from native engine code and the opt-out is refused, so perf-events.cloud.unity3d.com uploads continue; see the [UNITY-TELEMETRY] line in LogOutput.log. Not covered: RudderStack, gamesight, and minidumps sent by the native crash handler on the launch after a hard crash. Set false to let all of it through.");
|
||||
|
||||
// Not a patch — Unity's telemetry has a real opt-out, so we just set it. Retried from
|
||||
// OnSceneLoaded until it takes, since the native setters can refuse this early.
|
||||
Patches.UnityTelemetryPatch.Apply();
|
||||
|
||||
Harmony.CreateAndPatchAll(typeof(Plugin).Assembly);
|
||||
|
||||
SceneManager.sceneLoaded += (Action<Scene, LoadSceneMode>)OnSceneLoaded;
|
||||
}
|
||||
|
||||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
// No-op once the switches have stuck; must run before the early return below.
|
||||
Patches.UnityTelemetryPatch.Apply();
|
||||
|
||||
// CheatManager boots us out of rooms when it runs, but it's ALSO the DUID service the DI
|
||||
// container resolves for account creation / login (destroying it removes that service).
|
||||
// So instead of destroying it, *deactivate* the GameObject: it stops running (no Update /
|
||||
// coroutines, so no boot) while the component still exists, so the DI container can still
|
||||
// resolve PGECJHKNIEN and call its DUID methods. It's recreated per scene, so deactivate
|
||||
// each freshly-spawned (active) instance on every load. (GameObject.Find only returns active
|
||||
// objects, so once deactivated it isn't found again.)
|
||||
var cheatMgr = GameObject.Find("GameRoot/(Startup)(Clone)/Core Systems/[CheatManager]");
|
||||
if (cheatMgr == null)
|
||||
return;
|
||||
|
||||
// One-shot corruption for testing: must run while the component is still active (before we
|
||||
// deactivate it below), because it calls the live CheatManager.WriteDUIDs().
|
||||
if (CorruptStoredDUID.Value && !_corruptDone)
|
||||
_corruptDone = Patches.CorruptDUIDPatch.CorruptStored(cheatMgr);
|
||||
else if (RestoreStoredDUID.Value && !_corruptDone)
|
||||
_corruptDone = Patches.CorruptDUIDPatch.RestoreStored(cheatMgr);
|
||||
|
||||
cheatMgr.SetActive(false);
|
||||
Log.LogInfo("cheatmanager deactivated");
|
||||
}
|
||||
}
|
||||
@@ -1,175 +1,142 @@
|
||||
# RecNet Plugin
|
||||
# RR Redirector (native)
|
||||
|
||||
A [BepInEx 6](https://github.com/BepInEx/BepInEx) (IL2CPP) plugin that points the Rec Room client at a self-hosted / private server.
|
||||
A native (C/Win32) DLL that points the Rec Room client at a self-hosted server, **without any
|
||||
managed mod loader**. BepInEx 6 and MelonLoader both fail on current Rec Room builds (crash in
|
||||
`il2cpp_init` / loader trips the anti-cheat memory-integrity scan). This build sidesteps that: it is
|
||||
loaded as a `version.dll` proxy and applies its patches — Winsock DNS, the HTTP request URL, TLS
|
||||
pinning, the memory-integrity scan and EAC — directly in native code.
|
||||
|
||||
It does this entirely client-side with [Harmony](https://harmony.pardeike.net/) patches — no game files are modified on disk. The plugin rewrites the RecNet name-server lookups, swaps in your own Photon credentials, and disables the client-side guards (EasyAntiCheat, TLS certificate pinning, image signature verification) that would otherwise reject a non-official server.
|
||||
## How it works
|
||||
|
||||
> ⚠️ This disables anti-cheat, certificate validation, and RSA signature verification on the client. Use at your own risk.
|
||||
1. **Loading vector.** `RecRoom.exe`/`UnityPlayer.dll` import `VERSION.dll` by name, and the loader
|
||||
searches the game folder before `System32`. We ship our own `version.dll` there; each of its 17
|
||||
exports is a thin wrapper that lazily loads the real system `version.dll` (by full path, so no
|
||||
recursion) and calls through, so the game keeps working. Its `DllMain` starts the hook thread.
|
||||
Loads very early, before `UnityPlayer.dll`. Self-contained — nothing else to ship.
|
||||
|
||||
## Projects
|
||||
`RecRoom.exe` spawns `UnityCrashHandler64.exe` from the same folder, so our DLL loads there too.
|
||||
The hook thread checks the host executable and exits immediately in anything but `RecRoom.exe` —
|
||||
otherwise every launch opened a second debug console and left a stray process waiting on Unity.
|
||||
|
||||
[<img width="100" height="100" alt="image" src="https://github.com/user-attachments/assets/f0b91aa3-49f5-4077-8eb9-5ae676888709" />](https://www.recflare.net)
|
||||
2. **DNS host rewrite** (`src/hooks/dns_hook.c`). Detours `ws2_32!getaddrinfo`. A lookup for an exact
|
||||
`from` host in `redirector.json` (e.g. `ns.rec.net`) is resolved as its `to` host
|
||||
(`ns.recflare.net`) instead — we hand the rewritten name to real DNS, so the client reaches the
|
||||
target's *current* IP (survives dynamic IPs) rather than a pinned address. Surgical: only the
|
||||
configured hosts are affected. Necessary but **not sufficient** on its own — it changes only name
|
||||
resolution, leaving SNI and the `Host:` header saying `ns.rec.net`. Kept as a safety net under (3).
|
||||
|
||||
This plugin powers [RecFlare](https://www.recflare.net) - an open source, cloud-native Rec Room server.
|
||||
3. **HTTP host rewrite** (`src/unity/http_rewrite.c`) — the patch that actually moves traffic. Hooks
|
||||
the concrete static `BestHTTP.HTTPManager.SendRequest(HTTPRequest)`, reads
|
||||
`req.Uri.AbsoluteUri`, swaps the host through the same `redirector.json` pairs, and assigns a
|
||||
fresh `new Uri(...)` back before letting the real `SendRequest` run. The new host therefore
|
||||
carries end-to-end — URL, SNI and `Host:` — so the target can serve it as its own vhost with its
|
||||
own cert. Native equivalent of the managed build's `SendRequestPatch`. This is the one
|
||||
**call-through** hook, so it depends on the relocating trampoline in `src/memory/detour.c`.
|
||||
|
||||
## Safety
|
||||
4. **TLS pinning bypass** (`src/unity/ssl_patch.c`). Redirecting HTTPS means the handshake presents a
|
||||
cert the client would reject. Resolves the **concrete**
|
||||
`Org.BouncyCastle.Crypto.Tls.LegacyTlsAuthentication.NotifyServerCertificate` and detours its
|
||||
compiled body to a no-op that accepts unconditionally — the native equivalent of the managed
|
||||
build's `DisableTLSPinning` Harmony patch.
|
||||
|
||||
Using BepInEx plugins may cause anti-virus scanners or Windows Defender to pick it up as a threat.
|
||||
5. **Memory-integrity scan neutralizer** (`src/unity/memcheck_patch.c`). The client runs a background
|
||||
scan that hashes `GameAssembly.dll` code against baked-in hashes; the inline hooks above change
|
||||
that memory, so boot dies with *"Launch validation failed."* The scanner's name is obfuscated and
|
||||
rotates every build, so it is found **by signature** instead: the class in `Assembly-CSharp` that
|
||||
holds both a `Thread` and a `CancellationTokenSource` field. Its public instance 0-param non-void
|
||||
method is the scan entry point; we detour it to return an already-resolved promise (fetched from
|
||||
the promise type's static `Resolved` getter), so boot's await satisfies instantly. Started first
|
||||
among the il2cpp patches — the boot step that awaits the scan can fire early, and the reflection
|
||||
sweep needs a head start.
|
||||
|
||||
If you don't trust the compiled .DLL, you can build it yourself.
|
||||
6. **EAC neutralizer** (`src/unity/eac_patch.c`). Two replace-only hooks on
|
||||
`RecRoom.AntiCheat.EACManager`: the readiness check (the sole static 0-param `bool`
|
||||
non-property-getter method — again resolved by signature, since the name rotates) is forced to
|
||||
`true`, because the real check needs EasyAntiCheat services that no longer exist; and
|
||||
`GenerateChallengeResponse(string)` (unobfuscated) returns `base64(challenge)`, with
|
||||
`base64("nothing")` for an empty/null challenge. Safe to patch only because (5) has already
|
||||
neutralized the hash check.
|
||||
|
||||
See https://github.com/djdevin/recnet-plugin#from-source
|
||||
Everything from (2) on runs off one background thread spawned in `DllMain`; each il2cpp patch gets
|
||||
its own thread, since they must wait on the runtime independently. `src/unity/module_watch.c` just
|
||||
logs `GameAssembly.dll` / `UnityPlayer.dll` as they appear and then stops.
|
||||
|
||||
## What it does
|
||||
The `connect` and `gethostbyname` hooks are present but **intentionally not installed**: the
|
||||
`connect` hook redirects *all* :443 traffic (would break Photon/CDN/telemetry), and `getaddrinfo`
|
||||
already covers the il2cpp DNS path.
|
||||
|
||||
| Patch | File | Effect |
|
||||
| --- | --- | --- |
|
||||
| Name-server redirect | `Patches/SendRequestPatch.cs` | Intercepts `BestHTTP` requests and rewrites the host `ns.rec.net` → your configured server. Also provides optional HTTP request/response logging for development. |
|
||||
| Photon override | `Patches/PhotonPatches.cs` | Replaces the Realtime / Voice / Chat App IDs (and optionally the Photon name server + port) with your own. |
|
||||
| EAC bypass | `Patches/EACPatches.cs` | Forces EasyAntiCheat "ready" and stubs the challenge-response so the client connects without the official anti-cheat. |
|
||||
| TLS bypass | `Patches/DisableTLSPinning.cs` | Skips server-certificate validation so a custom server's cert is accepted. |
|
||||
| Image signing bypass | `Patches/ImageSigningPatch.cs` | Forces the mscorlib RSA verify to succeed, so images your server serves load without being signed by Rec Room's key. **On by default.** |
|
||||
| CheatManager handling | `Plugin.cs` | Deactivates the in-game `CheatManager` (which would otherwise boot you from rooms) while keeping it resolvable for account creation / login. |
|
||||
| DUID mismatch workaround | `Patches/DUIDMismatchPatch.cs` | Forces the device-id mismatch check to "no mismatch" so the Create Account hang (below) is skipped. **On by default**; no-op on healthy machines. |
|
||||
| DUID diagnostics | `Patches/DUIDProbePatch.cs`, `Patches/CorruptDUIDPatch.cs`, `Patches/DeviceIdResponsePatch.cs` | Investigation tooling for the hang: PlayerPrefs/DUID call logging, deliberately corrupting or restoring the stored id, and rewriting the `deviceId` response in flight. All off by default — see [Configuration](#configuration). |
|
||||
### Resolving obfuscated targets
|
||||
|
||||
## The Create Account / DUID hang
|
||||
Rec Room obfuscates its own type/method names and they rotate every build, so nothing here
|
||||
hard-codes one. Framework names (`SendRequest`, `get_Uri`, `NotifyServerCertificate`,
|
||||
`GenerateChallengeResponse`, `EACManager`) are stable and resolved literally; the anti-cheat internals
|
||||
are resolved by **shape** — field types, method signature, return type — through the il2cpp
|
||||
reflection API at runtime. Every candidate is logged, and an ambiguous match logs a `WARNING` rather
|
||||
than silently guessing.
|
||||
|
||||
Some machines hang forever on **Create Account**. This turned out to be a genuinely nasty one, so it's
|
||||
worth documenting.
|
||||
## Build
|
||||
|
||||
**What happens:** when the client's *stored* device id (DUID) differs from the one derived at runtime,
|
||||
the client takes a "migration" path — it POSTs to `PlayerReporting/v1/deviceId`, the server answers
|
||||
`200 {"success":true}`, and then the client **stalls**: it never makes the `create_account` OAuth call
|
||||
and never persists the new id. Machines whose stored id already matches never take this path, which is
|
||||
why the bug hits some players and not others (and is hard to reproduce if your own machine is fine).
|
||||
Requires VS 2022 (C toolchain) + CMake + Ninja (both ship with VS). **Must build x64** — a 32-bit
|
||||
DLL silently fails to load. Import the amd64 VC environment first:
|
||||
|
||||
**The decision point** is `CheatManager.CheckForDUIDMismatch`. Forcing it to return *true* reproduces
|
||||
the hang on any machine; forcing it *false* skips the whole path. That false-forcing is the shipping
|
||||
workaround, exposed as the `Suppress DUID Mismatch` config option, which is **on by default**. It's a
|
||||
no-op on healthy machines (their real check already returns false) and skips the hang on affected ones.
|
||||
|
||||
**Still unsolved:** we have not found where the "old" device id in that POST actually comes from. It
|
||||
survives deleting the entire `HKCU\Software\Against Gravity\Rec Room` registry key, and on the failing
|
||||
run the local `cm_did_ppk` PlayerPref is never even read — so clearing local storage does **not** fix
|
||||
it. The leading theory is that it's held server-side (recorded by the server from earlier reports)
|
||||
and/or cached in memory from a server response, which would make the proper fix server-side. See
|
||||
`CLAUDE.md` for the full investigation and the diagnostic tooling.
|
||||
|
||||
> ⚠️ `Suppress DUID Mismatch` is a workaround: it lets account creation through but does **not** repair
|
||||
> a genuinely corrupt stored/served id — it just stops the client from acting on the mismatch.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A Rec Room install set up with **BepInEx 6 (IL2CPP, bleeding-edge)**, launched at least once so the IL2CPP interop assemblies have been generated under `BepInEx/interop/`.
|
||||
- **.NET 6 SDK** to build the plugin.
|
||||
- Your own server endpoints: a RecNet name server, and [Photon](https://www.photonengine.com) app keys.
|
||||
|
||||
_Looking for a custom RecNet server?_ Try https://github.com/djdevin/recflare
|
||||
|
||||
## Installing
|
||||
|
||||
1. Download the game using https://github.com/SteamRE/DepotDownloader. The manifest ID is `6426603215211043630` (the **20230414** build).
|
||||
Example: `depotdownloader -app 471710 -depot 471711 -manifest 6426603215211043630`
|
||||
**You must use this specific version.** Rec Room's type and method names are obfuscated and
|
||||
re-rolled every build, so the patches only bind against the build they were written for.
|
||||
2. Install BepInEx to the game. See https://docs.bepinex.dev/articles/user_guide/installation/index.html. **Note that you must use version 6!**
|
||||
3. Launch the game once so BepInEx generates its `config/` folder and the IL2CPP interop assemblies.
|
||||
|
||||
Alternatively, use the [RecFlare client](https://github.com/djdevin/recflare-client)
|
||||
|
||||
### From release
|
||||
|
||||
1. Download a release from [Releases](https://github.com/djdevin/recnet-plugin/releases)
|
||||
2. Drop the `.dll` file into `BepInEx/plugins/`
|
||||
|
||||
### From source
|
||||
|
||||
The project references the game's interop DLLs, so the build needs to know where your Rec Room install lives. Set `GamePath` using any one of:
|
||||
|
||||
1. **A local props file** (recommended):
|
||||
```sh
|
||||
cp GamePath.props.example GamePath.props
|
||||
```
|
||||
then edit `GamePath` in `GamePath.props` to point at your Rec Room install root. This file is local-only and stays out of the repo.
|
||||
|
||||
2. **An environment variable:**
|
||||
```sh
|
||||
set RECROOM_PATH=C:\Path\To\RecRoom # cmd
|
||||
$env:RECROOM_PATH = "C:\Path\To\RecRoom" # PowerShell
|
||||
```
|
||||
|
||||
3. **On the command line:**
|
||||
```sh
|
||||
dotnet build -p:GamePath="C:\Path\To\RecRoom"
|
||||
```
|
||||
|
||||
Then build:
|
||||
|
||||
```sh
|
||||
dotnet build
|
||||
```powershell
|
||||
& "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
The build validates that `GamePath` is set and that `$(GamePath)\BepInEx\interop` exists, and fails with a clear message otherwise.
|
||||
Output: `build\version.dll` — a single self-contained proxy (it loads the real system `version.dll`
|
||||
at runtime, so there is nothing else to ship).
|
||||
|
||||
A post-build step (the `DeployPlugin` target in the `.csproj`) automatically copies the built `RecNetPlugin.dll` into your Rec Room install's `BepInEx/plugins/` folder after every build. (The copy will fail if Rec Room is running, since the DLL is locked — close the game and rebuild.) Since `GamePath` already points at your install, you don't need to copy anything by hand — just `dotnet build` and launch the game.
|
||||
One-step deploy into the game folder (close Rec Room first — the DLL is locked while it runs):
|
||||
|
||||
If you need the DLL elsewhere, it's also left in `bin/Debug/net6.0/`.
|
||||
```powershell
|
||||
cmake -S . -B build -G Ninja -DGAME_DIR="C:\Games\recflare-client-unstable"
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
## Configuration
|
||||
## Install (manual)
|
||||
|
||||
Start the game for the first time. In `BepInEx` you should now see a `config` folder. If not, verify BepInEx installation and version.
|
||||
1. Copy `build\version.dll` into the Rec Room install root (next to `RecRoom.exe`). If BepInEx's
|
||||
`version.dll` is there, replace it (this build does not use BepInEx).
|
||||
2. Copy `redirector.json.example` to `redirector.json` there and set the `rewrite` pairs
|
||||
(`{ "from": "ns.rec.net", "to": "ns.recflare.net" }`). The same pairs drive both the DNS and the
|
||||
HTTP rewrite. Matching is exact — add one entry per host. Parsed by a flat key scan, not a real
|
||||
JSON parser, so keep it flat: one object per rewrite.
|
||||
3. Launch. A console window opens; logs also go to `redirector_<pid>.log` beside `RecRoom.exe`.
|
||||
|
||||
Inside `config`, edit the `net.rec.plugin.cfg` file and update as needed:
|
||||
A healthy run logs all of these (each patch runs on its own thread, so they interleave; `[MEMCHECK]`
|
||||
lands last — its reflection sweep takes a moment):
|
||||
|
||||
**[Server]**
|
||||
- `RecNet NameServer Host` — base URL of your RecNet name server (like `https://ns.rec.net`).
|
||||
```
|
||||
[STATUS] DNS REDIRECT ACTIVE
|
||||
[SSL] TLS pinning bypassed (NotifyServerCertificate -> accept-all)
|
||||
[EAC] readiness check forced true
|
||||
[EAC] GenerateChallengeResponse -> base64(challenge)
|
||||
[HTTP] host rewrite installed on SendRequest
|
||||
[MEMCHECK] native memory integrity scan skipped (scan-start -> resolved promise)
|
||||
[HTTP] https://ns.rec.net/ -> https://ns.recflare.net/ (one per request)
|
||||
```
|
||||
|
||||
**[Photon]**
|
||||
- `App Id Realtime` — Photon Realtime App ID.
|
||||
- `App Id Voice` — Photon Voice App ID.
|
||||
- `App Id Chat` — Photon Chat App ID.
|
||||
The per-request `[HTTP] ... -> ...` lines are the proof traffic is actually moving; everything above
|
||||
them only says the hooks installed. `[DETOUR] ... refusing hook` means the detour engine wouldn't
|
||||
touch that prologue (see below) and that patch is **not** active.
|
||||
|
||||
**[Signing]**
|
||||
- `Disable Signature Verification` — stops the client checking that images are signed with Rec Room's
|
||||
private key, so your own server can serve images. **On by default**; leave it alone.
|
||||
> ⚠️ This forces **all** mscorlib RSA verification to pass, not just image signatures. TLS is
|
||||
> unaffected (BestHTTP uses its own bundled BouncyCastle).
|
||||
## Known limitations / open items
|
||||
|
||||
**[Advanced]**
|
||||
- `Enabled Advanced Settings` — must be `true` to apply the custom Photon name server / port below.
|
||||
- `Photon NameServer` — custom Photon name server host.
|
||||
- `Photon NameServer Port` — custom port (`0` uses the default, `4533`).
|
||||
- `Debug` — verbose HTTP request/response logging (only needed for development)
|
||||
> ⚠️ Debug logs include **sensitive data** (passwords, auth tokens). Be careful when sharing them.
|
||||
- `Suppress DUID Mismatch` — skips the Create Account / DUID hang (see above). **On by default**; the
|
||||
only DUID option meant for normal use. Set `false` only to observe the real mismatch for debugging.
|
||||
|
||||
The remaining `[Advanced]` DUID options — `Simulate DUID Mismatch`, `Corrupt Stored DUID`,
|
||||
`Restore Stored DUID`, `DeviceId Response Override`, `DeviceId Response Status` — are **diagnostic
|
||||
tools** used to investigate the hang. Leave them at their defaults unless you're debugging it; see
|
||||
`CLAUDE.md` for what each one does.
|
||||
|
||||
## Project layout
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `Plugin.cs` | Plugin entry point, config bindings, Harmony bootstrap |
|
||||
| `Patches/` | Harmony patches (HTTP, EAC, TLS, Photon, image signing, DUID) |
|
||||
| `CLAUDE.md` | Developer notes: build gotchas, IL2CPP/interop caveats, and the full DUID-hang investigation |
|
||||
| `RecNetPlugin.csproj` | Build config + interop references (driven by `GamePath`), and the `DeployPlugin` post-build copy |
|
||||
| `GamePath.props.example` | Template for your local `GamePath.props` |
|
||||
|
||||
## FAQ
|
||||
|
||||
**Can I use this for my own Rec Room server?**
|
||||
|
||||
Yes. That's the point.
|
||||
|
||||
## Credits
|
||||
|
||||
Based on https://github.com/CannedNet/CannedNet.Client
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
- **Obfuscated targets are matched by shape, not name.** A Rec Room build that changes the *structure*
|
||||
of the scanner class or the EAC readiness method — not just its name — will break that patch. The
|
||||
logs list every candidate considered, and warn when more than one matched, so a drift shows up as a
|
||||
`WARNING` or a "not identified" line rather than a silent misfire. Watch for `[MEMCHECK] scanner
|
||||
candidate` lines: more than one means the field-signature match is no longer unique.
|
||||
- **The detour engine's length decoder is minimal.** It relocates rip-relative `disp32` and `rel32`
|
||||
branches into a trampoline allocated within ±2 GB, but bails on two-byte (`0F`) opcodes, `rel8`
|
||||
branches, and anything it doesn't model — and `InstallDetour` then **refuses the hook** rather than
|
||||
corrupt code. This only constrains call-through hooks (currently just `SendRequest`); replace-only
|
||||
hooks take a blind 14-byte overwrite, which is safe because they jump away and never execute the
|
||||
torn tail.
|
||||
- **Nothing is undone on unload.** The detours stay installed for the life of the process; the saved
|
||||
original bytes are kept but never restored.
|
||||
- **The anti-cheat may catch up.** The memory-integrity scan is neutralized at its managed entry
|
||||
point, not at the native scanner itself — a build that calls the scan from somewhere else, or adds a
|
||||
second check, would reject the client again.
|
||||
|
||||
@@ -1,948 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<AssemblyName>RecNetPlugin</AssemblyName>
|
||||
<Product>RecNetPlugin</Product>
|
||||
<Version>1.0.0</Version>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<RestoreAdditionalProjectSources>
|
||||
https://api.nuget.org/v3/index.json;
|
||||
https://nuget.bepinex.dev/v3/index.json;
|
||||
https://nuget.samboy.dev/v3/index.json
|
||||
</RestoreAdditionalProjectSources>
|
||||
<RootNamespace>RecNetPlugin</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- GamePath = root of a Rec Room install whose BepInEx/interop/ has been populated.
|
||||
It is intentionally NOT hardcoded here so the repo is distributable. Set it via ONE of:
|
||||
1. a local GamePath.props file (copy GamePath.props.example -> GamePath.props; gitignored)
|
||||
2. a RECROOM_PATH environment variable
|
||||
3. the command line: dotnet build -p:GamePath="D:\Path\To\RecRoom" -->
|
||||
<Import Project="$(MSBuildThisFileDirectory)GamePath.props" Condition="Exists('$(MSBuildThisFileDirectory)GamePath.props')" />
|
||||
|
||||
<PropertyGroup>
|
||||
<GamePath Condition="'$(GamePath)' == '' and '$(RECROOM_PATH)' != ''">$(RECROOM_PATH)</GamePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="ValidateGamePath" BeforeTargets="ResolveAssemblyReferences;Build">
|
||||
<Error Condition="'$(GamePath)' == ''"
|
||||
Text="GamePath is not set. Copy GamePath.props.example to GamePath.props and set your Rec Room install path (or set the RECROOM_PATH env var, or pass -p:GamePath=...). See CLAUDE.md." />
|
||||
<Error Condition="'$(GamePath)' != '' and !Exists('$(GamePath)\BepInEx\interop')"
|
||||
Text="GamePath '$(GamePath)' has no BepInEx\interop folder. Point it at a Rec Room install that has been launched once under BepInEx so the IL2CPP interop assemblies are generated." />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.*" IncludeAssets="compile"/>
|
||||
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.*"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Assembly-CSharp">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Assembly-CSharp-firstpass">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Assembly-CSharp-firstpass.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="AstarPathfindingProject">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\AstarPathfindingProject.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Backtrace.Unity">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Backtrace.Unity.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.All.Injection.Debugging">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.All.Injection.Debugging.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.All.Injection.PhotonNetSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.All.Injection.PhotonNetSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.All.Injection.UnityEngine">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.All.Injection.UnityEngine.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.All.Mock">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.All.Mock.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.All.RecRoom">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.All.RecRoom.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Dynamic.Core.NetSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Dynamic.Core.NetSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Dynamic.Mock">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Dynamic.Mock.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Shared.Core.ByteCode">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Shared.Core.ByteCode.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Shared.CV2.Dependencies">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Shared.CV2.Dependencies.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Shared.RecRoom.Engine">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Shared.RecRoom.Engine.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Shared.RecRoom.Objects">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Shared.RecRoom.Objects.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Shared.Utilities">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Shared.Utilities.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.Core.CompileSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.Core.CompileSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.Core.GraphSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.Core.GraphSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.Core.NetSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.Core.NetSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.Core.RequestReduce">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.Core.RequestReduce.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.Core.TreeSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.Core.TreeSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.Core.TypeCheckSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.Core.TypeCheckSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.Core.TypeSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.Core.TypeSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.Core.UnificationSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.Core.UnificationSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.EV">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.EV.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.RecRoom">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.RecRoom.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.Static.Utilities">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.Static.Utilities.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Circuits.V2">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Circuits.V2.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Codestage.Anticheattoolkit.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Codestage.Anticheattoolkit.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="CSCore">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\CSCore.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="EasyAntiCheat.Client">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\EasyAntiCheat.Client.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Google.Protobuf">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Google.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ICSharpCode.SharpZipLib">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\ICSharpCode.SharpZipLib.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppMicrosoft.Bcl.HashCode">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppMicrosoft.Bcl.HashCode.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppMicrosoft.CognitiveServices.Speech.csharp">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppMicrosoft.CognitiveServices.Speech.csharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppMicrosoft.Toolkit.HighPerformance">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppMicrosoft.Toolkit.HighPerformance.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppMono.Security">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppMono.Security.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2Cppmscorlib">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2Cppmscorlib.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Buffers">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Buffers.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Configuration">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Configuration.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Core">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Data">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Data.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Drawing">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Drawing.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Memory">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Memory.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Numerics">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Numerics.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Numerics.Vectors">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Numerics.Vectors.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Runtime.CompilerServices.Unsafe">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Runtime.CompilerServices.Unsafe.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Runtime.Serialization">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Runtime.Serialization.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Xml">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Xml.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppSystem.Xml.Linq">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Il2CppSystem.Xml.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Kyub.EmojiSearch">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Kyub.EmojiSearch.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Logger.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Logger.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NewPlayerChallenges.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\NewPlayerChallenges.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Nito.Collections.Deque">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Nito.Collections.Deque.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Oculus.Platform">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Oculus.Platform.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Oculus.VR">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Oculus.VR.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="OSA">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\OSA.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Pathfinding.ClipperLib">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Pathfinding.ClipperLib.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Pathfinding.Ionic.Zip.Reduced">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Pathfinding.Ionic.Zip.Reduced.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Pathfinding.Poly2Tri">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Pathfinding.Poly2Tri.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Photon3Unity3D">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Photon3Unity3D.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PhotonChat">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\PhotonChat.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PhotonRealtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\PhotonRealtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PhotonUnityNetworking">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\PhotonUnityNetworking.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PhotonUnityNetworking.Utilities">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\PhotonUnityNetworking.Utilities.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PhotonVoice">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\PhotonVoice.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PhotonVoice.API">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\PhotonVoice.API.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PhotonVoice.PUN">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\PhotonVoice.PUN.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Pngcs">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Pngcs.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecNet.Interfaces">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecNet.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecNet.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecNet.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Agdxgidisplays.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Agdxgidisplays.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.AgInitialization.Interfaces">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.AgInitialization.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.AgInitialization.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.AgInitialization.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Agmobilear.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Agmobilear.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Analytics.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Analytics.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ApplicationLifecycle.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ApplicationLifecycle.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Assetbundles.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Assetbundles.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Async">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Async.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Attributes.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Attributes.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Audio.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Audio.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.AutomationTests.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.AutomationTests.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.BitPacker.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.BitPacker.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Build.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Build.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Challenges.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Challenges.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Chat.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Chat.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.CircuitsV1.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.CircuitsV1.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ClusterLods.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ClusterLods.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.CodeGen.Attributes">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.CodeGen.Attributes.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Commandline.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Commandline.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.CommonDataTypes.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.CommonDataTypes.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Configloader.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Configloader.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Connectables.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Connectables.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Content.Authoring.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Content.Authoring.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Creation.Interfaces.UX.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Creation.Interfaces.UX.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Creation.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Creation.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.CultureUtil.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.CultureUtil.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Datastructures.CollisionMesh.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Datastructures.CollisionMesh.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Datastructures.CullingGroupManager.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Datastructures.CullingGroupManager.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Datastructures.OverridableFields.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Datastructures.OverridableFields.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Datastructures.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Datastructures.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Datastructures.Singletons.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Datastructures.Singletons.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Debugging.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Debugging.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.EditorHelpers.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.EditorHelpers.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Encoding.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Encoding.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Experiments.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Experiments.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.FastLines.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.FastLines.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.FuzzySearch.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.FuzzySearch.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.GameSystems.Interfaces">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.GameSystems.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Imageutils.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Imageutils.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Imposters.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Imposters.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Instantiation.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Instantiation.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.iOSNative.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.iOSNative.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.JuniorAccountVisibility.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.JuniorAccountVisibility.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Keepsakes.Interfaces">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Keepsakes.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Keepsakes.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Keepsakes.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Keepsakes.UnityExtensions">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Keepsakes.UnityExtensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Localization.Service">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Localization.Service.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Maker.Core.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Maker.Core.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Maker.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Maker.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.MemoryStats.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.MemoryStats.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Minijson.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Minijson.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.MobileHome.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.MobileHome.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Nativemesh.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Nativemesh.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Networking.DataTypes.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Networking.DataTypes.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Networking.NetworkedObjects.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Networking.NetworkedObjects.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Networking.PhotonImpl.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Networking.PhotonImpl.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Networking.RoomLoading.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Networking.RoomLoading.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Networking.RPC.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Networking.RPC.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Networking.SynchronizedFields.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Networking.SynchronizedFields.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.NoEngine.Algorithms.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.NoEngine.Algorithms.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.NoEngine.Common.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.NoEngine.Common.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.NoEngine.DataStructures.Performance.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.NoEngine.DataStructures.Performance.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.NoEngine.DataStructures.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.NoEngine.DataStructures.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.NoEngine.Debugging.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.NoEngine.Debugging.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.NoEngine.JetBrains.Annotations">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.NoEngine.JetBrains.Annotations.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Attributes.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Attributes.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.BitPacker.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.BitPacker.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.ComponentData.Generated.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.ComponentData.Generated.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.ComponentData.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.ComponentData.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.ConfigUI.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.ConfigUI.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Entities.Core.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Entities.Core.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Entities.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Entities.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Interfaces.ConfigUI.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Interfaces.ConfigUI.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Interfaces.Prefabs.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Interfaces.Prefabs.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Interfaces.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Interfaces.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Prefabs.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Prefabs.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Properties.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Properties.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Protobufs.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Protobufs.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.RendererV1.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.RendererV1.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Services.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Services.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Systems.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Systems.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Telemetry.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Telemetry.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Transmission.PUN.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Transmission.PUN.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectModel.Transmission.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectModel.Transmission.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ObjectPool.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ObjectPool.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Persistence.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Persistence.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.PlatformNotifications.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.PlatformNotifications.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Preferences.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Preferences.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.PrefParsers.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.PrefParsers.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ProgressionEvents.Interfaces">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ProgressionEvents.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ProgressionEvents.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ProgressionEvents.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Promises.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Promises.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Protobuf.Debugging.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Protobuf.Debugging.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Protobuf.Extensions.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Protobuf.Extensions.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Protobuf.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Protobuf.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Protobuf.UnityExtensions.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Protobuf.UnityExtensions.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Rbex.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Rbex.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ResourceManagement.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ResourceManagement.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.RoomLoading.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.RoomLoading.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.RoomPermissions.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.RoomPermissions.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Rranticheat.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Rranticheat.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.RRUI.Core.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.RRUI.Core.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.RRUI.Data.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.RRUI.Data.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.RRUI.Navigation.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.RRUI.Navigation.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.RRUI.Theme.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.RRUI.Theme.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Scheduling.Interface.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Scheduling.Interface.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Scheduling.Scheduler.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Scheduling.Scheduler.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Scheduling.Schedules.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Scheduling.Schedules.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.ShapeRendering.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.ShapeRendering.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Streamingaudio.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Streamingaudio.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Studio.Common.LocalTesting">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Studio.Common.LocalTesting.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Studio.Common.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Studio.Common.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.TagsAndLayers.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.TagsAndLayers.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Time.Interfaces">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Time.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Time.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Time.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Tweening.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Tweening.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.UIInteraction.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.UIInteraction.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Unityextensions.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Unityextensions.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.UrlHandler.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.UrlHandler.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Utf8json.Interfaces">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Utf8json.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Utf8json.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Utf8json.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="RecRoom.Versioning.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\RecRoom.Versioning.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SA.Foundation">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\SA.Foundation.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SA.Foundation.Network">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\SA.Foundation.Network.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SA.iOS">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\SA.iOS.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SA.iOS.XCode">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\SA.iOS.XCode.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Singular">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Singular.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="StansAssets.Foundation">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\StansAssets.Foundation.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="StansAssets.Plugins">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\StansAssets.Plugins.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="StatsigUnity.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\StatsigUnity.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SteamVR">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\SteamVR.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SteamVR_Actions">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\SteamVR_Actions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="TextureTool.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\TextureTool.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ToxMod">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\ToxMod.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UJect.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UJect.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UJect.UnityExtensions">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UJect.UnityExtensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Addressables">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Addressables.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Burst">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Burst.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Burst.Unsafe">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Burst.Unsafe.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Collections">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Collections.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Entities">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Entities.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.InputSystem">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.InputSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Jobs">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Jobs.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Localization">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Localization.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Mathematics">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Mathematics.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.ProBuilder">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.ProBuilder.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Properties">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Properties.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.RenderPipeline.Universal.ShaderLibrary">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.RenderPipeline.Universal.ShaderLibrary.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.RenderPipelines.Core.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.RenderPipelines.Core.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.RenderPipelines.Universal.Runtime">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.RenderPipelines.Universal.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.ResourceManager">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.ResourceManager.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Serialization">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.Serialization.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.TextMeshPro">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.TextMeshPro.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.XR.ARFoundation">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.XR.ARFoundation.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.XR.ARSubsystems">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.XR.ARSubsystems.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.XR.Management">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.XR.Management.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.XR.Oculus">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.XR.Oculus.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.XR.OpenVR">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.XR.OpenVR.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.XR.PSVR">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Unity.XR.PSVR.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AccessibilityModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.AccessibilityModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AIModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.AIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AndroidJNIModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.AndroidJNIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AnimationModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.AnimationModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AssetBundleModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.AssetBundleModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AudioModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.AudioModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ClothModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.ClothModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ClusterInputModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.ClusterInputModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ClusterRendererModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.ClusterRendererModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CoreModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.CoreModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CrashReportingModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.CrashReportingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.DirectorModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.DirectorModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.DSPGraphModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.DSPGraphModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.GameCenterModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.GameCenterModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.GIModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.GIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.GridModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.GridModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.HotReloadModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.HotReloadModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ImageConversionModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.ImageConversionModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.IMGUIModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.IMGUIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.InputLegacyModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.InputLegacyModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.InputModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.InputModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.JSONSerializeModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.JSONSerializeModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.LocalizationModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.LocalizationModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ParticleSystemModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.ParticleSystemModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.PerformanceReportingModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.PerformanceReportingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.Physics2DModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.Physics2DModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.PhysicsModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.PhysicsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ProfilerModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.ProfilerModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ScreenCaptureModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.ScreenCaptureModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SharedInternalsModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.SharedInternalsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SpatialTracking">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.SpatialTracking.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SpriteMaskModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.SpriteMaskModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SpriteShapeModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.SpriteShapeModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.StreamingModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.StreamingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SubstanceModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.SubstanceModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SubsystemsModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.SubsystemsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TerrainModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.TerrainModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TerrainPhysicsModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.TerrainPhysicsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TextCoreModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.TextCoreModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TextRenderingModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.TextRenderingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TilemapModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.TilemapModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TLSModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.TLSModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UI">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UI.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UIElementsModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UIElementsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UIElementsNativeModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UIElementsNativeModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UIModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UmbraModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UmbraModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UNETModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UNETModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityAnalyticsModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UnityAnalyticsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityConnectModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UnityConnectModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityCurlModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UnityCurlModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityTestProtocolModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UnityTestProtocolModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestAssetBundleModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UnityWebRequestAssetBundleModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestAudioModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UnityWebRequestAudioModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UnityWebRequestModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestTextureModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UnityWebRequestTextureModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestWWWModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.UnityWebRequestWWWModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VehiclesModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.VehiclesModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VFXModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.VFXModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VideoModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.VideoModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VirtualTexturingModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.VirtualTexturingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VRModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.VRModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.WindModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.WindModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.XRModule">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\UnityEngine.XRModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Valve.Newtonsoft.Json">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\Valve.Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="XboxUWP">
|
||||
<HintPath>$(GamePath)\BepInEx\interop\XboxUWP.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="DeployPlugin" AfterTargets="Build">
|
||||
<Copy SourceFiles="$(OutputPath)$(AssemblyName).dll" DestinationFolder="$(GamePath)\BepInEx\plugins\" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
void LogAPIRequest(
|
||||
const char *method,
|
||||
const char *url
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
|
||||
#include <windows.h>
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <dbghelp.h>
|
||||
#include <psapi.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#pragma comment(lib,"ws2_32.lib")
|
||||
#pragma comment(lib,"dbghelp.lib")
|
||||
#pragma comment(lib,"psapi.lib")
|
||||
|
||||
#ifndef ARRAYSIZE
|
||||
#define ARRAYSIZE(x) (sizeof(x)/sizeof((x)[0]))
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#define CONFIG_FILE "redirector.json"
|
||||
#define MAX_REDIRECTS 64
|
||||
#define MAX_REWRITES 32
|
||||
#define DEFAULT_IP "127.0.0.1"
|
||||
#define DEFAULT_PORT 443
|
||||
|
||||
// Static-IP redirect (used only by the disabled connect hook; DNS uses host rewrite below).
|
||||
extern char redirect_ip[16];
|
||||
extern int redirect_port;
|
||||
|
||||
extern char *redirect_domains[MAX_REDIRECTS];
|
||||
extern int redirect_count_config;
|
||||
|
||||
// Host rewrite pairs: a DNS lookup for exactly <from> is resolved as <to> instead, so real DNS
|
||||
// returns the target's current (possibly dynamic) IP.
|
||||
extern char *rewrite_from[MAX_REWRITES];
|
||||
extern char *rewrite_to[MAX_REWRITES];
|
||||
extern int rewrite_count;
|
||||
|
||||
void LoadConfig(void);
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
typedef int (WSAAPI *connect_t)(
|
||||
SOCKET,
|
||||
const struct sockaddr *,
|
||||
int
|
||||
);
|
||||
|
||||
extern connect_t real_connect;
|
||||
extern connect_t original_connect;
|
||||
|
||||
extern BYTE backup_connect[14];
|
||||
|
||||
int WSAAPI hook_connect(
|
||||
SOCKET,
|
||||
const struct sockaddr *,
|
||||
int
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
int InstallDetour(
|
||||
LPVOID target,
|
||||
LPVOID hook,
|
||||
BYTE *backup,
|
||||
LPVOID *outTrampoline
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
typedef int (WSAAPI *getaddrinfo_t)(
|
||||
PCSTR,
|
||||
PCSTR,
|
||||
const ADDRINFOA *,
|
||||
PADDRINFOA *
|
||||
);
|
||||
|
||||
typedef struct hostent *(WSAAPI *gethostbyname_t)(
|
||||
const char *
|
||||
);
|
||||
|
||||
extern getaddrinfo_t real_getaddrinfo;
|
||||
extern getaddrinfo_t original_getaddrinfo;
|
||||
|
||||
extern gethostbyname_t real_gethostbyname;
|
||||
|
||||
extern BYTE backup_getaddrinfo[32]; // holds whole stolen instructions (>= 14 bytes)
|
||||
|
||||
int WSAAPI hook_getaddrinfo(
|
||||
PCSTR,
|
||||
PCSTR,
|
||||
const ADDRINFOA *,
|
||||
PADDRINFOA *
|
||||
);
|
||||
|
||||
struct hostent *WSAAPI hook_gethostbyname(
|
||||
const char *
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// Neutralizes EasyAntiCheat integration so the client runs against the self-hosted server. Ports the
|
||||
// managed EACPatches: (1) force EACManager's readiness check true, (2) make GenerateChallengeResponse
|
||||
// return base64(challenge). Resolves EACManager by its (unobfuscated) name and the readiness method by
|
||||
// signature (its obfuscated name rotates per build). Waits for the il2cpp runtime; safe on its own thread.
|
||||
void PatchEAC(void);
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
DWORD WINAPI HookThread(LPVOID);
|
||||
|
||||
BOOL InstallHooks(void);
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// Installs the HTTP-layer host rewrite: hooks BestHTTP.HTTPManager.SendRequest(HTTPRequest) and
|
||||
// rewrites the request's Uri (ns.rec.net -> ns.recflare.net) so the URL, TLS SNI and Host header all
|
||||
// carry the target host -- a genuine request to the alternate backend, not just a redirected IP.
|
||||
// Waits for the il2cpp runtime; safe to call on its own thread. No-op if no rewrite pairs configured.
|
||||
void PatchHttpHostRewrite(void);
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
void InitConsole(void);
|
||||
void InitLogger(void);
|
||||
|
||||
void Log(const char *fmt, ...);
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// Neutralizes the client's native memory-integrity scan, which otherwise fails boot with
|
||||
// "Launch validation failed. Is Rec Room installed correctly?" because it hashes GameAssembly.dll's
|
||||
// executable memory and our inline hooks (SendRequest, NotifyServerCertificate) change it.
|
||||
//
|
||||
// Mirrors the managed MemoryIntegrityPatch: find the scanner by signature (a class with both a
|
||||
// Thread and a CancellationTokenSource field), detour its public 0-param scan-start method to return
|
||||
// an already-resolved promise, so the boot step never sees a mismatch. All resolution is by
|
||||
// signature at runtime -- no obfuscated names, survives per-build name rotation. Waits for the
|
||||
// il2cpp runtime; safe on its own thread.
|
||||
void PatchMemoryIntegrityCheck(void);
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
void WatchModules(void);
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
void LogPacket(
|
||||
const char *direction,
|
||||
const void *data,
|
||||
size_t size
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
BOOL IsGameProcess(void);
|
||||
|
||||
void LogProcessInfo(void);
|
||||
|
||||
void DumpLoadedModules(void);
|
||||
|
||||
void LogStack(void);
|
||||
|
||||
LONG WINAPI MyExceptionHandler(EXCEPTION_POINTERS *e);
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
void PatchBestHTTPSSL(void);
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
int ShouldRedirect(const char *host);
|
||||
|
||||
// If host exactly matches a configured rewrite pair's <from>, writes <to> into out (up to outlen)
|
||||
// and returns 1; otherwise returns 0 and leaves out untouched.
|
||||
// Example: host "ns.rec.net", from "ns.rec.net", to "ns.recflare.net" -> "ns.recflare.net".
|
||||
int RewriteHost(const char *host, char *out, size_t outlen);
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
void LogTLS(
|
||||
const char *event
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"_comment": "Copy to redirector.json next to RecRoom.exe. Parsed by src/core/config.c (simple key scan, not a real JSON parser -- keep it flat, one object per rewrite).",
|
||||
|
||||
"_rewrite_comment": "DNS lookups for an exact 'from' host are resolved as 'to' instead, so real DNS returns the target's current IP (survives dynamic IPs). Matching is EXACT -- list each host you want redirected.",
|
||||
"rewrite": [
|
||||
{ "from": "ns.rec.net", "to": "ns.recflare.net" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "logger.h"
|
||||
|
||||
|
||||
char redirect_ip[16] = DEFAULT_IP;
|
||||
|
||||
int redirect_port = DEFAULT_PORT;
|
||||
|
||||
|
||||
char *redirect_domains[MAX_REDIRECTS];
|
||||
|
||||
int redirect_count_config = 0;
|
||||
|
||||
|
||||
char *rewrite_from[MAX_REWRITES];
|
||||
char *rewrite_to[MAX_REWRITES];
|
||||
|
||||
int rewrite_count = 0;
|
||||
|
||||
|
||||
|
||||
void LoadConfig()
|
||||
{
|
||||
HANDLE hFile =
|
||||
CreateFileA(
|
||||
CONFIG_FILE,
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ,
|
||||
NULL,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
NULL
|
||||
);
|
||||
|
||||
|
||||
if(hFile == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
Log(
|
||||
"[CONFIG] No config found, using defaults %s:%d",
|
||||
redirect_ip,
|
||||
redirect_port
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
DWORD size =
|
||||
GetFileSize(
|
||||
hFile,
|
||||
NULL
|
||||
);
|
||||
|
||||
|
||||
if(size == INVALID_FILE_SIZE)
|
||||
{
|
||||
CloseHandle(hFile);
|
||||
|
||||
Log(
|
||||
"[CONFIG] Failed reading file size"
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
char *buffer =
|
||||
calloc(
|
||||
1,
|
||||
size + 1
|
||||
);
|
||||
|
||||
|
||||
if(!buffer)
|
||||
{
|
||||
CloseHandle(hFile);
|
||||
|
||||
Log(
|
||||
"[CONFIG] Memory allocation failed"
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
DWORD read = 0;
|
||||
|
||||
|
||||
ReadFile(
|
||||
hFile,
|
||||
buffer,
|
||||
size,
|
||||
&read,
|
||||
NULL
|
||||
);
|
||||
|
||||
|
||||
CloseHandle(hFile);
|
||||
|
||||
|
||||
|
||||
//
|
||||
// IP
|
||||
//
|
||||
|
||||
char *ip =
|
||||
strstr(
|
||||
buffer,
|
||||
"\"ip\""
|
||||
);
|
||||
|
||||
|
||||
if(ip)
|
||||
{
|
||||
char *colon =
|
||||
strchr(
|
||||
ip,
|
||||
':'
|
||||
);
|
||||
|
||||
|
||||
if(colon)
|
||||
{
|
||||
char *start =
|
||||
strchr(
|
||||
colon,
|
||||
'"'
|
||||
);
|
||||
|
||||
|
||||
if(start)
|
||||
{
|
||||
start++;
|
||||
|
||||
|
||||
char *end =
|
||||
strchr(
|
||||
start,
|
||||
'"'
|
||||
);
|
||||
|
||||
|
||||
if(end)
|
||||
{
|
||||
size_t len =
|
||||
end - start;
|
||||
|
||||
|
||||
if(len < sizeof(redirect_ip))
|
||||
{
|
||||
memcpy(
|
||||
redirect_ip,
|
||||
start,
|
||||
len
|
||||
);
|
||||
|
||||
|
||||
redirect_ip[len] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Port
|
||||
//
|
||||
|
||||
char *port =
|
||||
strstr(
|
||||
buffer,
|
||||
"\"port\""
|
||||
);
|
||||
|
||||
|
||||
if(port)
|
||||
{
|
||||
char *colon =
|
||||
strchr(
|
||||
port,
|
||||
':'
|
||||
);
|
||||
|
||||
|
||||
if(colon)
|
||||
{
|
||||
int p =
|
||||
atoi(
|
||||
colon + 1
|
||||
);
|
||||
|
||||
|
||||
if(
|
||||
p > 0 &&
|
||||
p < 65536
|
||||
)
|
||||
{
|
||||
redirect_port = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Redirect domains
|
||||
//
|
||||
|
||||
char *redirect =
|
||||
strstr(
|
||||
buffer,
|
||||
"\"redirect\""
|
||||
);
|
||||
|
||||
|
||||
if(redirect)
|
||||
{
|
||||
char *array =
|
||||
strchr(
|
||||
redirect,
|
||||
'['
|
||||
);
|
||||
|
||||
|
||||
if(array)
|
||||
{
|
||||
char *current =
|
||||
array;
|
||||
|
||||
|
||||
while(
|
||||
redirect_count_config < MAX_REDIRECTS
|
||||
)
|
||||
{
|
||||
char *q1 =
|
||||
strchr(
|
||||
current,
|
||||
'"'
|
||||
);
|
||||
|
||||
|
||||
if(!q1)
|
||||
break;
|
||||
|
||||
|
||||
q1++;
|
||||
|
||||
|
||||
char *q2 =
|
||||
strchr(
|
||||
q1,
|
||||
'"'
|
||||
);
|
||||
|
||||
|
||||
if(!q2)
|
||||
break;
|
||||
|
||||
|
||||
|
||||
size_t len =
|
||||
q2 - q1;
|
||||
|
||||
|
||||
|
||||
redirect_domains[
|
||||
redirect_count_config
|
||||
] =
|
||||
calloc(
|
||||
1,
|
||||
len + 1
|
||||
);
|
||||
|
||||
|
||||
if(
|
||||
redirect_domains[
|
||||
redirect_count_config
|
||||
]
|
||||
)
|
||||
{
|
||||
memcpy(
|
||||
redirect_domains[
|
||||
redirect_count_config
|
||||
],
|
||||
q1,
|
||||
len
|
||||
);
|
||||
|
||||
|
||||
redirect_count_config++;
|
||||
}
|
||||
|
||||
|
||||
current =
|
||||
q2 + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Host rewrite pairs: "rewrite": [ { "from": "rec.net", "to": "recflare.net" }, ... ]
|
||||
// Scanned sequentially -- within each object "from" precedes "to".
|
||||
//
|
||||
|
||||
char *rw =
|
||||
strstr(
|
||||
buffer,
|
||||
"\"rewrite\""
|
||||
);
|
||||
|
||||
|
||||
if(rw)
|
||||
{
|
||||
char *current = rw;
|
||||
|
||||
|
||||
while(rewrite_count < MAX_REWRITES)
|
||||
{
|
||||
//
|
||||
// "from" value
|
||||
//
|
||||
|
||||
char *fk =
|
||||
strstr(current, "\"from\"");
|
||||
|
||||
if(!fk)
|
||||
break;
|
||||
|
||||
|
||||
char *fv1 = strchr(fk + 6, '"');
|
||||
if(!fv1) break;
|
||||
fv1++;
|
||||
|
||||
char *fv2 = strchr(fv1, '"');
|
||||
if(!fv2) break;
|
||||
|
||||
|
||||
//
|
||||
// "to" value (must follow this object's "from")
|
||||
//
|
||||
|
||||
char *tk =
|
||||
strstr(fv2, "\"to\"");
|
||||
|
||||
if(!tk)
|
||||
break;
|
||||
|
||||
|
||||
char *tv1 = strchr(tk + 4, '"');
|
||||
if(!tv1) break;
|
||||
tv1++;
|
||||
|
||||
char *tv2 = strchr(tv1, '"');
|
||||
if(!tv2) break;
|
||||
|
||||
|
||||
size_t flen = fv2 - fv1;
|
||||
size_t tlen = tv2 - tv1;
|
||||
|
||||
|
||||
char *fbuf = calloc(1, flen + 1);
|
||||
char *tbuf = calloc(1, tlen + 1);
|
||||
|
||||
|
||||
if(fbuf && tbuf)
|
||||
{
|
||||
memcpy(fbuf, fv1, flen);
|
||||
memcpy(tbuf, tv1, tlen);
|
||||
|
||||
rewrite_from[rewrite_count] = fbuf;
|
||||
rewrite_to[rewrite_count] = tbuf;
|
||||
|
||||
rewrite_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
free(fbuf);
|
||||
free(tbuf);
|
||||
}
|
||||
|
||||
|
||||
current = tv2 + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
free(buffer);
|
||||
|
||||
|
||||
|
||||
Log(
|
||||
"[CONFIG] Loaded %d redirects",
|
||||
redirect_count_config
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[CONFIG] Loaded %d host rewrites",
|
||||
rewrite_count
|
||||
);
|
||||
|
||||
|
||||
for(
|
||||
int i = 0;
|
||||
i < rewrite_count;
|
||||
i++
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[CONFIG] rewrite %s -> %s",
|
||||
rewrite_from[i],
|
||||
rewrite_to[i]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Log(
|
||||
"[CONFIG] Redirect IP %s:%d",
|
||||
redirect_ip,
|
||||
redirect_port
|
||||
);
|
||||
|
||||
|
||||
|
||||
for(
|
||||
int i = 0;
|
||||
i < redirect_count_config;
|
||||
i++
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[CONFIG] %s",
|
||||
redirect_domains[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
|
||||
static FILE *logFile = NULL;
|
||||
|
||||
|
||||
void InitConsole()
|
||||
{
|
||||
AllocConsole();
|
||||
|
||||
FILE *fp;
|
||||
|
||||
freopen_s(
|
||||
&fp,
|
||||
"CONOUT$",
|
||||
"w",
|
||||
stdout
|
||||
);
|
||||
|
||||
|
||||
printf("\n");
|
||||
printf("==============================\n");
|
||||
printf(" RR Redirector Loaded\n");
|
||||
printf("==============================\n\n");
|
||||
}
|
||||
|
||||
|
||||
void InitLogger()
|
||||
{
|
||||
char path[MAX_PATH];
|
||||
|
||||
GetModuleFileNameA(
|
||||
NULL,
|
||||
path,
|
||||
sizeof(path)
|
||||
);
|
||||
|
||||
|
||||
char *slash = strrchr(
|
||||
path,
|
||||
'\\'
|
||||
);
|
||||
|
||||
|
||||
if(slash)
|
||||
*(slash + 1) = 0;
|
||||
|
||||
|
||||
//
|
||||
// Per-PID filename. version.dll is loaded into several processes (the game, the EAC
|
||||
// launcher/bootstrap, the crash handler); a shared redirector.log means they truncate each
|
||||
// other's output with "w". One file per process keeps the game's diagnostics intact.
|
||||
//
|
||||
char name[64];
|
||||
|
||||
sprintf_s(
|
||||
name,
|
||||
sizeof(name),
|
||||
"redirector_%lu.log",
|
||||
GetCurrentProcessId()
|
||||
);
|
||||
|
||||
|
||||
strcat_s(
|
||||
path,
|
||||
sizeof(path),
|
||||
name
|
||||
);
|
||||
|
||||
|
||||
logFile = fopen(
|
||||
path,
|
||||
"w"
|
||||
);
|
||||
|
||||
|
||||
if(logFile)
|
||||
{
|
||||
fprintf(
|
||||
logFile,
|
||||
"==============================\n"
|
||||
);
|
||||
|
||||
fprintf(
|
||||
logFile,
|
||||
" RR Redirector Loaded\n"
|
||||
);
|
||||
|
||||
fprintf(
|
||||
logFile,
|
||||
"==============================\n\n"
|
||||
);
|
||||
|
||||
|
||||
fflush(logFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Log(
|
||||
const char *fmt,
|
||||
...
|
||||
)
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
|
||||
GetLocalTime(
|
||||
&st
|
||||
);
|
||||
|
||||
|
||||
DWORD tid =
|
||||
GetCurrentThreadId();
|
||||
|
||||
|
||||
char buffer[4096];
|
||||
|
||||
|
||||
va_list args;
|
||||
|
||||
va_start(
|
||||
args,
|
||||
fmt
|
||||
);
|
||||
|
||||
|
||||
vsprintf_s(
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
fmt,
|
||||
args
|
||||
);
|
||||
|
||||
|
||||
va_end(args);
|
||||
|
||||
|
||||
|
||||
printf(
|
||||
"[%02d:%02d:%02d.%03d][TID %lu] %s\n",
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
st.wSecond,
|
||||
st.wMilliseconds,
|
||||
tid,
|
||||
buffer
|
||||
);
|
||||
|
||||
|
||||
if(logFile)
|
||||
{
|
||||
fprintf(
|
||||
logFile,
|
||||
"[%02d:%02d:%02d.%03d][TID %lu] %s\n",
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
st.wSecond,
|
||||
st.wMilliseconds,
|
||||
tid,
|
||||
buffer
|
||||
);
|
||||
|
||||
|
||||
fflush(logFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "process.h"
|
||||
#include "logger.h"
|
||||
|
||||
|
||||
|
||||
//
|
||||
// version.dll gets loaded by every process launched from the game folder -- RecRoom.exe itself and
|
||||
// UnityCrashHandler64.exe, which RecRoom spawns from the same directory. Only the game is worth
|
||||
// hooking (and worth a console window: two AllocConsole calls = two debug windows on every launch).
|
||||
//
|
||||
BOOL IsGameProcess()
|
||||
{
|
||||
char path[MAX_PATH];
|
||||
|
||||
if(
|
||||
!GetModuleFileNameA(
|
||||
NULL,
|
||||
path,
|
||||
sizeof(path)
|
||||
)
|
||||
)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
char *slash = strrchr(
|
||||
path,
|
||||
'\\'
|
||||
);
|
||||
|
||||
|
||||
const char *exe =
|
||||
slash ? slash + 1 : path;
|
||||
|
||||
|
||||
return _stricmp(
|
||||
exe,
|
||||
"RecRoom.exe"
|
||||
) == 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void LogProcessInfo()
|
||||
{
|
||||
char path[MAX_PATH];
|
||||
|
||||
|
||||
GetModuleFileNameA(
|
||||
NULL,
|
||||
path,
|
||||
sizeof(path)
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[PROCESS] %s",
|
||||
path
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[PID] %lu",
|
||||
GetCurrentProcessId()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void DumpLoadedModules()
|
||||
{
|
||||
Log(
|
||||
"========== MODULE LIST =========="
|
||||
);
|
||||
|
||||
|
||||
HANDLE snap =
|
||||
CreateToolhelp32Snapshot(
|
||||
TH32CS_SNAPMODULE,
|
||||
GetCurrentProcessId()
|
||||
);
|
||||
|
||||
|
||||
if(snap == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
Log(
|
||||
"[MODULE] Failed"
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
MODULEENTRY32 me;
|
||||
|
||||
me.dwSize =
|
||||
sizeof(me);
|
||||
|
||||
|
||||
|
||||
if(Module32First(
|
||||
snap,
|
||||
&me
|
||||
))
|
||||
{
|
||||
do
|
||||
{
|
||||
MODULEINFO info;
|
||||
|
||||
|
||||
if(GetModuleInformation(
|
||||
GetCurrentProcess(),
|
||||
me.hModule,
|
||||
&info,
|
||||
sizeof(info)
|
||||
))
|
||||
{
|
||||
Log(
|
||||
"[MODULE] %s Base=%p Size=%lu",
|
||||
me.szModule,
|
||||
info.lpBaseOfDll,
|
||||
info.SizeOfImage
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(
|
||||
"[MODULE] %s",
|
||||
me.szModule
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
} while(Module32Next(
|
||||
snap,
|
||||
&me
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
CloseHandle(
|
||||
snap
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"================================"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void LogStack()
|
||||
{
|
||||
void *frames[32];
|
||||
|
||||
|
||||
USHORT count =
|
||||
CaptureStackBackTrace(
|
||||
1,
|
||||
32,
|
||||
frames,
|
||||
NULL
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[STACK] Frames=%d",
|
||||
count
|
||||
);
|
||||
|
||||
|
||||
for(
|
||||
int i = 0;
|
||||
i < count;
|
||||
i++
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[STACK] %p",
|
||||
frames[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
LONG WINAPI MyExceptionHandler(
|
||||
EXCEPTION_POINTERS *e
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"========== DLL CRASH =========="
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[EXCEPTION] 0x%08X",
|
||||
e->ExceptionRecord->ExceptionCode
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[ADDRESS] %p",
|
||||
e->ExceptionRecord->ExceptionAddress
|
||||
);
|
||||
|
||||
|
||||
LogStack();
|
||||
|
||||
|
||||
Log(
|
||||
"=============================="
|
||||
);
|
||||
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
|
||||
void LogAPIRequest(
|
||||
const char *method,
|
||||
const char *url
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[API] %s %s",
|
||||
method ? method : "UNKNOWN",
|
||||
url ? url : "NULL"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "common.h"
|
||||
#include "logger.h"
|
||||
|
||||
|
||||
void LogPacket(
|
||||
const char *direction,
|
||||
const void *data,
|
||||
size_t size
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[PACKET] %s %zu bytes",
|
||||
direction ? direction : "UNKNOWN",
|
||||
size
|
||||
);
|
||||
|
||||
|
||||
//
|
||||
// Hex dumping can be added later
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// SEND:
|
||||
// 16 03 01 00 5A ...
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
|
||||
void LogTLS(
|
||||
const char *event
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[TLS] %s",
|
||||
event ? event : "NULL"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "logger.h"
|
||||
#include "process.h"
|
||||
#include "hook_manager.h"
|
||||
|
||||
|
||||
BOOL WINAPI DllMain(
|
||||
HINSTANCE hinst,
|
||||
DWORD reason,
|
||||
LPVOID reserved
|
||||
)
|
||||
{
|
||||
if(reason == DLL_PROCESS_ATTACH)
|
||||
{
|
||||
DisableThreadLibraryCalls(hinst);
|
||||
|
||||
HANDLE thread = CreateThread(
|
||||
NULL,
|
||||
0,
|
||||
HookThread,
|
||||
NULL,
|
||||
0,
|
||||
NULL
|
||||
);
|
||||
|
||||
if(thread)
|
||||
CloseHandle(thread);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "connect_hook.h"
|
||||
|
||||
#include "logger.h"
|
||||
#include "config.h"
|
||||
|
||||
|
||||
|
||||
connect_t real_connect = NULL;
|
||||
|
||||
connect_t original_connect = NULL;
|
||||
|
||||
|
||||
BYTE backup_connect[14];
|
||||
|
||||
|
||||
|
||||
|
||||
int WSAAPI hook_connect(
|
||||
SOCKET s,
|
||||
const struct sockaddr *name,
|
||||
int namelen
|
||||
)
|
||||
{
|
||||
if(!name)
|
||||
{
|
||||
if(original_connect)
|
||||
{
|
||||
return original_connect(
|
||||
s,
|
||||
name,
|
||||
namelen
|
||||
);
|
||||
}
|
||||
|
||||
return SOCKET_ERROR;
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct sockaddr_in redirect_addr;
|
||||
|
||||
memcpy(
|
||||
&redirect_addr,
|
||||
name,
|
||||
sizeof(struct sockaddr_in)
|
||||
);
|
||||
|
||||
|
||||
|
||||
char ip[INET_ADDRSTRLEN] = {0};
|
||||
|
||||
|
||||
|
||||
if(name->sa_family == AF_INET)
|
||||
{
|
||||
SOCKADDR_IN *addr =
|
||||
(SOCKADDR_IN*)name;
|
||||
|
||||
|
||||
|
||||
inet_ntop(
|
||||
AF_INET,
|
||||
&addr->sin_addr,
|
||||
ip,
|
||||
sizeof(ip)
|
||||
);
|
||||
|
||||
|
||||
|
||||
Log(
|
||||
"[CONNECT] Socket %d attempting connection to %s:%d",
|
||||
s,
|
||||
ip,
|
||||
ntohs(addr->sin_port)
|
||||
);
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Redirect HTTPS traffic
|
||||
//
|
||||
|
||||
if(
|
||||
addr->sin_port == htons(443)
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[CONNECT REDIRECT] %s:%d -> %s:%d",
|
||||
ip,
|
||||
ntohs(addr->sin_port),
|
||||
redirect_ip,
|
||||
redirect_port
|
||||
);
|
||||
|
||||
|
||||
|
||||
redirect_addr.sin_addr.s_addr =
|
||||
inet_addr(
|
||||
redirect_ip
|
||||
);
|
||||
|
||||
|
||||
redirect_addr.sin_port =
|
||||
htons(
|
||||
redirect_port
|
||||
);
|
||||
|
||||
|
||||
|
||||
name =
|
||||
(struct sockaddr*)
|
||||
&redirect_addr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if(original_connect)
|
||||
{
|
||||
int ret =
|
||||
original_connect(
|
||||
s,
|
||||
name,
|
||||
namelen
|
||||
);
|
||||
|
||||
|
||||
|
||||
if(
|
||||
ret == 0 ||
|
||||
(
|
||||
ret == SOCKET_ERROR &&
|
||||
WSAGetLastError() ==
|
||||
WSAEWOULDBLOCK
|
||||
)
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[CONNECT] Socket %d connection established",
|
||||
s
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(
|
||||
"[CONNECT] Socket %d connection FAILED. Error: %d",
|
||||
s,
|
||||
WSAGetLastError()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return SOCKET_ERROR;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "dns_hook.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "logger.h"
|
||||
#include "strings.h"
|
||||
#include "process.h"
|
||||
|
||||
|
||||
|
||||
getaddrinfo_t real_getaddrinfo = NULL;
|
||||
|
||||
getaddrinfo_t original_getaddrinfo = NULL;
|
||||
|
||||
|
||||
gethostbyname_t real_gethostbyname = NULL;
|
||||
|
||||
|
||||
BYTE backup_getaddrinfo[32];
|
||||
|
||||
|
||||
|
||||
static int redirect_count = 0;
|
||||
|
||||
|
||||
|
||||
struct hostent *WSAAPI hook_gethostbyname(
|
||||
const char *name
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[HOSTBYNAME] %s",
|
||||
name ? name : "NULL"
|
||||
);
|
||||
|
||||
|
||||
if(real_gethostbyname)
|
||||
return real_gethostbyname(name);
|
||||
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
int WSAAPI hook_getaddrinfo(
|
||||
PCSTR node,
|
||||
PCSTR service,
|
||||
const ADDRINFOA *hints,
|
||||
PADDRINFOA *result
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"=============================="
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[DNS REQUEST]"
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[THREAD] %lu",
|
||||
GetCurrentThreadId()
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[DNS HOST] %s",
|
||||
node ? node : "NULL"
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[DNS SERVICE] %s",
|
||||
service ? service : "NULL"
|
||||
);
|
||||
|
||||
|
||||
LogStack();
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Host rewrite: swap the hostname (e.g. ns.rec.net -> ns.recflare.net) and let real DNS
|
||||
// resolve the target's current IP. We don't synthesize a static address, so the redirect
|
||||
// survives the target's IP changing.
|
||||
//
|
||||
|
||||
char rewritten[256];
|
||||
|
||||
|
||||
if(
|
||||
RewriteHost(
|
||||
node,
|
||||
rewritten,
|
||||
sizeof(rewritten)
|
||||
)
|
||||
)
|
||||
{
|
||||
redirect_count++;
|
||||
|
||||
|
||||
Log(
|
||||
"[REDIRECT #%d] %s -> %s (resolving)",
|
||||
redirect_count,
|
||||
node,
|
||||
rewritten
|
||||
);
|
||||
|
||||
|
||||
if(original_getaddrinfo)
|
||||
{
|
||||
int ret =
|
||||
original_getaddrinfo(
|
||||
rewritten,
|
||||
service,
|
||||
hints,
|
||||
result
|
||||
);
|
||||
|
||||
|
||||
if(ret != 0)
|
||||
Log(
|
||||
"[DNS FAIL] %s (rewritten, %d)",
|
||||
rewritten,
|
||||
ret
|
||||
);
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
return EAI_FAIL;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if(original_getaddrinfo)
|
||||
{
|
||||
int ret =
|
||||
original_getaddrinfo(
|
||||
node,
|
||||
service,
|
||||
hints,
|
||||
result
|
||||
);
|
||||
|
||||
|
||||
|
||||
if(
|
||||
ret == 0 &&
|
||||
result &&
|
||||
*result
|
||||
)
|
||||
{
|
||||
SOCKADDR_IN *addr =
|
||||
(SOCKADDR_IN*)
|
||||
(*result)->ai_addr;
|
||||
|
||||
|
||||
|
||||
char ip[INET_ADDRSTRLEN];
|
||||
|
||||
|
||||
inet_ntop(
|
||||
AF_INET,
|
||||
&addr->sin_addr,
|
||||
ip,
|
||||
sizeof(ip)
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[DNS RESULT] %s -> %s",
|
||||
node,
|
||||
ip
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(
|
||||
"[DNS FAIL] %s (%d)",
|
||||
node,
|
||||
ret
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return EAI_FAIL;
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "hook_manager.h"
|
||||
|
||||
#include "logger.h"
|
||||
#include "config.h"
|
||||
#include "process.h"
|
||||
|
||||
#include "dns_hook.h"
|
||||
#include "connect_hook.h"
|
||||
|
||||
#include "detour.h"
|
||||
|
||||
#include "module_watch.h"
|
||||
#include "ssl_patch.h"
|
||||
#include "http_rewrite.h"
|
||||
#include "memcheck_patch.h"
|
||||
#include "eac_patch.h"
|
||||
|
||||
|
||||
|
||||
static BOOL unity_loaded = FALSE;
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Wait until UnityPlayer.dll exists. Bounded: our version.dll also loads into non-game processes
|
||||
// (EAC launcher/bootstrap, crash handler) where UnityPlayer never appears -- those must give up and
|
||||
// let the thread exit instead of spinning forever. Returns FALSE if Unity never showed up.
|
||||
//
|
||||
|
||||
#define UNITY_WAIT_MS 60000
|
||||
|
||||
static BOOL WaitForUnity()
|
||||
{
|
||||
Log(
|
||||
"[UNITY] Waiting for UnityPlayer.dll..."
|
||||
);
|
||||
|
||||
|
||||
for(int waited = 0; waited < UNITY_WAIT_MS; waited += 100)
|
||||
{
|
||||
if(GetModuleHandleA("UnityPlayer.dll"))
|
||||
{
|
||||
unity_loaded = TRUE;
|
||||
|
||||
Log(
|
||||
"[UNITY] UnityPlayer.dll detected"
|
||||
);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
Sleep(100);
|
||||
}
|
||||
|
||||
|
||||
Log(
|
||||
"[UNITY] UnityPlayer.dll not found after %d ms -- not a game process, exiting hook thread",
|
||||
UNITY_WAIT_MS
|
||||
);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Install all hooks
|
||||
//
|
||||
|
||||
BOOL InstallHooks()
|
||||
{
|
||||
HMODULE ws2 =
|
||||
GetModuleHandleA(
|
||||
"ws2_32.dll"
|
||||
);
|
||||
|
||||
|
||||
if(!ws2)
|
||||
{
|
||||
Log(
|
||||
"[HOOK] ws2_32.dll missing"
|
||||
);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
Log(
|
||||
"[HOOK] ws2_32.dll loaded"
|
||||
);
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Resolve functions
|
||||
//
|
||||
|
||||
real_getaddrinfo =
|
||||
(getaddrinfo_t)GetProcAddress(
|
||||
ws2,
|
||||
"getaddrinfo"
|
||||
);
|
||||
|
||||
|
||||
real_gethostbyname =
|
||||
(gethostbyname_t)GetProcAddress(
|
||||
ws2,
|
||||
"gethostbyname"
|
||||
);
|
||||
|
||||
|
||||
real_connect =
|
||||
(connect_t)GetProcAddress(
|
||||
ws2,
|
||||
"connect"
|
||||
);
|
||||
|
||||
|
||||
|
||||
Log(
|
||||
"[ADDR] getaddrinfo=%p",
|
||||
real_getaddrinfo
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[ADDR] gethostbyname=%p",
|
||||
real_gethostbyname
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[ADDR] connect=%p",
|
||||
real_connect
|
||||
);
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Install DNS hook
|
||||
//
|
||||
|
||||
if(real_getaddrinfo)
|
||||
{
|
||||
Log(
|
||||
"[HOOK] Installing getaddrinfo"
|
||||
);
|
||||
|
||||
|
||||
if(
|
||||
InstallDetour(
|
||||
real_getaddrinfo,
|
||||
hook_getaddrinfo,
|
||||
backup_getaddrinfo,
|
||||
(LPVOID*)&original_getaddrinfo
|
||||
)
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[HOOK] getaddrinfo installed"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(
|
||||
"[HOOK] getaddrinfo failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(
|
||||
"[HOOK] getaddrinfo missing"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Connect hook will be enabled later
|
||||
//
|
||||
// Currently disabled exactly like
|
||||
// your original test build.
|
||||
//
|
||||
|
||||
|
||||
|
||||
Log(
|
||||
"===================================="
|
||||
);
|
||||
|
||||
Log(
|
||||
"[STATUS] DNS REDIRECT ACTIVE"
|
||||
);
|
||||
|
||||
Log(
|
||||
"===================================="
|
||||
);
|
||||
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Main redirector thread
|
||||
//
|
||||
|
||||
DWORD WINAPI HookThread(
|
||||
LPVOID param
|
||||
)
|
||||
{
|
||||
//
|
||||
// Bail before AllocConsole/InitLogger in non-game processes. RecRoom.exe spawns
|
||||
// UnityCrashHandler64.exe out of the same folder, so our version.dll loads there too; without
|
||||
// this check every launch opened two debug consoles and left the crash handler spinning in
|
||||
// WaitForUnity for a minute. Nothing here belongs in that process anyway.
|
||||
//
|
||||
|
||||
if(!IsGameProcess())
|
||||
return 0;
|
||||
|
||||
|
||||
InitConsole();
|
||||
|
||||
InitLogger();
|
||||
|
||||
|
||||
SetUnhandledExceptionFilter(
|
||||
MyExceptionHandler
|
||||
);
|
||||
|
||||
|
||||
Log(
|
||||
"[THREAD] Hook thread started"
|
||||
);
|
||||
|
||||
|
||||
// Identify which process we're in (game vs EAC launcher vs crash handler).
|
||||
LogProcessInfo();
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Start module watcher
|
||||
//
|
||||
|
||||
HANDLE moduleThread =
|
||||
CreateThread(
|
||||
NULL,
|
||||
0,
|
||||
(LPTHREAD_START_ROUTINE)
|
||||
WatchModules,
|
||||
NULL,
|
||||
0,
|
||||
NULL
|
||||
);
|
||||
|
||||
|
||||
if(moduleThread)
|
||||
CloseHandle(moduleThread);
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Load redirect config
|
||||
//
|
||||
|
||||
LoadConfig();
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Wait for Unity. If this isn't the game process (EAC launcher, crash handler, ...), bail so we
|
||||
// don't spin forever or install hooks where they don't belong.
|
||||
//
|
||||
|
||||
if(!WaitForUnity())
|
||||
return 0;
|
||||
|
||||
|
||||
|
||||
Sleep(2000);
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Install hooks
|
||||
//
|
||||
|
||||
if(!InstallHooks())
|
||||
{
|
||||
Log(
|
||||
"[HOOK] Installation failed"
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Memory-integrity scan neutralizer. Started FIRST among the il2cpp patches because the boot
|
||||
// step that awaits the scan can fire early -- its reflection search needs a head start so the
|
||||
// scan-start detour is in place before boot calls it. Without this, boot fails "Launch
|
||||
// validation failed" once our other hooks perturb GameAssembly memory.
|
||||
//
|
||||
|
||||
HANDLE memThread =
|
||||
CreateThread(
|
||||
NULL,
|
||||
0,
|
||||
(LPTHREAD_START_ROUTINE)
|
||||
PatchMemoryIntegrityCheck,
|
||||
NULL,
|
||||
0,
|
||||
NULL
|
||||
);
|
||||
|
||||
|
||||
if(memThread)
|
||||
CloseHandle(memThread);
|
||||
|
||||
|
||||
|
||||
//
|
||||
// TLS pinning bypass. Runs on its own thread because it waits for the il2cpp runtime to
|
||||
// finish init (GameAssembly.dll + il2cpp_domain_get) before it can resolve+detour the
|
||||
// BouncyCastle NotifyServerCertificate method. Without this, HTTPS to the redirected server
|
||||
// fails the handshake (mismatched/pinned cert).
|
||||
//
|
||||
|
||||
HANDLE sslThread =
|
||||
CreateThread(
|
||||
NULL,
|
||||
0,
|
||||
(LPTHREAD_START_ROUTINE)
|
||||
PatchBestHTTPSSL,
|
||||
NULL,
|
||||
0,
|
||||
NULL
|
||||
);
|
||||
|
||||
|
||||
if(sslThread)
|
||||
CloseHandle(sslThread);
|
||||
|
||||
|
||||
|
||||
//
|
||||
// HTTP-layer host rewrite (ns.rec.net -> ns.recflare.net in the request Uri). Own thread: like
|
||||
// the SSL patch it waits for the il2cpp runtime before resolving+hooking SendRequest.
|
||||
//
|
||||
|
||||
HANDLE httpThread =
|
||||
CreateThread(
|
||||
NULL,
|
||||
0,
|
||||
(LPTHREAD_START_ROUTINE)
|
||||
PatchHttpHostRewrite,
|
||||
NULL,
|
||||
0,
|
||||
NULL
|
||||
);
|
||||
|
||||
|
||||
if(httpThread)
|
||||
CloseHandle(httpThread);
|
||||
|
||||
|
||||
|
||||
//
|
||||
// EAC neutralizer (force readiness true + base64 challenge response). Own thread; waits for the
|
||||
// il2cpp runtime. Safe now that the memory-integrity scan is neutralized.
|
||||
//
|
||||
|
||||
HANDLE eacThread =
|
||||
CreateThread(
|
||||
NULL,
|
||||
0,
|
||||
(LPTHREAD_START_ROUTINE)
|
||||
PatchEAC,
|
||||
NULL,
|
||||
0,
|
||||
NULL
|
||||
);
|
||||
|
||||
|
||||
if(eacThread)
|
||||
CloseHandle(eacThread);
|
||||
|
||||
|
||||
|
||||
Log(
|
||||
"[THREAD] Redirector initialized"
|
||||
);
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "detour.h"
|
||||
#include "logger.h"
|
||||
|
||||
|
||||
//
|
||||
// 14-byte absolute indirect jump: FF 25 00000000 <8-byte target>. rip-relative disp=0 means the
|
||||
// 64-bit pointer sits immediately after the 6-byte opcode, so this needs no register and can reach
|
||||
// anywhere in the address space.
|
||||
//
|
||||
#define JMP_PATCH_LEN 14
|
||||
|
||||
// Trampoline capacity: stolen bytes (<= ~24) + the 14-byte jump back.
|
||||
#define TRAMP_SIZE 128
|
||||
|
||||
|
||||
//
|
||||
// How a copied instruction must be fixed up once relocated to the trampoline.
|
||||
//
|
||||
typedef enum
|
||||
{
|
||||
RK_NONE, // position-independent, copy verbatim
|
||||
RK_RIPREL, // has a rip-relative disp32 operand (mod=00,rm=101)
|
||||
RK_REL32, // CALL/JMP rel32 (E8/E9)
|
||||
RK_UNSUPPORTED // rel8 branch etc. -- we won't relocate it, refuse the hook
|
||||
} reloc_kind;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
size_t len; // total instruction length
|
||||
reloc_kind kind;
|
||||
size_t disp_off; // offset of the disp32/rel32 field within the instruction
|
||||
} insn_t;
|
||||
|
||||
|
||||
//
|
||||
// Minimal x86-64 length decoder + relocation classifier.
|
||||
//
|
||||
// Measures whole-instruction boundaries so a trampoline never copies a torn instruction, and flags
|
||||
// the two relocation cases we can fix (rip-relative disp32, rel32 branch). Returns 1 on success with
|
||||
// *out filled; 0 if it hits an opcode we don't model (caller then refuses the hook rather than
|
||||
// corrupt code). rel8 branches are modelled (so length is known) but flagged RK_UNSUPPORTED.
|
||||
//
|
||||
static int decode(const uint8_t *p, insn_t *out)
|
||||
{
|
||||
size_t n = 0;
|
||||
int opsize = 0; // 0x66 present
|
||||
int rexW = 0;
|
||||
out->kind = RK_NONE;
|
||||
out->disp_off = 0;
|
||||
|
||||
// Legacy prefixes.
|
||||
for (;;)
|
||||
{
|
||||
uint8_t c = p[n];
|
||||
if (c == 0x66) { opsize = 1; n++; continue; }
|
||||
if (c == 0x67 || c == 0xF0 || c == 0xF2 || c == 0xF3 ||
|
||||
c == 0x2E || c == 0x36 || c == 0x3E || c == 0x26 ||
|
||||
c == 0x64 || c == 0x65) { n++; continue; }
|
||||
break;
|
||||
}
|
||||
|
||||
// REX prefix (0x40-0x4F).
|
||||
if ((p[n] & 0xF0) == 0x40) { rexW = (p[n] & 0x08) != 0; n++; }
|
||||
|
||||
uint8_t op = p[n++];
|
||||
|
||||
if (op == 0x0F)
|
||||
return 0; // two-byte opcodes: unsupported here, bail
|
||||
|
||||
// Relative branches. rel32 forms we can relocate; rel8 we model (length) but won't move.
|
||||
if (op == 0xE8 || op == 0xE9) // CALL/JMP rel32
|
||||
{
|
||||
out->kind = RK_REL32;
|
||||
out->disp_off = n;
|
||||
n += 4;
|
||||
out->len = n;
|
||||
return 1;
|
||||
}
|
||||
if ((op >= 0x70 && op <= 0x7F) || // Jcc rel8
|
||||
(op >= 0xE0 && op <= 0xE3) || // LOOP/JrCXZ
|
||||
op == 0xEB) // JMP rel8
|
||||
{
|
||||
out->kind = RK_UNSUPPORTED;
|
||||
n += 1;
|
||||
out->len = n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Does this opcode carry a ModR/M byte?
|
||||
int hasModRM =
|
||||
( op <= 0x3F && (op & 0x07) < 0x04 ) || // arith r/m forms
|
||||
op == 0x62 || op == 0x63 || op == 0x69 || op == 0x6B ||
|
||||
( op >= 0x80 && op <= 0x8F ) || // grp1/test/xchg/mov/lea/pop
|
||||
op == 0xC0 || op == 0xC1 || op == 0xC6 || op == 0xC7 ||
|
||||
( op >= 0xD0 && op <= 0xD3 ) ||
|
||||
( op >= 0xD8 && op <= 0xDF ) || // x87
|
||||
op == 0xF6 || op == 0xF7 || op == 0xFE || op == 0xFF;
|
||||
|
||||
uint8_t modrm_reg = 0;
|
||||
|
||||
if (hasModRM)
|
||||
{
|
||||
uint8_t modrm = p[n++];
|
||||
uint8_t mod = modrm >> 6;
|
||||
uint8_t rm = modrm & 0x07;
|
||||
modrm_reg = (modrm >> 3) & 0x07;
|
||||
|
||||
if (mod != 0x03)
|
||||
{
|
||||
if (rm == 0x04) // SIB
|
||||
{
|
||||
uint8_t sib = p[n++];
|
||||
uint8_t base = sib & 0x07;
|
||||
if (mod == 0x00 && base == 0x05) n += 4; // disp32, no base
|
||||
else if (mod == 0x01) n += 1;
|
||||
else if (mod == 0x02) n += 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mod == 0x00)
|
||||
{
|
||||
if (rm == 0x05) // rip-relative disp32
|
||||
{
|
||||
out->kind = RK_RIPREL;
|
||||
out->disp_off = n;
|
||||
n += 4;
|
||||
}
|
||||
}
|
||||
else if (mod == 0x01) n += 1;
|
||||
else if (mod == 0x02) n += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Immediate size (comes AFTER any disp -- matters for rip-relative rip = end of whole insn).
|
||||
size_t imm = 0;
|
||||
switch (op)
|
||||
{
|
||||
case 0x04: case 0x0C: case 0x14: case 0x1C:
|
||||
case 0x24: case 0x2C: case 0x34: case 0x3C:
|
||||
case 0x6A: case 0x6B: case 0x80: case 0x82: case 0x83:
|
||||
case 0xA8: case 0xC0: case 0xC1: case 0xC6:
|
||||
case 0xB0: case 0xB1: case 0xB2: case 0xB3:
|
||||
case 0xB4: case 0xB5: case 0xB6: case 0xB7:
|
||||
imm = 1; break;
|
||||
|
||||
case 0xC2: case 0xCA: // ret imm16
|
||||
imm = 2; break;
|
||||
|
||||
case 0x05: case 0x0D: case 0x15: case 0x1D:
|
||||
case 0x25: case 0x2D: case 0x35: case 0x3D:
|
||||
case 0x68: case 0x69: case 0x81: case 0xA9:
|
||||
case 0xC7:
|
||||
imm = opsize ? 2 : 4; break;
|
||||
|
||||
case 0xB8: case 0xB9: case 0xBA: case 0xBB:
|
||||
case 0xBC: case 0xBD: case 0xBE: case 0xBF: // mov r,imm (imm64 if REX.W)
|
||||
imm = rexW ? 8 : (opsize ? 2 : 4); break;
|
||||
|
||||
case 0xF6: // grp3: imm8 only for TEST (reg 0/1)
|
||||
if (modrm_reg <= 1) imm = 1; break;
|
||||
case 0xF7: // grp3: imm16/32 for TEST (reg 0/1)
|
||||
if (modrm_reg <= 1) imm = opsize ? 2 : 4; break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
n += imm;
|
||||
|
||||
out->len = n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Sum whole instructions until we have at least JMP_PATCH_LEN bytes to overwrite. 0 => a decode
|
||||
// failed (unknown opcode) and the hook must be refused.
|
||||
//
|
||||
static size_t steal_len(const uint8_t *target)
|
||||
{
|
||||
size_t total = 0;
|
||||
insn_t insn;
|
||||
while (total < JMP_PATCH_LEN)
|
||||
{
|
||||
if (!decode(target + total, &insn)) return 0;
|
||||
total += insn.len;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
|
||||
static void WriteAbsJump(BYTE *at, LPVOID dest)
|
||||
{
|
||||
at[0] = 0xFF; at[1] = 0x25;
|
||||
at[2] = at[3] = at[4] = at[5] = 0x00;
|
||||
*(void **)&at[6] = dest;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Allocate executable memory within +-2GB of target, so relocated rip-relative disp32 / rel32
|
||||
// fields (which reference addresses near the original code) still encode in 32 bits. Falls back to
|
||||
// anywhere; the per-field range check in InstallDetour is the safety net if that isn't close enough.
|
||||
//
|
||||
static LPVOID AllocNear(void *target, size_t size)
|
||||
{
|
||||
SYSTEM_INFO si;
|
||||
GetSystemInfo(&si);
|
||||
uintptr_t gran = si.dwAllocationGranularity;
|
||||
|
||||
uintptr_t t = (uintptr_t)target;
|
||||
uintptr_t base = t & ~(gran - 1);
|
||||
|
||||
const uintptr_t MAXDIST = 0x70000000ULL; // ~1.87GB, margin under the 2GB limit
|
||||
|
||||
for (uintptr_t off = gran; off < MAXDIST; off += gran)
|
||||
{
|
||||
uintptr_t lo = base - off;
|
||||
if (lo < base) // no underflow
|
||||
{
|
||||
LPVOID p = VirtualAlloc((LPVOID)lo, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
|
||||
if (p) return p;
|
||||
}
|
||||
|
||||
uintptr_t hi = base + off;
|
||||
if (hi > base) // no overflow
|
||||
{
|
||||
LPVOID p = VirtualAlloc((LPVOID)hi, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
|
||||
if (p) return p;
|
||||
}
|
||||
}
|
||||
|
||||
return VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Copy [target, target+stolen) into the trampoline, fixing rip-relative and rel32 operands for the
|
||||
// new location. Returns 1 on success, 0 if an instruction can't be relocated (rel8, or a fixup that
|
||||
// no longer fits in int32).
|
||||
//
|
||||
static int RelocateInto(BYTE *tramp, const uint8_t *target, size_t stolen)
|
||||
{
|
||||
size_t off = 0;
|
||||
insn_t insn;
|
||||
|
||||
while (off < stolen)
|
||||
{
|
||||
if (!decode(target + off, &insn))
|
||||
return 0;
|
||||
|
||||
memcpy(tramp + off, target + off, insn.len);
|
||||
|
||||
if (insn.kind == RK_RIPREL || insn.kind == RK_REL32)
|
||||
{
|
||||
const uint8_t *src = target + off;
|
||||
BYTE *dst = tramp + off;
|
||||
|
||||
// rip is relative to the END of the whole instruction (past any trailing immediate),
|
||||
// so use insn.len, not disp_off+4.
|
||||
int32_t oldDisp = *(int32_t *)(src + insn.disp_off);
|
||||
uintptr_t absTarget = (uintptr_t)src + insn.len + (intptr_t)oldDisp;
|
||||
|
||||
int64_t newDisp = (int64_t)absTarget - (int64_t)((uintptr_t)dst + insn.len);
|
||||
if (newDisp > INT32_MAX || newDisp < INT32_MIN)
|
||||
{
|
||||
Log("[DETOUR] relocation out of int32 range at +%zu -- refusing", off);
|
||||
return 0;
|
||||
}
|
||||
|
||||
*(int32_t *)(dst + insn.disp_off) = (int32_t)newDisp;
|
||||
}
|
||||
else if (insn.kind == RK_UNSUPPORTED)
|
||||
{
|
||||
Log("[DETOUR] rel8 branch in stolen prologue at +%zu -- refusing (needs rel8->rel32 rewrite)", off);
|
||||
return 0;
|
||||
}
|
||||
|
||||
off += insn.len;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int InstallDetour(
|
||||
LPVOID target,
|
||||
LPVOID hook,
|
||||
BYTE *backup,
|
||||
LPVOID *outTrampoline
|
||||
)
|
||||
{
|
||||
if (!target || !hook)
|
||||
{
|
||||
Log("[DETOUR] Invalid target/hook");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// How many bytes we overwrite at the entry. For a call-through hook (outTrampoline != NULL) we
|
||||
// steal whole instructions, relocate them into a trampoline, and chain back to the original.
|
||||
// For a replace-only hook (outTrampoline == NULL, e.g. the SSL accept-all that never calls the
|
||||
// original) 14 bytes is enough -- we jump away immediately, so a torn trailing instruction is
|
||||
// never executed.
|
||||
//
|
||||
size_t stolen = JMP_PATCH_LEN;
|
||||
|
||||
if (outTrampoline)
|
||||
{
|
||||
stolen = steal_len((const uint8_t *)target);
|
||||
if (stolen == 0)
|
||||
{
|
||||
Log("[DETOUR] %p: undecodable prologue -- refusing hook", target);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Preserve the original bytes for the caller.
|
||||
memcpy(backup, target, stolen);
|
||||
|
||||
|
||||
//
|
||||
// Build the trampoline (call-through hooks only): relocated stolen instructions + jump back.
|
||||
//
|
||||
LPVOID trampoline = NULL;
|
||||
|
||||
if (outTrampoline)
|
||||
{
|
||||
trampoline = AllocNear(target, TRAMP_SIZE);
|
||||
if (!trampoline)
|
||||
{
|
||||
Log("[DETOUR] trampoline allocation failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!RelocateInto((BYTE *)trampoline, (const uint8_t *)target, stolen))
|
||||
{
|
||||
VirtualFree(trampoline, 0, MEM_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
WriteAbsJump((BYTE *)trampoline + stolen, (BYTE *)target + stolen); // resume original
|
||||
FlushInstructionCache(GetCurrentProcess(), trampoline, stolen + JMP_PATCH_LEN);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Overwrite the entry: 14-byte jump to hook, NOP-pad any remaining stolen bytes so no torn
|
||||
// instruction is left in the live code stream.
|
||||
//
|
||||
DWORD oldProtect;
|
||||
if (!VirtualProtect(target, stolen, PAGE_EXECUTE_READWRITE, &oldProtect))
|
||||
{
|
||||
Log("[DETOUR] VirtualProtect failed %lu", GetLastError());
|
||||
if (trampoline) VirtualFree(trampoline, 0, MEM_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
WriteAbsJump((BYTE *)target, hook);
|
||||
for (size_t i = JMP_PATCH_LEN; i < stolen; i++)
|
||||
((BYTE *)target)[i] = 0x90; // NOP pad
|
||||
|
||||
DWORD tmp;
|
||||
VirtualProtect(target, stolen, oldProtect, &tmp);
|
||||
FlushInstructionCache(GetCurrentProcess(), target, stolen);
|
||||
|
||||
|
||||
if (outTrampoline)
|
||||
*outTrampoline = trampoline;
|
||||
|
||||
|
||||
Log("[DETOUR] %p -> %p (stole %zu bytes%s)", target, hook, stolen,
|
||||
outTrampoline ? ", relocated trampoline" : "");
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
#include "common.h"
|
||||
|
||||
//
|
||||
// version.dll proxy (runtime-forwarding, self-contained).
|
||||
//
|
||||
// RecRoom.exe/UnityPlayer.dll import VERSION.dll by name, and the loader searches the app dir before
|
||||
// System32, so our version.dll in the game root is loaded in their place -- our injection vector
|
||||
// (DllMain in dllmain.c starts the hook thread). To keep being a working version.dll we must still
|
||||
// satisfy every export the game asks for.
|
||||
//
|
||||
// Rather than ship a renamed copy of the system DLL (version_orig.dll) and use static PE forwarders,
|
||||
// each export here is a thin wrapper that lazily loads the REAL system version.dll -- by FULL PATH,
|
||||
// so it never recurses back into us -- and calls through. Result: a single self-contained version.dll
|
||||
// with nothing extra to ship, which is less confusing for users.
|
||||
//
|
||||
// Only UnityPlayer.dll imports version.dll here, and only GetFileVersionInfoA/SizeA + VerQueryValueA,
|
||||
// but we export the full set faithfully.
|
||||
//
|
||||
// We can't name these functions the same as the Win32 APIs (the SDK headers declare them dllimport,
|
||||
// which conflicts with dllexport), so they get my_ names and are exported under the real names via
|
||||
// linker /export aliases below. Aliases (no dot) reference our local symbols; they are NOT forwarders.
|
||||
|
||||
static volatile HMODULE g_real;
|
||||
|
||||
// Load (once) the genuine system version.dll by absolute path so app-dir search can't loop to us.
|
||||
static HMODULE RealVersion(void)
|
||||
{
|
||||
HMODULE h = g_real;
|
||||
if (h) return h;
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
UINT n = GetSystemDirectoryW(path, MAX_PATH);
|
||||
if (n == 0 || n > MAX_PATH - 16) return NULL;
|
||||
lstrcatW(path, L"\\version.dll");
|
||||
|
||||
HMODULE loaded = LoadLibraryW(path);
|
||||
HMODULE prev = (HMODULE)InterlockedCompareExchangePointer((volatile PVOID *)&g_real, loaded, NULL);
|
||||
if (prev) { if (loaded) FreeLibrary(loaded); return prev; } // lost the race
|
||||
return loaded;
|
||||
}
|
||||
|
||||
static FARPROC Proc(const char *name)
|
||||
{
|
||||
HMODULE h = RealVersion();
|
||||
return h ? GetProcAddress(h, name) : NULL;
|
||||
}
|
||||
|
||||
// One wrapper per export. The static function-pointer cache is a benign race (GetProcAddress is
|
||||
// idempotent). A NULL real proc (system DLL missing) degrades to a harmless zero/FALSE return.
|
||||
|
||||
BOOL WINAPI my_GetFileVersionInfoA(LPCSTR f, DWORD h, DWORD len, LPVOID data)
|
||||
{
|
||||
typedef BOOL (WINAPI *F)(LPCSTR, DWORD, DWORD, LPVOID);
|
||||
static F fn; if (!fn) fn = (F)Proc("GetFileVersionInfoA");
|
||||
return fn ? fn(f, h, len, data) : FALSE;
|
||||
}
|
||||
|
||||
BOOL WINAPI my_GetFileVersionInfoW(LPCWSTR f, DWORD h, DWORD len, LPVOID data)
|
||||
{
|
||||
typedef BOOL (WINAPI *F)(LPCWSTR, DWORD, DWORD, LPVOID);
|
||||
static F fn; if (!fn) fn = (F)Proc("GetFileVersionInfoW");
|
||||
return fn ? fn(f, h, len, data) : FALSE;
|
||||
}
|
||||
|
||||
BOOL WINAPI my_GetFileVersionInfoExA(DWORD flags, LPCSTR f, DWORD h, DWORD len, LPVOID data)
|
||||
{
|
||||
typedef BOOL (WINAPI *F)(DWORD, LPCSTR, DWORD, DWORD, LPVOID);
|
||||
static F fn; if (!fn) fn = (F)Proc("GetFileVersionInfoExA");
|
||||
return fn ? fn(flags, f, h, len, data) : FALSE;
|
||||
}
|
||||
|
||||
BOOL WINAPI my_GetFileVersionInfoExW(DWORD flags, LPCWSTR f, DWORD h, DWORD len, LPVOID data)
|
||||
{
|
||||
typedef BOOL (WINAPI *F)(DWORD, LPCWSTR, DWORD, DWORD, LPVOID);
|
||||
static F fn; if (!fn) fn = (F)Proc("GetFileVersionInfoExW");
|
||||
return fn ? fn(flags, f, h, len, data) : FALSE;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_GetFileVersionInfoSizeA(LPCSTR f, LPDWORD h)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(LPCSTR, LPDWORD);
|
||||
static F fn; if (!fn) fn = (F)Proc("GetFileVersionInfoSizeA");
|
||||
return fn ? fn(f, h) : 0;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_GetFileVersionInfoSizeW(LPCWSTR f, LPDWORD h)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(LPCWSTR, LPDWORD);
|
||||
static F fn; if (!fn) fn = (F)Proc("GetFileVersionInfoSizeW");
|
||||
return fn ? fn(f, h) : 0;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_GetFileVersionInfoSizeExA(DWORD flags, LPCSTR f, LPDWORD h)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(DWORD, LPCSTR, LPDWORD);
|
||||
static F fn; if (!fn) fn = (F)Proc("GetFileVersionInfoSizeExA");
|
||||
return fn ? fn(flags, f, h) : 0;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_GetFileVersionInfoSizeExW(DWORD flags, LPCWSTR f, LPDWORD h)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(DWORD, LPCWSTR, LPDWORD);
|
||||
static F fn; if (!fn) fn = (F)Proc("GetFileVersionInfoSizeExW");
|
||||
return fn ? fn(flags, f, h) : 0;
|
||||
}
|
||||
|
||||
BOOL WINAPI my_VerQueryValueA(LPCVOID block, LPCSTR sub, LPVOID *buf, PUINT len)
|
||||
{
|
||||
typedef BOOL (WINAPI *F)(LPCVOID, LPCSTR, LPVOID *, PUINT);
|
||||
static F fn; if (!fn) fn = (F)Proc("VerQueryValueA");
|
||||
return fn ? fn(block, sub, buf, len) : FALSE;
|
||||
}
|
||||
|
||||
BOOL WINAPI my_VerQueryValueW(LPCVOID block, LPCWSTR sub, LPVOID *buf, PUINT len)
|
||||
{
|
||||
typedef BOOL (WINAPI *F)(LPCVOID, LPCWSTR, LPVOID *, PUINT);
|
||||
static F fn; if (!fn) fn = (F)Proc("VerQueryValueW");
|
||||
return fn ? fn(block, sub, buf, len) : FALSE;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_VerFindFileA(DWORD flags, LPCSTR file, LPCSTR win, LPCSTR app,
|
||||
LPSTR cur, PUINT curLen, LPSTR dest, PUINT destLen)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(DWORD, LPCSTR, LPCSTR, LPCSTR, LPSTR, PUINT, LPSTR, PUINT);
|
||||
static F fn; if (!fn) fn = (F)Proc("VerFindFileA");
|
||||
return fn ? fn(flags, file, win, app, cur, curLen, dest, destLen) : 0;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_VerFindFileW(DWORD flags, LPCWSTR file, LPCWSTR win, LPCWSTR app,
|
||||
LPWSTR cur, PUINT curLen, LPWSTR dest, PUINT destLen)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(DWORD, LPCWSTR, LPCWSTR, LPCWSTR, LPWSTR, PUINT, LPWSTR, PUINT);
|
||||
static F fn; if (!fn) fn = (F)Proc("VerFindFileW");
|
||||
return fn ? fn(flags, file, win, app, cur, curLen, dest, destLen) : 0;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_VerInstallFileA(DWORD flags, LPCSTR src, LPCSTR dst, LPCSTR srcDir,
|
||||
LPCSTR dstDir, LPCSTR curDir, LPSTR tmp, PUINT tmpLen)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(DWORD, LPCSTR, LPCSTR, LPCSTR, LPCSTR, LPCSTR, LPSTR, PUINT);
|
||||
static F fn; if (!fn) fn = (F)Proc("VerInstallFileA");
|
||||
return fn ? fn(flags, src, dst, srcDir, dstDir, curDir, tmp, tmpLen) : 0;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_VerInstallFileW(DWORD flags, LPCWSTR src, LPCWSTR dst, LPCWSTR srcDir,
|
||||
LPCWSTR dstDir, LPCWSTR curDir, LPWSTR tmp, PUINT tmpLen)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(DWORD, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, LPWSTR, PUINT);
|
||||
static F fn; if (!fn) fn = (F)Proc("VerInstallFileW");
|
||||
return fn ? fn(flags, src, dst, srcDir, dstDir, curDir, tmp, tmpLen) : 0;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_VerLanguageNameA(DWORD lang, LPSTR buf, DWORD size)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(DWORD, LPSTR, DWORD);
|
||||
static F fn; if (!fn) fn = (F)Proc("VerLanguageNameA");
|
||||
return fn ? fn(lang, buf, size) : 0;
|
||||
}
|
||||
|
||||
DWORD WINAPI my_VerLanguageNameW(DWORD lang, LPWSTR buf, DWORD size)
|
||||
{
|
||||
typedef DWORD (WINAPI *F)(DWORD, LPWSTR, DWORD);
|
||||
static F fn; if (!fn) fn = (F)Proc("VerLanguageNameW");
|
||||
return fn ? fn(lang, buf, size) : 0;
|
||||
}
|
||||
|
||||
// Undocumented; not imported by anything in this game. Faithful passthrough with a best-effort
|
||||
// signature (never actually called here).
|
||||
BOOL WINAPI my_GetFileVersionInfoByHandle(int a, HANDLE b, DWORD c, LPVOID d)
|
||||
{
|
||||
typedef BOOL (WINAPI *F)(int, HANDLE, DWORD, LPVOID);
|
||||
static F fn; if (!fn) fn = (F)Proc("GetFileVersionInfoByHandle");
|
||||
return fn ? fn(a, b, c, d) : FALSE;
|
||||
}
|
||||
|
||||
// Export each under its real name (alias to our local my_ symbol; not a forwarder).
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoA=my_GetFileVersionInfoA")
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoW=my_GetFileVersionInfoW")
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoExA=my_GetFileVersionInfoExA")
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoExW=my_GetFileVersionInfoExW")
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoSizeA=my_GetFileVersionInfoSizeA")
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoSizeW=my_GetFileVersionInfoSizeW")
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoSizeExA=my_GetFileVersionInfoSizeExA")
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoSizeExW=my_GetFileVersionInfoSizeExW")
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoByHandle=my_GetFileVersionInfoByHandle")
|
||||
#pragma comment(linker, "/export:VerQueryValueA=my_VerQueryValueA")
|
||||
#pragma comment(linker, "/export:VerQueryValueW=my_VerQueryValueW")
|
||||
#pragma comment(linker, "/export:VerFindFileA=my_VerFindFileA")
|
||||
#pragma comment(linker, "/export:VerFindFileW=my_VerFindFileW")
|
||||
#pragma comment(linker, "/export:VerInstallFileA=my_VerInstallFileA")
|
||||
#pragma comment(linker, "/export:VerInstallFileW=my_VerInstallFileW")
|
||||
#pragma comment(linker, "/export:VerLanguageNameA=my_VerLanguageNameA")
|
||||
#pragma comment(linker, "/export:VerLanguageNameW=my_VerLanguageNameW")
|
||||
@@ -0,0 +1,251 @@
|
||||
#include "common.h"
|
||||
#include "eac_patch.h"
|
||||
#include "logger.h"
|
||||
#include "detour.h"
|
||||
|
||||
//
|
||||
// Native port of the managed EACPatches (RecNetPlugin). Two hooks on RecRoom.AntiCheat.EACManager:
|
||||
//
|
||||
// 1. Readiness check -> true. The client won't proceed unless EAC reports "ready"; the real check
|
||||
// depends on the EasyAntiCheat runtime talking to live services that no longer exist. It's the
|
||||
// only static, 0-param, bool-returning, non-property-getter method on EACManager (obfuscated
|
||||
// name rotates every build -- resolved by that signature).
|
||||
//
|
||||
// 2. GenerateChallengeResponse(string) -> base64(challenge). Unobfuscated name. The server-side
|
||||
// handshake expects base64 of the challenge (empty/null -> base64("nothing")), matching what the
|
||||
// managed build supplied.
|
||||
//
|
||||
// Both are replace-only detours (we never call the originals). Safe to modify EACManager code now that
|
||||
// the native memory-integrity scan is neutralized (see memcheck_patch.c) -- otherwise this would trip
|
||||
// the hash mismatch.
|
||||
|
||||
#define METHOD_ATTRIBUTE_MEMBER_ACCESS_MASK 0x0007
|
||||
#define METHOD_ATTRIBUTE_STATIC 0x0010
|
||||
#define METHOD_ATTRIBUTE_SPECIAL_NAME 0x0800
|
||||
#define IL2CPP_TYPE_BOOLEAN 0x02
|
||||
|
||||
typedef void* (*il2cpp_domain_get_t)(void);
|
||||
typedef int (*il2cpp_thread_attach_t)(void*);
|
||||
typedef void** (*il2cpp_domain_get_assemblies_t)(void*, size_t*);
|
||||
typedef void* (*il2cpp_assembly_get_image_t)(void*);
|
||||
typedef void* (*il2cpp_class_from_name_t)(void*, const char*, const char*);
|
||||
typedef void* (*il2cpp_class_get_method_from_name_t)(void*, const char*, int);
|
||||
typedef void* (*il2cpp_class_get_methods_t)(void*, void**);
|
||||
typedef const char* (*il2cpp_method_get_name_t)(void*);
|
||||
typedef uint32_t (*il2cpp_method_get_flags_t)(void*, uint32_t*);
|
||||
typedef uint32_t (*il2cpp_method_get_param_count_t)(void*);
|
||||
typedef void* (*il2cpp_method_get_return_type_t)(void*);
|
||||
typedef int (*il2cpp_type_get_type_t)(void*);
|
||||
typedef void* (*il2cpp_string_new_t)(const char*);
|
||||
typedef uint16_t*(*il2cpp_string_chars_t)(void*);
|
||||
typedef int (*il2cpp_string_length_t)(void*);
|
||||
|
||||
static il2cpp_domain_get_t p_domain_get;
|
||||
static il2cpp_thread_attach_t p_thread_attach;
|
||||
static il2cpp_domain_get_assemblies_t p_get_assemblies;
|
||||
static il2cpp_assembly_get_image_t p_get_image;
|
||||
static il2cpp_class_from_name_t p_class_from_name;
|
||||
static il2cpp_class_get_method_from_name_t p_get_method;
|
||||
static il2cpp_class_get_methods_t p_get_methods;
|
||||
static il2cpp_method_get_name_t p_method_name;
|
||||
static il2cpp_method_get_flags_t p_method_flags;
|
||||
static il2cpp_method_get_param_count_t p_param_count;
|
||||
static il2cpp_method_get_return_type_t p_return_type;
|
||||
static il2cpp_type_get_type_t p_type_kind;
|
||||
static il2cpp_string_new_t p_string_new;
|
||||
static il2cpp_string_chars_t p_string_chars;
|
||||
static il2cpp_string_length_t p_string_length;
|
||||
|
||||
static int g_gcr_static; // is GenerateChallengeResponse a static method?
|
||||
static BYTE backup_isready[32];
|
||||
static BYTE backup_gcr[32];
|
||||
|
||||
|
||||
// ---- base64 of a UTF-8 buffer ----
|
||||
static void base64(const unsigned char *in, size_t len, char *out)
|
||||
{
|
||||
static const char tbl[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
size_t o = 0;
|
||||
for (size_t i = 0; i < len; i += 3)
|
||||
{
|
||||
unsigned v = in[i] << 16;
|
||||
int n = 1;
|
||||
if (i + 1 < len) { v |= in[i + 1] << 8; n = 2; }
|
||||
if (i + 2 < len) { v |= in[i + 2]; n = 3; }
|
||||
out[o++] = tbl[(v >> 18) & 0x3F];
|
||||
out[o++] = tbl[(v >> 12) & 0x3F];
|
||||
out[o++] = (n >= 2) ? tbl[(v >> 6) & 0x3F] : '=';
|
||||
out[o++] = (n >= 3) ? tbl[v & 0x3F] : '=';
|
||||
}
|
||||
out[o] = 0;
|
||||
}
|
||||
|
||||
// UTF-16 (il2cpp string) -> UTF-8. Returns byte count written (excl NUL). BMP only; ample buffer assumed.
|
||||
static size_t utf16_to_utf8(const uint16_t *w, int wlen, unsigned char *out, size_t outcap)
|
||||
{
|
||||
size_t o = 0;
|
||||
for (int i = 0; i < wlen && o + 4 < outcap; i++)
|
||||
{
|
||||
uint32_t c = w[i];
|
||||
if (c < 0x80) out[o++] = (unsigned char)c;
|
||||
else if (c < 0x800)
|
||||
{
|
||||
out[o++] = (unsigned char)(0xC0 | (c >> 6));
|
||||
out[o++] = (unsigned char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
out[o++] = (unsigned char)(0xE0 | (c >> 12));
|
||||
out[o++] = (unsigned char)(0x80 | ((c >> 6) & 0x3F));
|
||||
out[o++] = (unsigned char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
out[o] = 0;
|
||||
return o;
|
||||
}
|
||||
|
||||
|
||||
// ---- hooks ----
|
||||
|
||||
// Readiness check: force true. Static 0-param bool -> native (RCX=MethodInfo*); return in AL.
|
||||
static int32_t IsReadyHook(void *methodInfo)
|
||||
{
|
||||
(void)methodInfo;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// GenerateChallengeResponse(string) -> base64(challenge). Register order is RCX,RDX,R8 regardless of
|
||||
// static-ness; for an instance method a=this,b=challenge,c=MethodInfo, for static a=challenge,b=MethodInfo.
|
||||
static void* GcrHook(void *a, void *b, void *c)
|
||||
{
|
||||
(void)c;
|
||||
void *challenge = g_gcr_static ? a : b;
|
||||
|
||||
unsigned char utf8[1024];
|
||||
const char *src;
|
||||
|
||||
if (challenge)
|
||||
{
|
||||
int len = p_string_length(challenge);
|
||||
if (len > 0 && len < 300)
|
||||
{
|
||||
uint16_t *w = p_string_chars(challenge);
|
||||
utf16_to_utf8(w, len, utf8, sizeof(utf8));
|
||||
src = (const char *)utf8;
|
||||
}
|
||||
else src = "nothing";
|
||||
}
|
||||
else src = "nothing";
|
||||
|
||||
char b64[1600];
|
||||
base64((const unsigned char *)src, strlen(src), b64);
|
||||
return p_string_new(b64);
|
||||
}
|
||||
|
||||
|
||||
static BOOL ResolveApi(HMODULE ga)
|
||||
{
|
||||
p_domain_get = (il2cpp_domain_get_t) GetProcAddress(ga, "il2cpp_domain_get");
|
||||
p_thread_attach = (il2cpp_thread_attach_t) GetProcAddress(ga, "il2cpp_thread_attach");
|
||||
p_get_assemblies = (il2cpp_domain_get_assemblies_t) GetProcAddress(ga, "il2cpp_domain_get_assemblies");
|
||||
p_get_image = (il2cpp_assembly_get_image_t) GetProcAddress(ga, "il2cpp_assembly_get_image");
|
||||
p_class_from_name= (il2cpp_class_from_name_t) GetProcAddress(ga, "il2cpp_class_from_name");
|
||||
p_get_method = (il2cpp_class_get_method_from_name_t) GetProcAddress(ga, "il2cpp_class_get_method_from_name");
|
||||
p_get_methods = (il2cpp_class_get_methods_t) GetProcAddress(ga, "il2cpp_class_get_methods");
|
||||
p_method_name = (il2cpp_method_get_name_t) GetProcAddress(ga, "il2cpp_method_get_name");
|
||||
p_method_flags = (il2cpp_method_get_flags_t) GetProcAddress(ga, "il2cpp_method_get_flags");
|
||||
p_param_count = (il2cpp_method_get_param_count_t) GetProcAddress(ga, "il2cpp_method_get_param_count");
|
||||
p_return_type = (il2cpp_method_get_return_type_t) GetProcAddress(ga, "il2cpp_method_get_return_type");
|
||||
p_type_kind = (il2cpp_type_get_type_t) GetProcAddress(ga, "il2cpp_type_get_type");
|
||||
p_string_new = (il2cpp_string_new_t) GetProcAddress(ga, "il2cpp_string_new");
|
||||
p_string_chars = (il2cpp_string_chars_t) GetProcAddress(ga, "il2cpp_string_chars");
|
||||
p_string_length = (il2cpp_string_length_t) GetProcAddress(ga, "il2cpp_string_length");
|
||||
|
||||
return p_domain_get && p_get_assemblies && p_get_image && p_class_from_name && p_get_method &&
|
||||
p_get_methods && p_method_name && p_method_flags && p_param_count && p_return_type &&
|
||||
p_type_kind && p_string_new && p_string_chars && p_string_length;
|
||||
}
|
||||
|
||||
static void* FindClass(void *domain, const char *ns, const char *name)
|
||||
{
|
||||
size_t n = 0;
|
||||
void **asms = p_get_assemblies(domain, &n);
|
||||
for (size_t i = 0; i < n; i++)
|
||||
{
|
||||
void *img = p_get_image(asms[i]);
|
||||
if (!img) continue;
|
||||
void *k = p_class_from_name(img, ns, name);
|
||||
if (k) return k;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void PatchEAC(void)
|
||||
{
|
||||
HMODULE ga = NULL;
|
||||
while (!ga) { ga = GetModuleHandleA("GameAssembly.dll"); if (!ga) Sleep(100); }
|
||||
|
||||
if (!ResolveApi(ga)) { Log("[EAC] missing il2cpp exports -- aborting"); return; }
|
||||
|
||||
void *domain = NULL;
|
||||
for (int i = 0; i < 600 && !domain; i++) { domain = p_domain_get(); if (!domain) Sleep(100); }
|
||||
if (!domain) { Log("[EAC] il2cpp domain never came up"); return; }
|
||||
if (p_thread_attach) p_thread_attach(domain);
|
||||
|
||||
void *cls = NULL;
|
||||
for (int i = 0; i < 100 && !cls; i++) { cls = FindClass(domain, "RecRoom.AntiCheat", "EACManager"); if (!cls) Sleep(100); }
|
||||
if (!cls) { Log("[EAC] RecRoom.AntiCheat.EACManager not found"); return; }
|
||||
|
||||
//
|
||||
// Readiness check: the sole static, 0-param, bool, non-property-getter method.
|
||||
//
|
||||
void *isReady = NULL;
|
||||
int readyCandidates = 0;
|
||||
void *iter = NULL, *m;
|
||||
while ((m = p_get_methods(cls, &iter)) != NULL)
|
||||
{
|
||||
uint32_t iflags = 0;
|
||||
uint32_t f = p_method_flags(m, &iflags);
|
||||
if (!(f & METHOD_ATTRIBUTE_STATIC)) continue;
|
||||
if (f & METHOD_ATTRIBUTE_SPECIAL_NAME) continue; // exclude property getters
|
||||
if (p_param_count(m) != 0) continue;
|
||||
void *rt = p_return_type(m);
|
||||
if (!rt || p_type_kind(rt) != IL2CPP_TYPE_BOOLEAN) continue;
|
||||
|
||||
const char *mn = p_method_name(m);
|
||||
Log("[EAC] readiness candidate: %s", mn ? mn : "?");
|
||||
isReady = m;
|
||||
readyCandidates++;
|
||||
}
|
||||
|
||||
if (!isReady)
|
||||
Log("[EAC] no static bool() readiness method -- readiness NOT forced");
|
||||
else
|
||||
{
|
||||
if (readyCandidates > 1)
|
||||
Log("[EAC] WARNING %d readiness candidates; using the last", readyCandidates);
|
||||
void *code = *(void **)isReady;
|
||||
if (code && InstallDetour(code, IsReadyHook, backup_isready, NULL))
|
||||
Log("[EAC] readiness check forced true");
|
||||
else
|
||||
Log("[EAC] failed to hook readiness check");
|
||||
}
|
||||
|
||||
//
|
||||
// GenerateChallengeResponse(string) -> base64(challenge).
|
||||
//
|
||||
void *gcr = p_get_method(cls, "GenerateChallengeResponse", 1);
|
||||
if (!gcr)
|
||||
Log("[EAC] GenerateChallengeResponse(argc=1) not found -- challenge NOT patched");
|
||||
else
|
||||
{
|
||||
uint32_t iflags = 0;
|
||||
uint32_t f = p_method_flags(gcr, &iflags);
|
||||
g_gcr_static = (f & METHOD_ATTRIBUTE_STATIC) != 0;
|
||||
void *code = *(void **)gcr;
|
||||
if (code && InstallDetour(code, GcrHook, backup_gcr, NULL))
|
||||
Log("[EAC] GenerateChallengeResponse -> base64(challenge) (static=%d)", g_gcr_static);
|
||||
else
|
||||
Log("[EAC] failed to hook GenerateChallengeResponse");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
#include "common.h"
|
||||
#include "http_rewrite.h"
|
||||
#include "logger.h"
|
||||
#include "config.h"
|
||||
#include "strings.h"
|
||||
#include "detour.h"
|
||||
|
||||
//
|
||||
// HTTP-layer host rewrite.
|
||||
//
|
||||
// DNS-name rewrite alone can't make a request "belong" to ns.recflare.net: the client keeps the
|
||||
// original URL, so TLS SNI and the Host header still say ns.rec.net. recflare serves ns.recflare.net
|
||||
// (its own vhost/cert), so the request must carry that host end-to-end. We do what the managed
|
||||
// SendRequestPatch did: hook the concrete, static BestHTTP.HTTPManager.SendRequest(HTTPRequest),
|
||||
// read request.Uri's absolute URL, swap the host, and set a fresh Uri back before the send proceeds.
|
||||
//
|
||||
// This is a call-through hook (we must let the real SendRequest run), so it relies on the
|
||||
// length-aware trampoline in detour.c.
|
||||
|
||||
typedef void* (*il2cpp_domain_get_t)(void);
|
||||
typedef int (*il2cpp_thread_attach_t)(void*);
|
||||
typedef void** (*il2cpp_domain_get_assemblies_t)(void*, size_t*);
|
||||
typedef void* (*il2cpp_assembly_get_image_t)(void*);
|
||||
typedef void* (*il2cpp_class_from_name_t)(void*, const char*, const char*);
|
||||
typedef void* (*il2cpp_class_get_method_from_name_t)(void*, const char*, int);
|
||||
typedef void* (*il2cpp_runtime_invoke_t)(void* method, void* obj, void** params, void** exc);
|
||||
typedef void* (*il2cpp_object_new_t)(void* klass);
|
||||
typedef void* (*il2cpp_string_new_t)(const char* str);
|
||||
typedef uint16_t* (*il2cpp_string_chars_t)(void* str);
|
||||
typedef int (*il2cpp_string_length_t)(void* str);
|
||||
|
||||
static il2cpp_domain_get_t p_domain_get;
|
||||
static il2cpp_thread_attach_t p_thread_attach;
|
||||
static il2cpp_domain_get_assemblies_t p_domain_get_assemblies;
|
||||
static il2cpp_assembly_get_image_t p_assembly_get_image;
|
||||
static il2cpp_class_from_name_t p_class_from_name;
|
||||
static il2cpp_class_get_method_from_name_t p_get_method;
|
||||
static il2cpp_runtime_invoke_t p_runtime_invoke;
|
||||
static il2cpp_object_new_t p_object_new;
|
||||
static il2cpp_string_new_t p_string_new;
|
||||
static il2cpp_string_chars_t p_string_chars;
|
||||
static il2cpp_string_length_t p_string_length;
|
||||
|
||||
// Resolved il2cpp targets.
|
||||
static void *cls_Uri;
|
||||
static void *m_get_Uri; // HTTPRequest.get_Uri() -> Uri
|
||||
static void *m_set_Uri; // HTTPRequest.set_Uri(Uri)
|
||||
static void *m_get_AbsoluteUri; // Uri.get_AbsoluteUri() -> string
|
||||
static void *m_Uri_ctor; // Uri..ctor(string)
|
||||
|
||||
// Trampoline to the real SendRequest(HTTPRequest req, MethodInfo* method) -> HTTPRequest*.
|
||||
typedef void* (*SendRequest_t)(void *req, void *method);
|
||||
static SendRequest_t original_SendRequest;
|
||||
static BYTE backup_sendrequest[32];
|
||||
|
||||
|
||||
static void* FindClass(void *domain, const char *ns, const char *name)
|
||||
{
|
||||
size_t count = 0;
|
||||
void **assemblies = p_domain_get_assemblies(domain, &count);
|
||||
for (size_t i = 0; i < count; i++)
|
||||
{
|
||||
void *image = p_assembly_get_image(assemblies[i]);
|
||||
if (!image) continue;
|
||||
void *k = p_class_from_name(image, ns, name);
|
||||
if (k) return k;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//
|
||||
// Rewrite the host inside an absolute URL using the configured exact-match pairs.
|
||||
// e.g. "https://ns.rec.net/api/1" -> "https://ns.recflare.net/api/1". Returns 1 if changed.
|
||||
//
|
||||
static int RewriteUrlHost(const char *url, char *out, size_t outlen)
|
||||
{
|
||||
// Find "://"
|
||||
const char *p = strstr(url, "://");
|
||||
if (!p) return 0;
|
||||
p += 3;
|
||||
|
||||
// Host runs until '/', ':', or end.
|
||||
const char *hostEnd = p;
|
||||
while (*hostEnd && *hostEnd != '/' && *hostEnd != ':') hostEnd++;
|
||||
|
||||
size_t hostLen = (size_t)(hostEnd - p);
|
||||
if (hostLen == 0 || hostLen >= 256) return 0;
|
||||
|
||||
char host[256];
|
||||
memcpy(host, p, hostLen);
|
||||
host[hostLen] = 0;
|
||||
|
||||
char newHost[256];
|
||||
if (!RewriteHost(host, newHost, sizeof(newHost)))
|
||||
return 0; // host not in the rewrite list
|
||||
|
||||
// Reassemble: [scheme://][newHost][rest]
|
||||
size_t prefixLen = (size_t)(p - url); // through "://"
|
||||
size_t newHostLen = strlen(newHost);
|
||||
size_t restLen = strlen(hostEnd);
|
||||
if (prefixLen + newHostLen + restLen + 1 > outlen) return 0;
|
||||
|
||||
memcpy(out, url, prefixLen);
|
||||
memcpy(out + prefixLen, newHost, newHostLen);
|
||||
memcpy(out + prefixLen + newHostLen, hostEnd, restLen + 1); // include NUL
|
||||
return 1;
|
||||
}
|
||||
|
||||
//
|
||||
// Our replacement for the static SendRequest(HTTPRequest). Rewrites req.Uri then forwards.
|
||||
//
|
||||
static void* SendRequestHook(void *req, void *method)
|
||||
{
|
||||
if (!req)
|
||||
return original_SendRequest(req, method);
|
||||
|
||||
void *exc = NULL;
|
||||
|
||||
// Uri uri = req.get_Uri();
|
||||
void *uri = p_runtime_invoke(m_get_Uri, req, NULL, &exc);
|
||||
if (!uri || exc)
|
||||
return original_SendRequest(req, method);
|
||||
|
||||
// string url = uri.get_AbsoluteUri();
|
||||
void *urlStr = p_runtime_invoke(m_get_AbsoluteUri, uri, NULL, &exc);
|
||||
if (!urlStr || exc)
|
||||
return original_SendRequest(req, method);
|
||||
|
||||
// Copy the (ASCII) URL out of the il2cpp string.
|
||||
int len = p_string_length(urlStr);
|
||||
if (len <= 0 || len >= 1024)
|
||||
return original_SendRequest(req, method);
|
||||
|
||||
uint16_t *wchars = p_string_chars(urlStr);
|
||||
char url[1024];
|
||||
for (int i = 0; i < len; i++)
|
||||
url[i] = (wchars[i] < 0x80) ? (char)wchars[i] : '?';
|
||||
url[len] = 0;
|
||||
|
||||
char newUrl[1100];
|
||||
if (RewriteUrlHost(url, newUrl, sizeof(newUrl)))
|
||||
{
|
||||
// Uri newUri = new Uri(newUrl); req.set_Uri(newUri);
|
||||
void *newStr = p_string_new(newUrl);
|
||||
void *newUri = p_object_new(cls_Uri);
|
||||
void *ctorArgs[1] = { newStr };
|
||||
exc = NULL;
|
||||
p_runtime_invoke(m_Uri_ctor, newUri, ctorArgs, &exc);
|
||||
if (!exc)
|
||||
{
|
||||
void *setArgs[1] = { newUri };
|
||||
exc = NULL;
|
||||
p_runtime_invoke(m_set_Uri, req, setArgs, &exc);
|
||||
if (!exc)
|
||||
Log("[HTTP] %s -> %s", url, newUrl);
|
||||
else
|
||||
Log("[HTTP] set_Uri threw, left original");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("[HTTP] new Uri(%s) threw, left original", newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return original_SendRequest(req, method);
|
||||
}
|
||||
|
||||
static BOOL ResolveApi(HMODULE ga)
|
||||
{
|
||||
p_domain_get = (il2cpp_domain_get_t) GetProcAddress(ga, "il2cpp_domain_get");
|
||||
p_thread_attach = (il2cpp_thread_attach_t) GetProcAddress(ga, "il2cpp_thread_attach");
|
||||
p_domain_get_assemblies = (il2cpp_domain_get_assemblies_t) GetProcAddress(ga, "il2cpp_domain_get_assemblies");
|
||||
p_assembly_get_image = (il2cpp_assembly_get_image_t) GetProcAddress(ga, "il2cpp_assembly_get_image");
|
||||
p_class_from_name = (il2cpp_class_from_name_t) GetProcAddress(ga, "il2cpp_class_from_name");
|
||||
p_get_method = (il2cpp_class_get_method_from_name_t) GetProcAddress(ga, "il2cpp_class_get_method_from_name");
|
||||
p_runtime_invoke = (il2cpp_runtime_invoke_t) GetProcAddress(ga, "il2cpp_runtime_invoke");
|
||||
p_object_new = (il2cpp_object_new_t) GetProcAddress(ga, "il2cpp_object_new");
|
||||
p_string_new = (il2cpp_string_new_t) GetProcAddress(ga, "il2cpp_string_new");
|
||||
p_string_chars = (il2cpp_string_chars_t) GetProcAddress(ga, "il2cpp_string_chars");
|
||||
p_string_length = (il2cpp_string_length_t) GetProcAddress(ga, "il2cpp_string_length");
|
||||
|
||||
return p_domain_get && p_domain_get_assemblies && p_assembly_get_image && p_class_from_name &&
|
||||
p_get_method && p_runtime_invoke && p_object_new && p_string_new && p_string_chars &&
|
||||
p_string_length;
|
||||
}
|
||||
|
||||
void PatchHttpHostRewrite(void)
|
||||
{
|
||||
if (rewrite_count == 0)
|
||||
{
|
||||
Log("[HTTP] no rewrite pairs configured -- host rewrite disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
HMODULE ga = NULL;
|
||||
while (!ga) { ga = GetModuleHandleA("GameAssembly.dll"); if (!ga) Sleep(100); }
|
||||
|
||||
if (!ResolveApi(ga)) { Log("[HTTP] missing il2cpp exports -- aborting host rewrite"); return; }
|
||||
|
||||
void *domain = NULL;
|
||||
for (int i = 0; i < 600 && !domain; i++) { domain = p_domain_get(); if (!domain) Sleep(100); }
|
||||
if (!domain) { Log("[HTTP] il2cpp domain never came up"); return; }
|
||||
if (p_thread_attach) p_thread_attach(domain);
|
||||
|
||||
// Resolve classes (retry through early init).
|
||||
void *cls_Manager = NULL, *cls_Request = NULL;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
if (!cls_Manager) cls_Manager = FindClass(domain, "BestHTTP", "HTTPManager");
|
||||
if (!cls_Request) cls_Request = FindClass(domain, "BestHTTP", "HTTPRequest");
|
||||
if (!cls_Uri) cls_Uri = FindClass(domain, "System", "Uri");
|
||||
if (cls_Manager && cls_Request && cls_Uri) break;
|
||||
Sleep(100);
|
||||
}
|
||||
if (!cls_Manager || !cls_Request || !cls_Uri)
|
||||
{
|
||||
Log("[HTTP] class resolve failed (mgr=%p req=%p uri=%p)", cls_Manager, cls_Request, cls_Uri);
|
||||
return;
|
||||
}
|
||||
|
||||
void *m_send = p_get_method(cls_Manager, "SendRequest", 1); // SendRequest(HTTPRequest)
|
||||
m_get_Uri = p_get_method(cls_Request, "get_Uri", 0);
|
||||
m_set_Uri = p_get_method(cls_Request, "set_Uri", 1);
|
||||
m_get_AbsoluteUri = p_get_method(cls_Uri, "get_AbsoluteUri", 0);
|
||||
m_Uri_ctor = p_get_method(cls_Uri, ".ctor", 1);
|
||||
|
||||
if (!m_send || !m_get_Uri || !m_set_Uri || !m_get_AbsoluteUri || !m_Uri_ctor)
|
||||
{
|
||||
Log("[HTTP] method resolve failed (send=%p getUri=%p setUri=%p absUri=%p ctor=%p)",
|
||||
m_send, m_get_Uri, m_set_Uri, m_get_AbsoluteUri, m_Uri_ctor);
|
||||
return;
|
||||
}
|
||||
|
||||
void *code = *(void **)m_send; // MethodInfo.methodPointer (compiled entry)
|
||||
Log("[HTTP] SendRequest MethodInfo=%p code=%p", m_send, code);
|
||||
if (!code) { Log("[HTTP] SendRequest has no compiled body"); return; }
|
||||
|
||||
if (InstallDetour(code, SendRequestHook, backup_sendrequest, (LPVOID*)&original_SendRequest))
|
||||
Log("[HTTP] host rewrite installed on SendRequest");
|
||||
else
|
||||
Log("[HTTP] SendRequest detour refused (prologue not relocatable) -- host rewrite NOT active");
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
#include "common.h"
|
||||
#include "memcheck_patch.h"
|
||||
#include "logger.h"
|
||||
#include "detour.h"
|
||||
|
||||
//
|
||||
// Native port of the managed MemoryIntegrityPatch (see the RecNetPlugin project). The client runs a
|
||||
// background native memory-integrity scan that hashes GameAssembly.dll code and compares against
|
||||
// baked-in hashes; our inline hooks change that memory, so the scan mismatches and boot dies with
|
||||
// "Launch validation failed. Is Rec Room installed correctly?". The scanner is identified NOT by its
|
||||
// obfuscated name (which rotates every build) but by its signature: a class holding both a
|
||||
// System.Threading.Thread and a System.Threading.CancellationTokenSource field (the background
|
||||
// scanner + its cancellation source). Its public, instance, parameterless, non-void method is the
|
||||
// scan-start entry point; the type it returns is the promise the boot step awaits. We detour that
|
||||
// entry (replace-only -- we never call the original) to instead return an already-resolved promise,
|
||||
// obtained from the promise type's static parameterless "Resolved" property getter. Boot then sees
|
||||
// an instantly-satisfied promise and proceeds.
|
||||
//
|
||||
// Everything here is resolved through the il2cpp reflection API at runtime; there are no hardcoded
|
||||
// obfuscated names. The first run logs generously so an ambiguous match can be diagnosed.
|
||||
|
||||
// --- il2cpp method attribute flags / type enum (stable il2cpp-api constants) ---
|
||||
#define METHOD_ATTRIBUTE_MEMBER_ACCESS_MASK 0x0007
|
||||
#define METHOD_ATTRIBUTE_PUBLIC 0x0006
|
||||
#define METHOD_ATTRIBUTE_STATIC 0x0010
|
||||
#define METHOD_ATTRIBUTE_SPECIAL_NAME 0x0800
|
||||
#define IL2CPP_TYPE_VOID 0x01
|
||||
|
||||
typedef void* (*il2cpp_domain_get_t)(void);
|
||||
typedef int (*il2cpp_thread_attach_t)(void*);
|
||||
typedef void** (*il2cpp_domain_get_assemblies_t)(void*, size_t*);
|
||||
typedef void* (*il2cpp_assembly_get_image_t)(void*);
|
||||
typedef const char* (*il2cpp_image_get_name_t)(void*);
|
||||
typedef size_t (*il2cpp_image_get_class_count_t)(void*);
|
||||
typedef void* (*il2cpp_image_get_class_t)(void*, size_t);
|
||||
typedef const char* (*il2cpp_class_get_name_t)(void*);
|
||||
typedef void* (*il2cpp_class_get_fields_t)(void*, void**);
|
||||
typedef void* (*il2cpp_field_get_type_t)(void*);
|
||||
typedef char* (*il2cpp_type_get_name_t)(void*);
|
||||
typedef void* (*il2cpp_class_get_methods_t)(void*, void**);
|
||||
typedef const char* (*il2cpp_method_get_name_t)(void*);
|
||||
typedef uint32_t (*il2cpp_method_get_flags_t)(void*, uint32_t*);
|
||||
typedef uint32_t (*il2cpp_method_get_param_count_t)(void*);
|
||||
typedef void* (*il2cpp_method_get_return_type_t)(void*);
|
||||
typedef int (*il2cpp_type_get_type_t)(void*);
|
||||
typedef void* (*il2cpp_class_from_type_t)(void*);
|
||||
typedef void* (*il2cpp_runtime_invoke_t)(void*, void*, void**, void**);
|
||||
typedef void (*il2cpp_free_t)(void*);
|
||||
|
||||
static il2cpp_domain_get_t p_domain_get;
|
||||
static il2cpp_thread_attach_t p_thread_attach;
|
||||
static il2cpp_domain_get_assemblies_t p_get_assemblies;
|
||||
static il2cpp_assembly_get_image_t p_get_image;
|
||||
static il2cpp_image_get_name_t p_image_name;
|
||||
static il2cpp_image_get_class_count_t p_class_count;
|
||||
static il2cpp_image_get_class_t p_get_class;
|
||||
static il2cpp_class_get_name_t p_class_name;
|
||||
static il2cpp_class_get_fields_t p_get_fields;
|
||||
static il2cpp_field_get_type_t p_field_type;
|
||||
static il2cpp_type_get_name_t p_type_name;
|
||||
static il2cpp_class_get_methods_t p_get_methods;
|
||||
static il2cpp_method_get_name_t p_method_name;
|
||||
static il2cpp_method_get_flags_t p_method_flags;
|
||||
static il2cpp_method_get_param_count_t p_param_count;
|
||||
static il2cpp_method_get_return_type_t p_return_type;
|
||||
static il2cpp_type_get_type_t p_type_kind;
|
||||
static il2cpp_class_from_type_t p_class_from_type;
|
||||
static il2cpp_runtime_invoke_t p_invoke;
|
||||
static il2cpp_free_t p_free;
|
||||
|
||||
static void *g_resolvedGetter; // MethodInfo* for the promise's static Resolved getter
|
||||
static BYTE backup_scan[32];
|
||||
|
||||
|
||||
static int ends_with(const char *s, const char *suf)
|
||||
{
|
||||
size_t ls = strlen(s), lf = strlen(suf);
|
||||
return ls >= lf && strcmp(s + (ls - lf), suf) == 0;
|
||||
}
|
||||
|
||||
static char *type_name_dup(void *type)
|
||||
{
|
||||
// il2cpp_type_get_name returns a heap string; copy into a small static-free buffer via strdup.
|
||||
char *n = p_type_name(type);
|
||||
if (!n) return NULL;
|
||||
char *copy = _strdup(n);
|
||||
if (p_free) p_free(n);
|
||||
return copy;
|
||||
}
|
||||
|
||||
// True if klass has a field whose type name equals `full` or ends with `.suffix`.
|
||||
static int class_has_field_type(void *klass, const char *full, const char *dotsuffix)
|
||||
{
|
||||
void *iter = NULL, *field;
|
||||
int found = 0;
|
||||
while ((field = p_get_fields(klass, &iter)) != NULL)
|
||||
{
|
||||
void *ft = p_field_type(field);
|
||||
if (!ft) continue;
|
||||
char *tn = type_name_dup(ft);
|
||||
if (!tn) continue;
|
||||
if (strcmp(tn, full) == 0 || ends_with(tn, dotsuffix)) found = 1;
|
||||
free(tn);
|
||||
if (found) break;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
static BOOL ResolveApi(HMODULE ga)
|
||||
{
|
||||
p_domain_get = (il2cpp_domain_get_t) GetProcAddress(ga, "il2cpp_domain_get");
|
||||
p_thread_attach = (il2cpp_thread_attach_t) GetProcAddress(ga, "il2cpp_thread_attach");
|
||||
p_get_assemblies = (il2cpp_domain_get_assemblies_t) GetProcAddress(ga, "il2cpp_domain_get_assemblies");
|
||||
p_get_image = (il2cpp_assembly_get_image_t) GetProcAddress(ga, "il2cpp_assembly_get_image");
|
||||
p_image_name = (il2cpp_image_get_name_t) GetProcAddress(ga, "il2cpp_image_get_name");
|
||||
p_class_count = (il2cpp_image_get_class_count_t) GetProcAddress(ga, "il2cpp_image_get_class_count");
|
||||
p_get_class = (il2cpp_image_get_class_t) GetProcAddress(ga, "il2cpp_image_get_class");
|
||||
p_class_name = (il2cpp_class_get_name_t) GetProcAddress(ga, "il2cpp_class_get_name");
|
||||
p_get_fields = (il2cpp_class_get_fields_t) GetProcAddress(ga, "il2cpp_class_get_fields");
|
||||
p_field_type = (il2cpp_field_get_type_t) GetProcAddress(ga, "il2cpp_field_get_type");
|
||||
p_type_name = (il2cpp_type_get_name_t) GetProcAddress(ga, "il2cpp_type_get_name");
|
||||
p_get_methods = (il2cpp_class_get_methods_t) GetProcAddress(ga, "il2cpp_class_get_methods");
|
||||
p_method_name = (il2cpp_method_get_name_t) GetProcAddress(ga, "il2cpp_method_get_name");
|
||||
p_method_flags = (il2cpp_method_get_flags_t) GetProcAddress(ga, "il2cpp_method_get_flags");
|
||||
p_param_count = (il2cpp_method_get_param_count_t)GetProcAddress(ga, "il2cpp_method_get_param_count");
|
||||
p_return_type = (il2cpp_method_get_return_type_t)GetProcAddress(ga, "il2cpp_method_get_return_type");
|
||||
p_type_kind = (il2cpp_type_get_type_t) GetProcAddress(ga, "il2cpp_type_get_type");
|
||||
p_class_from_type = (il2cpp_class_from_type_t) GetProcAddress(ga, "il2cpp_class_from_type");
|
||||
p_invoke = (il2cpp_runtime_invoke_t) GetProcAddress(ga, "il2cpp_runtime_invoke");
|
||||
p_free = (il2cpp_free_t) GetProcAddress(ga, "il2cpp_free");
|
||||
|
||||
return p_domain_get && p_get_assemblies && p_get_image && p_image_name && p_class_count &&
|
||||
p_get_class && p_get_fields && p_field_type && p_type_name && p_get_methods &&
|
||||
p_method_name && p_method_flags && p_param_count && p_return_type && p_type_kind &&
|
||||
p_class_from_type && p_invoke;
|
||||
}
|
||||
|
||||
static void* FindImage(void *domain, const char *wantName)
|
||||
{
|
||||
size_t n = 0;
|
||||
void **asms = p_get_assemblies(domain, &n);
|
||||
for (size_t i = 0; i < n; i++)
|
||||
{
|
||||
void *img = p_get_image(asms[i]);
|
||||
if (!img) continue;
|
||||
const char *nm = p_image_name(img);
|
||||
if (nm && strcmp(nm, wantName) == 0) return img;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//
|
||||
// The detour: return a freshly-resolved promise instead of running the scan. Instance method ABI is
|
||||
// (RCX=this, RDX=MethodInfo*); we ignore both. Re-invoking the getter each call avoids holding a GC
|
||||
// reference. If the getter ever throws/returns null we return null -- the managed patch's fallback
|
||||
// was to let the original run, but by the time we're detoured that's not an option, so null it is
|
||||
// (boot's .Then on a null promise is still better than a guaranteed hash-mismatch rejection).
|
||||
//
|
||||
static void* ScanHook(void *self, void *methodInfo)
|
||||
{
|
||||
(void)self; (void)methodInfo;
|
||||
if (!g_resolvedGetter) return NULL;
|
||||
void *exc = NULL;
|
||||
return p_invoke(g_resolvedGetter, NULL, NULL, &exc);
|
||||
}
|
||||
|
||||
void PatchMemoryIntegrityCheck(void)
|
||||
{
|
||||
HMODULE ga = NULL;
|
||||
while (!ga) { ga = GetModuleHandleA("GameAssembly.dll"); if (!ga) Sleep(100); }
|
||||
|
||||
if (!ResolveApi(ga)) { Log("[MEMCHECK] missing il2cpp exports -- aborting"); return; }
|
||||
|
||||
void *domain = NULL;
|
||||
for (int i = 0; i < 600 && !domain; i++) { domain = p_domain_get(); if (!domain) Sleep(100); }
|
||||
if (!domain) { Log("[MEMCHECK] il2cpp domain never came up"); return; }
|
||||
if (p_thread_attach) p_thread_attach(domain);
|
||||
|
||||
// Assembly-CSharp holds the scanner. Retry through early init.
|
||||
void *img = NULL;
|
||||
for (int i = 0; i < 100 && !img; i++) { img = FindImage(domain, "Assembly-CSharp.dll"); if (!img) Sleep(100); }
|
||||
if (!img) { Log("[MEMCHECK] Assembly-CSharp.dll image not found"); return; }
|
||||
|
||||
//
|
||||
// Find the scanner class: has BOTH a Thread field and a CancellationTokenSource field.
|
||||
//
|
||||
size_t ccount = p_class_count(img);
|
||||
void *scanner = NULL;
|
||||
|
||||
for (size_t i = 0; i < ccount; i++)
|
||||
{
|
||||
void *c = p_get_class(img, i);
|
||||
if (!c) continue;
|
||||
|
||||
if (class_has_field_type(c, "System.Threading.Thread", ".Thread") &&
|
||||
class_has_field_type(c, "System.Threading.CancellationTokenSource", ".CancellationTokenSource"))
|
||||
{
|
||||
const char *cn = p_class_name(c);
|
||||
Log("[MEMCHECK] scanner candidate: %s", cn ? cn : "?");
|
||||
scanner = c; // keep last; log all so ambiguity is visible
|
||||
}
|
||||
}
|
||||
|
||||
if (!scanner)
|
||||
{
|
||||
Log("[MEMCHECK] no class with Thread+CancellationTokenSource found -- scanner not identified");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Scan-start method: public, instance, 0-param, non-void. Log every candidate; pick the sole one.
|
||||
//
|
||||
void *scanMethod = NULL;
|
||||
void *promiseClass = NULL;
|
||||
int candidates = 0;
|
||||
|
||||
void *iter = NULL, *m;
|
||||
while ((m = p_get_methods(scanner, &iter)) != NULL)
|
||||
{
|
||||
uint32_t iflags = 0;
|
||||
uint32_t f = p_method_flags(m, &iflags);
|
||||
if (f & METHOD_ATTRIBUTE_STATIC) continue;
|
||||
if ((f & METHOD_ATTRIBUTE_MEMBER_ACCESS_MASK) != METHOD_ATTRIBUTE_PUBLIC) continue;
|
||||
if (p_param_count(m) != 0) continue;
|
||||
|
||||
void *rt = p_return_type(m);
|
||||
if (!rt || p_type_kind(rt) == IL2CPP_TYPE_VOID) continue;
|
||||
|
||||
const char *mn = p_method_name(m);
|
||||
char *rtn = type_name_dup(rt);
|
||||
Log("[MEMCHECK] scan-start candidate: %s() -> %s", mn ? mn : "?", rtn ? rtn : "?");
|
||||
if (rtn) free(rtn);
|
||||
|
||||
scanMethod = m;
|
||||
promiseClass = p_class_from_type(rt);
|
||||
candidates++;
|
||||
}
|
||||
|
||||
if (!scanMethod)
|
||||
{
|
||||
Log("[MEMCHECK] no public instance 0-param non-void method on scanner -- cannot hook");
|
||||
return;
|
||||
}
|
||||
if (candidates > 1)
|
||||
Log("[MEMCHECK] WARNING %d scan-start candidates; using the last -- may be wrong", candidates);
|
||||
|
||||
//
|
||||
// Resolved-promise getter: any image, static, special-name (property getter), 0-param, returns
|
||||
// the promise class, name not ending _k__BackingField. (Managed found exactly one.)
|
||||
//
|
||||
size_t na = 0;
|
||||
void **asms = p_get_assemblies(domain, &na);
|
||||
int getters = 0;
|
||||
|
||||
for (size_t ai = 0; ai < na && getters < 1; ai++)
|
||||
{
|
||||
void *im = p_get_image(asms[ai]);
|
||||
if (!im) continue;
|
||||
size_t cc = p_class_count(im);
|
||||
for (size_t ci = 0; ci < cc && getters < 1; ci++)
|
||||
{
|
||||
void *c = p_get_class(im, ci);
|
||||
if (!c) continue;
|
||||
void *it = NULL, *mm;
|
||||
while ((mm = p_get_methods(c, &it)) != NULL)
|
||||
{
|
||||
uint32_t iflags = 0;
|
||||
uint32_t f = p_method_flags(mm, &iflags);
|
||||
if (!(f & METHOD_ATTRIBUTE_STATIC)) continue;
|
||||
if (!(f & METHOD_ATTRIBUTE_SPECIAL_NAME)) continue;
|
||||
if (p_param_count(mm) != 0) continue;
|
||||
|
||||
void *rt = p_return_type(mm);
|
||||
if (!rt || p_class_from_type(rt) != promiseClass) continue;
|
||||
|
||||
const char *mn = p_method_name(mm);
|
||||
if (mn && ends_with(mn, "_k__BackingField")) continue;
|
||||
|
||||
Log("[MEMCHECK] resolved-promise getter: %s.%s", p_class_name(c), mn ? mn : "?");
|
||||
g_resolvedGetter = mm;
|
||||
getters++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_resolvedGetter)
|
||||
{
|
||||
Log("[MEMCHECK] no static Resolved getter returning the promise type -- cannot build a resolved promise");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanity: make sure invoking the getter yields a non-null object before we commit the detour.
|
||||
void *exc = NULL;
|
||||
void *test = p_invoke(g_resolvedGetter, NULL, NULL, &exc);
|
||||
if (!test || exc)
|
||||
{
|
||||
Log("[MEMCHECK] Resolved getter returned null/threw -- not hooking (would hand boot a null promise)");
|
||||
return;
|
||||
}
|
||||
|
||||
void *code = *(void **)scanMethod; // MethodInfo.methodPointer
|
||||
Log("[MEMCHECK] scan-start MethodInfo=%p code=%p", scanMethod, code);
|
||||
if (!code) { Log("[MEMCHECK] scan-start has no compiled body"); return; }
|
||||
|
||||
// Replace-only (we never call the original), so a blind 14-byte overwrite is safe.
|
||||
if (InstallDetour(code, ScanHook, backup_scan, NULL))
|
||||
Log("[MEMCHECK] native memory integrity scan skipped (scan-start -> resolved promise)");
|
||||
else
|
||||
Log("[MEMCHECK] failed to install scan-start detour");
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "module_watch.h"
|
||||
|
||||
#include "logger.h"
|
||||
#include "process.h"
|
||||
|
||||
|
||||
|
||||
void WatchModules()
|
||||
{
|
||||
Log(
|
||||
"[MODULE WATCH] Started"
|
||||
);
|
||||
|
||||
|
||||
// One full dump for reference, then only announce the two modules we care about as they appear
|
||||
// and stop -- the old every-10s full dump buried the actual hook diagnostics.
|
||||
DumpLoadedModules();
|
||||
|
||||
|
||||
BOOL sawGame = FALSE;
|
||||
BOOL sawUnity = FALSE;
|
||||
|
||||
|
||||
while(!sawGame || !sawUnity)
|
||||
{
|
||||
if(!sawGame && GetModuleHandleA("GameAssembly.dll"))
|
||||
{
|
||||
sawGame = TRUE;
|
||||
Log("[MODULE WATCH] GameAssembly.dll loaded");
|
||||
}
|
||||
|
||||
if(!sawUnity && GetModuleHandleA("UnityPlayer.dll"))
|
||||
{
|
||||
sawUnity = TRUE;
|
||||
Log("[MODULE WATCH] UnityPlayer.dll loaded");
|
||||
}
|
||||
|
||||
Sleep(500);
|
||||
}
|
||||
|
||||
|
||||
Log(
|
||||
"[MODULE WATCH] game modules present, watcher done"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
#include "common.h"
|
||||
#include "ssl_patch.h"
|
||||
#include "logger.h"
|
||||
#include "detour.h"
|
||||
|
||||
//
|
||||
// Native TLS pinning bypass.
|
||||
//
|
||||
// Redirecting DNS to a self-hosted server means the TLS handshake presents a certificate the
|
||||
// client's pinning will reject, so the connection dies before any HTTP is sent. The managed build
|
||||
// solved this by Harmony-patching the CONCRETE BouncyCastle class
|
||||
// Org.BouncyCastle.Crypto.Tls.LegacyTlsAuthentication.NotifyServerCertificate (see CLAUDE.md
|
||||
// gotcha 3 -- the interface method never dispatches, you must hit the concrete impl). We do the
|
||||
// same here, natively, by detouring that method's compiled il2cpp body to a no-op that returns.
|
||||
//
|
||||
// "NotifyServerCertificate" is an unobfuscated framework name, stable across Rec Room builds --
|
||||
// this is why the managed build targeted it and why we can resolve it by literal name here.
|
||||
//
|
||||
// We never call the original, so the trampoline InstallDetour builds is never executed -- the
|
||||
// blind 14-byte copy that would mis-handle a rip-relative prologue is therefore harmless for this
|
||||
// target (we only care that the 14-byte jmp overwrite at the entry is valid, which it always is).
|
||||
|
||||
typedef void* (*il2cpp_domain_get_t)(void);
|
||||
typedef int (*il2cpp_thread_attach_t)(void* domain);
|
||||
typedef void** (*il2cpp_domain_get_assemblies_t)(void* domain, size_t* size);
|
||||
typedef void* (*il2cpp_assembly_get_image_t)(void* assembly);
|
||||
typedef void* (*il2cpp_class_from_name_t)(void* image, const char* ns, const char* name);
|
||||
typedef void* (*il2cpp_class_get_method_from_name_t)(void* klass, const char* name, int argc);
|
||||
|
||||
static il2cpp_domain_get_t p_domain_get;
|
||||
static il2cpp_thread_attach_t p_thread_attach;
|
||||
static il2cpp_domain_get_assemblies_t p_domain_get_assemblies;
|
||||
static il2cpp_assembly_get_image_t p_assembly_get_image;
|
||||
static il2cpp_class_from_name_t p_class_from_name;
|
||||
static il2cpp_class_get_method_from_name_t p_class_get_method_from_name;
|
||||
|
||||
static BYTE backup_notify[14];
|
||||
void *original_notify = NULL; // unused (we never call through); kept for symmetry/logging
|
||||
|
||||
//
|
||||
// Replacement for LegacyTlsAuthentication.NotifyServerCertificate(this, cert, MethodInfo*).
|
||||
// il2cpp passes args in the standard x64 convention (RCX=this, RDX=cert, R8=MethodInfo*) and the
|
||||
// method returns void, so simply returning accepts every server certificate. Because the entry was
|
||||
// reached via jmp (not call), our return goes straight back to the game's caller.
|
||||
//
|
||||
static void ReplNotifyServerCertificate(void *thisptr, void *cert, void *method)
|
||||
{
|
||||
(void)thisptr; (void)cert; (void)method;
|
||||
// Accept unconditionally -- no pinning, no validation.
|
||||
}
|
||||
|
||||
//
|
||||
// Resolve a class by namespace+name across every loaded il2cpp assembly. il2cpp_class_from_name
|
||||
// only searches the image it's given, so we sweep them (we don't hard-code which assembly the type
|
||||
// lives in -- it's RecNet.Runtime today, but that's incidental).
|
||||
//
|
||||
static void* FindClass(void *domain, const char *ns, const char *name)
|
||||
{
|
||||
size_t count = 0;
|
||||
void **assemblies = p_domain_get_assemblies(domain, &count);
|
||||
if (!assemblies || count == 0)
|
||||
return NULL;
|
||||
|
||||
for (size_t i = 0; i < count; i++)
|
||||
{
|
||||
void *image = p_assembly_get_image(assemblies[i]);
|
||||
if (!image) continue;
|
||||
|
||||
void *klass = p_class_from_name(image, ns, name);
|
||||
if (klass) return klass;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static BOOL ResolveIl2CppApi(HMODULE ga)
|
||||
{
|
||||
p_domain_get = (il2cpp_domain_get_t) GetProcAddress(ga, "il2cpp_domain_get");
|
||||
p_thread_attach = (il2cpp_thread_attach_t) GetProcAddress(ga, "il2cpp_thread_attach");
|
||||
p_domain_get_assemblies = (il2cpp_domain_get_assemblies_t) GetProcAddress(ga, "il2cpp_domain_get_assemblies");
|
||||
p_assembly_get_image = (il2cpp_assembly_get_image_t) GetProcAddress(ga, "il2cpp_assembly_get_image");
|
||||
p_class_from_name = (il2cpp_class_from_name_t) GetProcAddress(ga, "il2cpp_class_from_name");
|
||||
p_class_get_method_from_name = (il2cpp_class_get_method_from_name_t)GetProcAddress(ga, "il2cpp_class_get_method_from_name");
|
||||
|
||||
return p_domain_get && p_domain_get_assemblies && p_assembly_get_image &&
|
||||
p_class_from_name && p_class_get_method_from_name;
|
||||
}
|
||||
|
||||
void PatchBestHTTPSSL(void)
|
||||
{
|
||||
//
|
||||
// Wait for GameAssembly.dll to be mapped.
|
||||
//
|
||||
HMODULE ga = NULL;
|
||||
while (!ga)
|
||||
{
|
||||
ga = GetModuleHandleA("GameAssembly.dll");
|
||||
if (!ga) Sleep(100);
|
||||
}
|
||||
Log("[SSL] GameAssembly.dll at %p", ga);
|
||||
|
||||
if (!ResolveIl2CppApi(ga))
|
||||
{
|
||||
Log("[SSL] missing il2cpp exports -- aborting TLS patch");
|
||||
return;
|
||||
}
|
||||
Log("[SSL] il2cpp API resolved");
|
||||
|
||||
//
|
||||
// Wait for the il2cpp runtime to finish init: il2cpp_domain_get() returns NULL until then.
|
||||
//
|
||||
void *domain = NULL;
|
||||
for (int i = 0; i < 600 && !domain; i++) // up to ~60s
|
||||
{
|
||||
domain = p_domain_get();
|
||||
if (!domain) Sleep(100);
|
||||
}
|
||||
if (!domain)
|
||||
{
|
||||
Log("[SSL] il2cpp domain never came up -- aborting TLS patch");
|
||||
return;
|
||||
}
|
||||
|
||||
// Our thread is native; attach it so il2cpp metadata calls are safe.
|
||||
if (p_thread_attach) p_thread_attach(domain);
|
||||
|
||||
//
|
||||
// Resolve the concrete class + method. Metadata is present immediately after init, but classes
|
||||
// can briefly not resolve during early init, so retry a few times.
|
||||
//
|
||||
void *klass = NULL;
|
||||
for (int i = 0; i < 100 && !klass; i++) // up to ~10s
|
||||
{
|
||||
klass = FindClass(domain, "Org.BouncyCastle.Crypto.Tls", "LegacyTlsAuthentication");
|
||||
if (!klass) Sleep(100);
|
||||
}
|
||||
if (!klass)
|
||||
{
|
||||
Log("[SSL] LegacyTlsAuthentication not found -- TLS pinning NOT bypassed");
|
||||
return;
|
||||
}
|
||||
Log("[SSL] LegacyTlsAuthentication klass=%p", klass);
|
||||
|
||||
// argc counts only declared params: NotifyServerCertificate(Certificate) -> 1.
|
||||
void *method = p_class_get_method_from_name(klass, "NotifyServerCertificate", 1);
|
||||
if (!method)
|
||||
{
|
||||
Log("[SSL] NotifyServerCertificate(argc=1) not found -- TLS pinning NOT bypassed");
|
||||
return;
|
||||
}
|
||||
|
||||
// MethodInfo.methodPointer is the first field of the struct: the compiled native entry.
|
||||
void *code = *(void **)method;
|
||||
Log("[SSL] NotifyServerCertificate MethodInfo=%p code=%p", method, code);
|
||||
|
||||
if (!code)
|
||||
{
|
||||
Log("[SSL] method has no compiled body -- aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace-only hook: we never call the original, so pass NULL trampoline -- InstallDetour then
|
||||
// does a plain 14-byte overwrite and won't refuse a complex il2cpp prologue.
|
||||
if (InstallDetour(code, ReplNotifyServerCertificate, backup_notify, NULL))
|
||||
Log("[SSL] TLS pinning bypassed (NotifyServerCertificate -> accept-all)");
|
||||
else
|
||||
Log("[SSL] failed to install NotifyServerCertificate detour");
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "strings.h"
|
||||
#include "config.h"
|
||||
#include "logger.h"
|
||||
|
||||
|
||||
|
||||
int ShouldRedirect(const char *host)
|
||||
{
|
||||
if(!host)
|
||||
return 0;
|
||||
|
||||
|
||||
Log(
|
||||
"[CHECK] %s",
|
||||
host
|
||||
);
|
||||
|
||||
|
||||
for(
|
||||
int i = 0;
|
||||
i < redirect_count_config;
|
||||
i++
|
||||
)
|
||||
{
|
||||
//
|
||||
// Exact match
|
||||
//
|
||||
|
||||
if(
|
||||
_stricmp(
|
||||
host,
|
||||
redirect_domains[i]
|
||||
) == 0
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[MATCH] %s",
|
||||
host
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Subdomain match
|
||||
//
|
||||
|
||||
size_t len =
|
||||
strlen(
|
||||
redirect_domains[i]
|
||||
);
|
||||
|
||||
|
||||
size_t hostLen =
|
||||
strlen(
|
||||
host
|
||||
);
|
||||
|
||||
|
||||
if(
|
||||
hostLen > len &&
|
||||
host[hostLen - len - 1] == '.' &&
|
||||
_stricmp(
|
||||
host + (hostLen - len),
|
||||
redirect_domains[i]
|
||||
) == 0
|
||||
)
|
||||
{
|
||||
Log(
|
||||
"[SUBDOMAIN MATCH] %s",
|
||||
host
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
int RewriteHost(const char *host, char *out, size_t outlen)
|
||||
{
|
||||
if(!host || !out || outlen == 0)
|
||||
return 0;
|
||||
|
||||
|
||||
for(int i = 0; i < rewrite_count; i++)
|
||||
{
|
||||
//
|
||||
// Exact match only: whole host -> to
|
||||
//
|
||||
|
||||
if(_stricmp(host, rewrite_from[i]) == 0)
|
||||
{
|
||||
const char *to = rewrite_to[i];
|
||||
|
||||
if(strlen(to) + 1 > outlen)
|
||||
return 0;
|
||||
|
||||
strcpy_s(out, outlen, to);
|
||||
|
||||
Log("[REWRITE] %s -> %s", host, out);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user