mirror of
https://github.com/djdevin/recnet-plugin.git
synced 2026-09-08 14:41:30 -07:00
391 lines
18 KiB
C
391 lines
18 KiB
C
#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);
|
|
}
|
|
|
|
//
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Hardcoded-RVA path for build 2025-04-29 (recflare-client-unstable).
|
|
//
|
|
// GameAssembly.dll on this build has no export table, so ResolveApi() fails and the shape-based
|
|
// search above can't run at all -- the integrity scan has been completely UNPATCHED here, while we
|
|
// carry four inline .text detours. That makes it the prime suspect for the hard 0xC0000005 in the
|
|
// Themida-wrapped RecRoom.exe.dll ~35s in (see memory note unstable-build-identity-rvas.md).
|
|
//
|
|
// The scanner on this build is the static class `BLGELNMKAKM` in the Cpp2IL dump
|
|
// (RecRoom_Info/Code/2025-04-29_02-57-34) -- identified by shape, NOT by name (CLAUDE.md gotcha 7):
|
|
// it owns the const `"verification.sig"`, a 65536 chunk size, RSA modulus/exponent byte[] fields, and
|
|
// the `<CheckHashesInBackground>` compiler-generated closures. NOTE the obfuscated class name recorded
|
|
// for an older build (`CHPCJHMCKMA`) does NOT exist in this dump -- never reuse one across builds.
|
|
//
|
|
// BLGELNMKAKM.JEGANAFJCLA() RVA 0x133B720 public static, 0 params -> NCOKFFGPIJM<LOBLPLMBPEO>
|
|
//
|
|
// That is the promise-returning scan entry the boot step awaits (the same shape the export-based
|
|
// search looks for above). We start as a pure WITNESS: a call-through tracer that logs entry/exit and
|
|
// changes nothing, so we can first establish whether the scan even runs and whether it correlates
|
|
// with the crash -- returning a bogus promise here would risk the same null-deref crash the
|
|
// antitamper funnel hook caused. Only once that's confirmed should this become a neutralizer.
|
|
// ---------------------------------------------------------------------------------------------
|
|
//
|
|
#define MEMCHECK_SCAN_RVA 0x133B720
|
|
|
|
typedef void* (*scan_fn_t)(void *methodInfo);
|
|
static scan_fn_t real_scan_rva;
|
|
static BYTE backup_scan_rva[32];
|
|
|
|
// Static il2cpp method: MethodInfo* arrives in RCX, no declared params.
|
|
static void* ScanTraceHook(void *methodInfo)
|
|
{
|
|
Log("[MEMCHECK] *** integrity scan ENTERED (BLGELNMKAKM.JEGANAFJCLA) ***");
|
|
void *r = real_scan_rva(methodInfo);
|
|
Log("[MEMCHECK] *** integrity scan RETURNED promise=%p ***", r);
|
|
return r;
|
|
}
|
|
|
|
// Spin until the byte looks like decrypted code rather than a zero/int3 fill (the packer decrypts
|
|
// .text shortly after the module maps) -- same guard as ssl_patch.c.
|
|
static void WaitForCodeMc(const BYTE *p)
|
|
{
|
|
for (int i = 0; i < 600; i++)
|
|
{
|
|
BYTE b = p[0];
|
|
if (b != 0x00 && b != 0xCC) return;
|
|
Sleep(100);
|
|
}
|
|
}
|
|
|
|
static void PatchMemcheckByRVA(HMODULE ga)
|
|
{
|
|
(void)ga; (void)backup_scan_rva; (void)real_scan_rva;
|
|
(void)ScanTraceHook; (void)WaitForCodeMc;
|
|
|
|
//
|
|
// DISABLED -- MEMCHECK_SCAN_RVA IS WRONG FOR THIS BUILD. DO NOT RE-ENABLE AS-IS.
|
|
//
|
|
// 0x133B720 was read out of il2cpp-tools/out/dump.cs, which turned out to be a DIFFERENT BUILD
|
|
// than the installed client. Proof: that dump puts BestHTTP SendRequest at 0x3161AF0 and
|
|
// NotifyServerCertificate at 0x3F447C0, but the RVAs that actually work at runtime here are
|
|
// 0x71D7BE0 and 0x71CFD00. The correct dump for recflare-client-unstable is
|
|
// C:\Games\RecRoom_Info\Code\2025-04-29_02-57-34 (it lists SendRequest at 0x71D7BE0 -- match).
|
|
//
|
|
// Consequence: the byte at GA+0x133B720 is not a function entry on this build (observed prologue
|
|
// "DF C7 47 10" -- mid-instruction), so installing a detour there writes 14 bytes into the middle
|
|
// of unrelated code. The tracer never fired because nothing calls that address.
|
|
//
|
|
// Also: `CheckHashesInBackground` / `verification.sig` / class `BLGELNMKAKM` DO NOT EXIST in the
|
|
// correct dump, so the managed file-hash scanner those names came from is not present in this
|
|
// build at all. The integrity check that matters here is very likely NATIVE, inside the
|
|
// Themida-wrapped RecRoom.exe.dll -- which is exactly the module the fatal 0xC0000005 lands in.
|
|
// Re-deriving a scan entry from the CORRECT dump is the prerequisite for any RVA hook here.
|
|
//
|
|
Log("[MEMCHECK] no il2cpp exports and no verified scan RVA for this build -- not hooking "
|
|
"(see src/unity/memcheck_patch.c: out/dump.cs is the WRONG build)");
|
|
}
|
|
|
|
void PatchMemoryIntegrityCheck(void)
|
|
{
|
|
HMODULE ga = NULL;
|
|
while (!ga) { ga = GetModuleHandleA("GameAssembly.dll"); if (!ga) Sleep(100); }
|
|
|
|
if (!ResolveApi(ga)) { PatchMemcheckByRVA(ga); 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");
|
|
}
|