mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
1039 lines
38 KiB
C#
1039 lines
38 KiB
C#
using BepInEx;
|
|
using BepInEx.Logging;
|
|
using BepInEx.Unity.IL2CPP;
|
|
using HarmonyLib;
|
|
using RecRoom.AntiCheat;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
|
|
internal sealed class FixedSetting<T>
|
|
{
|
|
internal T Value { get; }
|
|
|
|
internal FixedSetting(T value) => Value = value;
|
|
}
|
|
|
|
[BepInPlugin("com.radicade.patch", "RadicadePatch", "3.0.0")]
|
|
public sealed class RadicadeEacBypassPlugin : BasePlugin
|
|
{
|
|
internal static ManualLogSource LogSource;
|
|
|
|
internal const string ServerAddress = "https://ns.radicade.app/";
|
|
internal const string ServerHost = "ns.radicade.app";
|
|
|
|
internal static FixedSetting<string> ServerUrl = new(ServerAddress);
|
|
internal static FixedSetting<string> TlsAllowedHosts = new(ServerHost);
|
|
internal static FixedSetting<bool> RealtimeLog = new(false);
|
|
internal static FixedSetting<bool> KeepsakeToolEnabled = new(true);
|
|
internal static Il2CppSystem.Threading.SynchronizationContext MainThreadContext;
|
|
internal static string ImageKeyId;
|
|
|
|
static string ReadMetadataKey(string path)
|
|
{
|
|
try
|
|
{
|
|
return File.Exists(path)
|
|
? ImageSignatureRequest.DetectKeyIdFromGameMetadata(File.ReadAllBytes(path))
|
|
: null;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LogSource.LogWarning($"[ImageSig] key id detection failed for {Path.GetFileName(path)}: {e.GetType().Name}: {e.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void LogFixedConnection()
|
|
{
|
|
Log.LogInfo($"[TLS] fixed server {ServerUrl.Value}; AllowedHosts = '{TlsAllowedHosts.Value}'");
|
|
if (!EmbeddedTlsPatches.IsHostAllowed(ServerHost))
|
|
Log.LogError($"[TLS] built-in AllowedHosts '{TlsAllowedHosts.Value}' does not cover '{ServerHost}' - the embedded TLS stack will reject the server certificate.");
|
|
}
|
|
|
|
void WarnIfMetadataPatchedForAnotherServer(Uri server)
|
|
{
|
|
try
|
|
{
|
|
var marker = Path.Combine(UnityEngine.Application.dataPath, "radicade-patch.json");
|
|
if (!File.Exists(marker))
|
|
{
|
|
Log.LogWarning($"[Meta] no {marker}: this install was never metadata-patched, so the client still resolves the official hosts.");
|
|
return;
|
|
}
|
|
|
|
var text = File.ReadAllText(marker);
|
|
var key = text.IndexOf("\"serverUrl\"", StringComparison.Ordinal);
|
|
var colon = key < 0 ? -1 : text.IndexOf(':', key);
|
|
var open = colon < 0 ? -1 : text.IndexOf('"', colon + 1);
|
|
var close = open < 0 ? -1 : text.IndexOf('"', open + 1);
|
|
if (close < 0)
|
|
{
|
|
Log.LogWarning($"[Meta] {marker} has no readable serverUrl field; cannot confirm the patched server matches {server}.");
|
|
return;
|
|
}
|
|
|
|
var patched = text[(open + 1)..close].TrimEnd('/');
|
|
var wanted = server.GetLeftPart(UriPartial.Authority).TrimEnd('/');
|
|
if (patched.Equals(wanted, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Log.LogInfo($"[Meta] metadata patched for {patched}; matches Connection:ServerUrl.");
|
|
return;
|
|
}
|
|
|
|
Log.LogWarning($"[Meta] this install's metadata was patched for '{patched}' but Connection:ServerUrl is '{wanted}'. "
|
|
+ "The client's own literals still point at the patched address, so requests will not reach the configured server. "
|
|
+ $"Re-patch from the pristine copy: python tools/patch-0831-client-metadata.py <game dir> --server {wanted} --apply");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.LogWarning($"[Meta] metadata marker check failed: {e.GetType().Name}: {e.Message}");
|
|
}
|
|
}
|
|
|
|
public override void Load()
|
|
{
|
|
LogSource = Log;
|
|
if (UnityEngine.Application.version != "20230831")
|
|
throw new NotSupportedException("This plugin requires Rec Room 20230831.");
|
|
|
|
MainThreadContext = Il2CppSystem.Threading.SynchronizationContext.Current;
|
|
if (MainThreadContext == null)
|
|
Log.LogWarning("[Patch] no main-thread synchronization context was captured; promise resolution may stall.");
|
|
|
|
if (!Uri.TryCreate(ServerUrl.Value, UriKind.Absolute, out var server) || server.Scheme != "https")
|
|
throw new InvalidOperationException("The RadicadePatch client requires an HTTPS server URL.");
|
|
|
|
LogFixedConnection();
|
|
WarnIfMetadataPatchedForAnotherServer(server);
|
|
|
|
var metadataRoot = Path.Combine(UnityEngine.Application.dataPath, "il2cpp_data", "Metadata");
|
|
var gameKeyId = ReadMetadataKey(Path.Combine(metadataRoot, "global-metadata.dat"));
|
|
var interopKeyId = gameKeyId == null ? ReadMetadataKey(Path.Combine(metadataRoot, "global-metadata.interop.dat")) : null;
|
|
ImageKeyId = gameKeyId ?? interopKeyId;
|
|
if (ImageKeyId == null)
|
|
Log.LogWarning("[ImageSig] no image key id was found in the game metadata; rewritten URLs will not state one.");
|
|
|
|
var harmony = new Harmony("com.radicade.patch");
|
|
|
|
var steps = new (string Label, Action Apply)[]
|
|
{
|
|
("module hiding", ApplyModuleHiding),
|
|
("hile gate", ArmHileGate),
|
|
("connection gate", () => ConnectionGate.Install(harmony)),
|
|
("august challenge", () => harmony.PatchAll(typeof(AugustChallengePatches))),
|
|
("august launch diagnostics", () => harmony.PatchAll(typeof(AugustLaunchDiagnostics))),
|
|
("august launch validator", () => harmony.PatchAll(typeof(AugustLaunchValidatorPatch))),
|
|
("rsa verify neutraliser", () => RsaVerifyNeutralizerPatches.Apply(harmony)),
|
|
("file check", () => harmony.PatchAll(typeof(FileCheckEntryPatch))),
|
|
("eac native client stub", () => harmony.PatchAll(typeof(EacNativeClientStubPatch))),
|
|
("eac health query", () => harmony.PatchAll(typeof(EacHealthQueryLiePatch))),
|
|
("cheat scan suppression", () => harmony.PatchAll(typeof(CheatScanSuppressPatch))),
|
|
("cheat detection suppression", () => harmony.PatchAll(typeof(CheatDetectionSuppressPatch))),
|
|
("cheat dorm drop guard", () => harmony.PatchAll(typeof(CheatDormDropGuardPatch))),
|
|
("cheat manager trace", () => harmony.PatchAll(typeof(CheatManagerTracePatch))),
|
|
("exit initiator tracer", () => harmony.PatchAll(typeof(ExitInitiatorTracerPatch))),
|
|
("exit sequence trace", () => harmony.PatchAll(typeof(ExitSequenceTracePatch))),
|
|
("unity quit veto", () => harmony.PatchAll(typeof(UnityQuitVetoPatch))),
|
|
("exit state veto", () => harmony.PatchAll(typeof(ExitStateVetoPatch))),
|
|
("crash address map", CrashAddressMap.Start),
|
|
("tool collision reset guard", () => harmony.PatchAll(typeof(ToolCollisionResetGuardPatch))),
|
|
("collider cast guard", () => ColliderCastGuardPatch.Apply(harmony)),
|
|
("unbound prop freeze", () => harmony.PatchAll(typeof(UnboundPropFreezePatch))),
|
|
("keepsake tool", () => KeepsakeToolPatch.Apply(harmony)),
|
|
("dynamic ui prefab repair", () => DynamicUiPrefabRepair.Apply(harmony)),
|
|
("client text rewrites", () => ClientTextRewrites.Apply(harmony)),
|
|
("voice chat restriction capture", () => VoiceChatRestrictionPatches.ArmInstanceCapture(harmony)),
|
|
("voice chat ban flags", () => VoiceChatBanFlagPatches.Apply(harmony)),
|
|
("invite online friends", () => InviteOnlineFriendsPatch.Apply(harmony)),
|
|
("log flood gate", () => ApplyLogFloodGate(harmony)),
|
|
("realtime exit watch", () => RealtimeDiagnostics.ApplyExitWatch(harmony)),
|
|
("realtime diagnostics", () => RealtimeDiagnostics.Apply(harmony, RealtimeLog?.Value ?? true)),
|
|
("embedded tls", () => EmbeddedTlsPatches.Apply(harmony)),
|
|
("http response tolerance", () => HttpTolerancePatches.Apply(harmony)),
|
|
("log noise filter", () => LogNoiseFilter.Apply(harmony)),
|
|
("nameserver connection", () => harmony.PatchAll(typeof(NameserverConnectionPatches))),
|
|
("websocket redirect", () => WebSocketRedirectPatches.Apply(harmony)),
|
|
};
|
|
|
|
var failed = 0;
|
|
for (var i = 0; i < steps.Length; i++)
|
|
{
|
|
try
|
|
{
|
|
steps[i].Apply();
|
|
Log.LogInfo($"[Patch] {i + 1}/{steps.Length} {steps[i].Label}");
|
|
}
|
|
catch (Exception stepEx)
|
|
{
|
|
failed++;
|
|
Log.LogError($"[Patch] {i + 1}/{steps.Length} {steps[i].Label} failed: {stepEx}");
|
|
}
|
|
}
|
|
|
|
Log.LogInfo("PATCH LIST END");
|
|
if (failed > 0)
|
|
Log.LogWarning($"[Patch] {failed} of {steps.Length} steps failed, so the client may not behave as configured.");
|
|
|
|
ConnectionGate.Open();
|
|
}
|
|
|
|
void ApplyModuleHiding()
|
|
{
|
|
if (!ModuleHidingPolicy.Enabled)
|
|
return;
|
|
|
|
var hidden = PebModuleHider.HideModule("winhttp.dll");
|
|
if (!hidden)
|
|
Log.LogWarning("[ModuleHide] winhttp.dll was not found in the PEB module lists; a module scan will still see it.");
|
|
|
|
var wiped = PeHeaderWiper.WipeSignatures("winhttp.dll");
|
|
if (!wiped)
|
|
Log.LogWarning("[ModuleHide] the winhttp.dll PE signature wipe did not apply (signatures missing or VirtualProtect failed).");
|
|
|
|
if (hidden && wiped)
|
|
Log.LogInfo("[ModuleHide] winhttp.dll is unlinked from the PEB module lists and its PE signatures are wiped.");
|
|
}
|
|
|
|
void ArmHileGate()
|
|
{
|
|
HileGate.Enabled = true;
|
|
Log.LogInfo(HileGate.DropEnabled
|
|
? "[HileGate] armed: anti-cheat report POSTs are dropped at the HTTP layer."
|
|
: "[HileGate] armed: anti-cheat report POSTs are captured in full and then allowed through (RADICADE_HILE_GATE=drop drops them instead).");
|
|
}
|
|
|
|
void ApplyLogFloodGate(Harmony harmony)
|
|
{
|
|
LogFloodGate.Apply(harmony);
|
|
LogFloodGate.RequestSelfTestIfAsked();
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class AugustLaunchDiagnostics
|
|
{
|
|
[HarmonyPrefix, HarmonyPatch(typeof(MILEHJBNIKI), "LMMMGKJCPOF")]
|
|
[HarmonyPatch(typeof(MILEHJBNIKI), "MBNHJLNEIKL")]
|
|
private static void Trace(FDJFJOBADEI __0)
|
|
{
|
|
if (__0 == FDJFJOBADEI.LAUNCH_VALIDATION_FAILED)
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[August launch diagnostic] " + Il2CppSystem.Environment.StackTrace);
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class AugustChallengePatches
|
|
{
|
|
[HarmonyPrefix, HarmonyPatch(typeof(EACManager), nameof(EACManager.GenerateChallengeResponse))]
|
|
private static bool Challenge(ref string __result)
|
|
{
|
|
__result = "radicade-eac-bypass";
|
|
return false;
|
|
}
|
|
|
|
[HarmonyPrefix, HarmonyPatch(typeof(EACManager), "JGFKOKIDHIN")]
|
|
private static bool Ready(ref bool __result)
|
|
{
|
|
__result = true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch(typeof(IDLGLOLKLEB.MKGADIPJDLK), "IDDOELEGIFB")]
|
|
internal static class AugustLaunchValidatorPatch
|
|
{
|
|
static readonly System.Threading.CancellationTokenSource Pump = new();
|
|
|
|
[HarmonyPrefix]
|
|
internal static bool Bypass(IDLGLOLKLEB.MKGADIPJDLK __instance)
|
|
{
|
|
var promise = __instance.promise;
|
|
if (promise == null)
|
|
return true;
|
|
|
|
var context = RadicadeEacBypassPlugin.MainThreadContext;
|
|
try
|
|
{
|
|
var before = Il2CppSystem.Threading.SynchronizationContext.Current;
|
|
if (context != null)
|
|
Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(context);
|
|
try
|
|
{
|
|
promise.BEJNCICKNOM();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogError($"[August] Resolve failed: {e}");
|
|
}
|
|
finally
|
|
{
|
|
Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(before);
|
|
}
|
|
}
|
|
catch (Exception outer)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogError($"[August] Context install failed: {outer}");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
internal static class RsaVerifyNeutralizerPatches
|
|
{
|
|
static readonly byte[] P1KeyModulus = System.Convert.FromBase64String(
|
|
"X07yXkxaaLcZ1wVXfkWjgFkkqdoLhDFm0GPODsF+Q47pSUlbLvtXGqStnyEJEIrQmgDiicAvCdGRq4lovr2l5sIPMaoyizsbVHBdwLUrCsji0RvSBnmvN+8KqQ8STnB4DP4pAsPilfD35def4WuX/xMCXB5+hQUVhv27HPV8Dj9XzHuJAijIM9UwDZmvUcECyiO4wv+TaZi2+ELBtaLCQR8Gm1ZPeDEwP62Ch6MJy0jx5pkvvD0KdF9Wye+3/Wx31Zn/Trdo9HL4sGFWPDM9H9kQhZd5wkTHuxpwGIIhlzIwvY2/pBGdZKP6fi1D2jROEmVkBDyhmYY9nO+s3/bndQ==");
|
|
|
|
static int p1Calls, totalCalls;
|
|
|
|
internal static void Apply(Harmony harmony)
|
|
{
|
|
PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyHash",
|
|
new[]
|
|
{
|
|
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>),
|
|
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>),
|
|
typeof(Il2CppSystem.Security.Cryptography.HashAlgorithmName),
|
|
typeof(Il2CppSystem.Security.Cryptography.RSASignaturePadding)
|
|
});
|
|
|
|
PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyHash",
|
|
new[]
|
|
{
|
|
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>),
|
|
typeof(int),
|
|
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>)
|
|
});
|
|
|
|
PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyData",
|
|
new[]
|
|
{
|
|
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>),
|
|
typeof(Il2CppSystem.Object),
|
|
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>)
|
|
});
|
|
}
|
|
|
|
static void PatchVerify(Harmony harmony, Type target, string methodName, Type[] args)
|
|
{
|
|
try
|
|
{
|
|
var m = AccessTools.Method(target, methodName, args);
|
|
if (m == null)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
|
$"[RsaVerify] {target.Name}.{methodName}({args.Length} args) not found - skipped");
|
|
return;
|
|
}
|
|
harmony.Patch(m, prefix: new HarmonyMethod(typeof(RsaVerifyNeutralizerPatches).GetMethod(nameof(VerifyPrefix))));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
|
$"[RsaVerify] failed to patch {target.Name}.{methodName}: {e.GetType().Name}: {e.Message}");
|
|
}
|
|
} public static bool VerifyPrefix(
|
|
Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider __instance, ref bool __result)
|
|
{
|
|
totalCalls++;
|
|
var isP1 = false;
|
|
try
|
|
{
|
|
var modulus = __instance.ExportParameters(false).Modulus;
|
|
isP1 = modulus != null && modulus.Length == P1KeyModulus.Length && modulus.SequenceEqual(P1KeyModulus);
|
|
if (isP1)
|
|
p1Calls++;
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
__result = isP1;
|
|
if (totalCalls <= 8)
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
|
$"[ImageSig] RSA verify #{totalCalls} ({(isP1 ? "p1 key" : "game key")}) forced {(isP1 ? "true" : "false")}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class FileCheckEntryPatch
|
|
{
|
|
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
|
{
|
|
yield return AccessTools.Method(typeof(GBOAMDFCNJA), "PEEADMOKNLI");
|
|
yield return AccessTools.Method(typeof(GBOAMDFCNJA), "IBPPCNEAGDP");
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
internal static bool Bypass(ref MFBKDKPGMNP<GBOAMDFCNJA.CMIGBOACMAG> __result)
|
|
{
|
|
var result = GBOAMDFCNJA.CMIGBOACMAG.NDGAGMGBOMD("radicade: file check bypassed");
|
|
__result = NOKBHGDKGNF.BGFCCOEKHOE(result);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal static unsafe class PebModuleHider
|
|
{
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct PROCESS_BASIC_INFORMATION
|
|
{
|
|
public IntPtr Reserved1;
|
|
public IntPtr PebBaseAddress;
|
|
public IntPtr Reserved2_0;
|
|
public IntPtr Reserved2_1;
|
|
public IntPtr UniqueProcessId;
|
|
public IntPtr Reserved3;
|
|
}
|
|
|
|
[DllImport("ntdll.dll")]
|
|
private static extern int NtQueryInformationProcess(
|
|
IntPtr hProcess, int processInfoClass, ref PROCESS_BASIC_INFORMATION pbi,
|
|
int cb, out int returnLength);
|
|
|
|
private struct LIST_ENTRY { public IntPtr Flink, Blink; }
|
|
|
|
private const int OffInMemoryOrder = 0x10;
|
|
private const int OffInInitOrder = 0x20;
|
|
private const int OffBaseDllName = 0x58;
|
|
|
|
public static bool HideModule(string dllName)
|
|
{
|
|
var pbi = default(PROCESS_BASIC_INFORMATION);
|
|
int status = NtQueryInformationProcess(new IntPtr(-1), 0, ref pbi,
|
|
Marshal.SizeOf<PROCESS_BASIC_INFORMATION>(), out _);
|
|
if (status != 0 || pbi.PebBaseAddress == IntPtr.Zero)
|
|
return false;
|
|
|
|
byte* peb = (byte*)pbi.PebBaseAddress;
|
|
byte* ldr = *(byte**)(peb + 0x18);
|
|
if (ldr == null)
|
|
return false;
|
|
|
|
LIST_ENTRY* head = (LIST_ENTRY*)(ldr + 0x10);
|
|
byte* target = null;
|
|
for (LIST_ENTRY* n = (LIST_ENTRY*)head->Flink; n != head && target == null; n = (LIST_ENTRY*)n->Flink)
|
|
{
|
|
byte* entry = (byte*)n;
|
|
ushort* us = (ushort*)(entry + OffBaseDllName);
|
|
ushort lenBytes = *us;
|
|
char* buf = *(char**)(entry + OffBaseDllName + 8);
|
|
if (buf == null || lenBytes == 0)
|
|
continue;
|
|
string name = new string(buf, 0, lenBytes / 2);
|
|
if (name.Equals(dllName, StringComparison.OrdinalIgnoreCase))
|
|
target = entry;
|
|
}
|
|
|
|
if (target == null)
|
|
return false;
|
|
|
|
Unlink((LIST_ENTRY*)target);
|
|
Unlink((LIST_ENTRY*)(target + OffInMemoryOrder));
|
|
Unlink((LIST_ENTRY*)(target + OffInInitOrder));
|
|
return true;
|
|
}
|
|
|
|
private static void Unlink(LIST_ENTRY* e)
|
|
{
|
|
var flink = (LIST_ENTRY*)e->Flink;
|
|
var blink = (LIST_ENTRY*)e->Blink;
|
|
if (flink == e || blink == e)
|
|
return;
|
|
blink->Flink = e->Flink;
|
|
flink->Blink = e->Blink;
|
|
e->Flink = (IntPtr)e;
|
|
e->Blink = (IntPtr)e;
|
|
}
|
|
}
|
|
|
|
internal static unsafe class PeHeaderWiper
|
|
{
|
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
private static extern IntPtr GetModuleHandleW(string moduleName);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern bool VirtualProtect(IntPtr address, UIntPtr size, uint newProtect, out uint oldProtect);
|
|
|
|
public static bool WipeSignatures(string dllName)
|
|
{
|
|
IntPtr baseAddr = GetModuleHandleW(dllName);
|
|
if (baseAddr == IntPtr.Zero)
|
|
return false;
|
|
|
|
byte* p = (byte*)baseAddr;
|
|
int eLfanew = *(int*)(p + 0x3C);
|
|
if (eLfanew <= 0 || eLfanew > 0x1000)
|
|
return false;
|
|
|
|
if (!VirtualProtect(baseAddr, (UIntPtr)0x1000, 0x04 , out uint old))
|
|
return false;
|
|
|
|
bool wasMz = p[0] == (byte)'M' && p[1] == (byte)'Z';
|
|
bool wasPe = *(uint*)(p + eLfanew) == 0x00004550;
|
|
|
|
*(ushort*)p = 0;
|
|
*(uint*)(p + eLfanew) = 0;
|
|
|
|
VirtualProtect(baseAddr, (UIntPtr)0x1000, old, out _);
|
|
return wasMz && wasPe;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class ExitInitiatorTracerPatch
|
|
{
|
|
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
|
{
|
|
yield return AccessTools.Method(typeof(KJAMNFCDBIO), "GMAKAKECEDK");
|
|
yield return AccessTools.Method(typeof(KJAMNFCDBIO), "FLMPEJAIKFO");
|
|
yield return AccessTools.Method(typeof(KJAMNFCDBIO), "LHNNEFHHPBG");
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
internal static bool TraceAndBlock()
|
|
{
|
|
var armed = HileGate.DroppedCount > 0;
|
|
if (armed)
|
|
{
|
|
var stack = "<unavailable>";
|
|
try { stack = Environment.StackTrace; } catch { }
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[ExitInit] blocked a managed exit initiator - caller trace:\n" + stack);
|
|
}
|
|
return !armed;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
[HarmonyPatch(typeof(BestHTTP.HTTPManager), "SendRequest", new[] { typeof(BestHTTP.HTTPRequest) })]
|
|
internal static class ExitSequenceTracePatch
|
|
{
|
|
private static int _dumped;
|
|
|
|
[HarmonyPriority(Priority.First)]
|
|
private static void Prefix(BestHTTP.HTTPRequest request)
|
|
{
|
|
try
|
|
{
|
|
var path = request?.Uri?.AbsolutePath;
|
|
if (path == null) return;
|
|
if (path.IndexOf("/matchmaking/player/logout", StringComparison.OrdinalIgnoreCase) < 0 &&
|
|
path.IndexOf("/playersettings/playersettings", StringComparison.OrdinalIgnoreCase) < 0)
|
|
return;
|
|
if (++_dumped > 4) return;
|
|
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
|
"[ExitTrace] shutdown request " + path + " - managed caller stack:" + ExitTraceHelper.SafeStack());
|
|
}
|
|
catch (Exception traceEx)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[ExitTrace] dump failed: " + traceEx.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class UnityQuitVetoPatch
|
|
{
|
|
internal static bool Enabled => _enabled;
|
|
|
|
private static readonly bool _enabled = IsEnabled();
|
|
|
|
private static int _blocks;
|
|
|
|
private static bool IsEnabled()
|
|
{
|
|
try
|
|
{
|
|
var raw = Environment.GetEnvironmentVariable("RADICADE_QUIT_VETO");
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
|
{
|
|
var noArg = AccessTools.Method(typeof(KJAMNFCDBIO), "OFBGCPAHADA", Type.EmptyTypes);
|
|
if (noArg != null) yield return noArg;
|
|
|
|
var flag = AccessTools.Method(typeof(KJAMNFCDBIO), "OFBGCPAHADA", new[] { typeof(bool) });
|
|
if (flag != null) yield return flag;
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Veto(ref bool __result)
|
|
{
|
|
if (!Enabled) return true;
|
|
|
|
__result = false;
|
|
|
|
var count = ++_blocks;
|
|
if (count <= 3 || count % 25 == 0)
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
|
"[QuitVeto] wantsToQuit answered false (block #" + count + ") - Unity's quit is cancelled. " +
|
|
"This is the anti-cheat shutdown, not a user quit; the veto is armed by RADICADE_QUIT_VETO=1, set it to 0 to disable." +
|
|
ExitTraceHelper.SafeStack());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal static class ModuleHidingPolicy
|
|
{
|
|
internal static bool Enabled { get; } = ReadFlag("RADICADE_MODULE_HIDE");
|
|
|
|
private static bool ReadFlag(string name)
|
|
{
|
|
try
|
|
{
|
|
var raw = Environment.GetEnvironmentVariable(name);
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class CheatScanSuppressPatch
|
|
{
|
|
internal static bool Enabled => !_disabled;
|
|
|
|
private static readonly bool _disabled = IsDisabled();
|
|
|
|
private static int _skips;
|
|
|
|
private static bool IsDisabled()
|
|
{
|
|
try
|
|
{
|
|
var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_SCAN");
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static System.Reflection.MethodBase TargetMethod()
|
|
{
|
|
var type = AccessTools.TypeByName("CheatManager");
|
|
if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved.");
|
|
|
|
var method = AccessTools.Method(type, "EKGOMPDGNEK", System.Type.EmptyTypes);
|
|
if (method == null) throw new System.MissingMethodException("CheatManager.EKGOMPDGNEK() is gone - the scan moved or was renamed.");
|
|
|
|
return method;
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Skip()
|
|
{
|
|
if (!Enabled) return true;
|
|
|
|
var count = ++_skips;
|
|
if (count <= 3 || count % 25 == 0)
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatScan] skipped CheatManager.EKGOMPDGNEK() (#" + count
|
|
+ ") - this is the pass behind the Obscured/winhttp.dll verdict, so no report and no shutdown follow from it.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class CheatDetectionSuppressPatch
|
|
{
|
|
internal static bool Enabled => !_disabled;
|
|
|
|
private static readonly bool _disabled = IsDisabled();
|
|
|
|
private static int _skips;
|
|
|
|
private static bool IsDisabled()
|
|
{
|
|
try
|
|
{
|
|
var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_TYPE");
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
|
{
|
|
var type = AccessTools.TypeByName("CheatManager");
|
|
if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved.");
|
|
|
|
var found = new System.Collections.Generic.List<string>();
|
|
var missing = new System.Collections.Generic.List<string>();
|
|
foreach (var name in new[] { "OnObscuredTypeCheatDetected", "OnTimeCheatDetected", "OnAdvancedMovementCheatDetected" })
|
|
{
|
|
var method = AccessTools.Method(type, name, System.Type.EmptyTypes);
|
|
if (method == null)
|
|
{
|
|
missing.Add(name);
|
|
continue;
|
|
}
|
|
|
|
found.Add(name);
|
|
yield return method;
|
|
}
|
|
|
|
_guarded = found.Count;
|
|
_unguarded = missing.Count == 0 ? null : string.Join(", ", missing);
|
|
}
|
|
|
|
internal static int Guarded => _guarded;
|
|
|
|
internal static string Unguarded => _unguarded;
|
|
|
|
private static int _guarded;
|
|
|
|
private static string _unguarded;
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Skip(System.Reflection.MethodBase __originalMethod)
|
|
{
|
|
if (!Enabled) return true;
|
|
|
|
var count = ++_skips;
|
|
if (count <= 5 || count % 25 == 0)
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatType] skipped CheatManager." + __originalMethod.Name + "() (#" + count
|
|
+ ") - this is a verdict handler that ends in the dorm drop with FOKOIBODNAD and the shutdown behind it, so the room survives the detection.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class CheatDormDropGuardPatch
|
|
{
|
|
private const string AntiCheatReason = "385120";
|
|
|
|
internal static bool Enabled => !_disabled;
|
|
|
|
private static readonly bool _disabled = IsDisabled();
|
|
|
|
private static int _skips;
|
|
|
|
private static bool IsDisabled()
|
|
{
|
|
try
|
|
{
|
|
var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_DROP");
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static System.Reflection.MethodBase TargetMethod()
|
|
{
|
|
var type = AccessTools.TypeByName("CheatManager");
|
|
if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved.");
|
|
|
|
var method = AccessTools.Method(type, "FOKOIBODNAD", new[] { typeof(string) });
|
|
if (method == null)
|
|
throw new System.MissingMethodException("CheatManager.FOKOIBODNAD(string) is gone - the dorm drop moved or was renamed, so the guard is NOT armed.");
|
|
|
|
return method;
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Skip(string __0)
|
|
{
|
|
if (!Enabled) return true;
|
|
|
|
if (string.IsNullOrEmpty(__0) || __0.IndexOf(AntiCheatReason, StringComparison.Ordinal) < 0)
|
|
return true;
|
|
|
|
var count = ++_skips;
|
|
if (count <= 5 || count % 25 == 0)
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatDrop] skipped CheatManager.FOKOIBODNAD('" + __0 + "') (#" + count
|
|
+ ") - the anti-cheat's own drop, so the player stays where they are instead of being sent to the dormroom mid-session.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class CheatManagerTracePatch
|
|
{
|
|
private const int PerMethodLimit = 4;
|
|
private const int LineLimit = 90;
|
|
|
|
private static readonly System.Diagnostics.Stopwatch Clock = System.Diagnostics.Stopwatch.StartNew();
|
|
private static readonly System.Collections.Generic.Dictionary<string, int> Seen = new System.Collections.Generic.Dictionary<string, int>();
|
|
private static int _lines;
|
|
|
|
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
|
{
|
|
var outer = AccessTools.TypeByName("CheatManager");
|
|
if (outer == null)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatTrace] CheatManager not resolvable - nothing traced.");
|
|
yield break;
|
|
}
|
|
|
|
var types = new System.Collections.Generic.List<System.Type> { outer };
|
|
foreach (var nested in outer.GetNestedTypes(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic))
|
|
types.Add(nested);
|
|
|
|
foreach (var type in types)
|
|
{
|
|
foreach (var method in type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly))
|
|
{
|
|
if (method.IsAbstract || method.ContainsGenericParameters) continue;
|
|
yield return method;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static readonly bool Enabled = IsEnabled();
|
|
|
|
private static bool IsEnabled()
|
|
{
|
|
try
|
|
{
|
|
var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_TRACE");
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static void Trace(System.Reflection.MethodBase __originalMethod)
|
|
{
|
|
if (!Enabled) return;
|
|
try
|
|
{
|
|
if (_lines >= LineLimit) return;
|
|
|
|
var key = __originalMethod.DeclaringType.Name + "." + __originalMethod.Name;
|
|
|
|
Seen.TryGetValue(key, out var count);
|
|
if (count >= PerMethodLimit) return;
|
|
Seen[key] = count + 1;
|
|
_lines++;
|
|
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatTrace] +" + Clock.ElapsedMilliseconds + "ms " + key);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
internal static class ExitTraceHelper
|
|
{
|
|
internal static string SafeStack()
|
|
{
|
|
try { return "\n" + Environment.StackTrace; } catch { return "\n<stack unavailable>"; }
|
|
}
|
|
}
|
|
|
|
internal static class EacGate
|
|
{
|
|
internal static bool Enabled => !_disabled;
|
|
|
|
private static readonly bool _disabled = IsDisabled();
|
|
|
|
private static int _logCount;
|
|
|
|
private static bool IsDisabled()
|
|
{
|
|
try
|
|
{
|
|
var raw = Environment.GetEnvironmentVariable("RADICADE_EAC_NEUTRALIZE");
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
internal static System.Type Find(string fullName)
|
|
{
|
|
try
|
|
{
|
|
var byName = System.Type.GetType(fullName + ", EasyAntiCheat.Client");
|
|
if (byName != null) return byName;
|
|
}
|
|
catch { }
|
|
|
|
try
|
|
{
|
|
var forced = typeof(EasyAntiCheat.Client.Hydra.Runtime).Assembly;
|
|
var direct = forced.GetType(fullName);
|
|
if (direct != null) return direct;
|
|
}
|
|
catch { }
|
|
|
|
try { return AccessTools.TypeByName(fullName); } catch { return null; }
|
|
}
|
|
|
|
private const int LogLimit = 8;
|
|
|
|
internal static void Log(string message)
|
|
{
|
|
try
|
|
{
|
|
if (++_logCount > LogLimit) return;
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(message);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class EacNativeClientStubPatch
|
|
{
|
|
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
|
{
|
|
var type = EacGate.Find("EasyAntiCheat.Client.Hydra.NativeClient");
|
|
if (type == null)
|
|
{
|
|
EacGate.Log("[EacStub] Hydra.NativeClient not resolvable - EAC runtime left untouched.");
|
|
yield break;
|
|
}
|
|
|
|
foreach (var method in type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
|
if (method.Name == "Initialize")
|
|
yield return method;
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Skip(System.Reflection.MethodBase __originalMethod)
|
|
{
|
|
if (!EacGate.Enabled) return true;
|
|
|
|
EacGate.Log("[EacStub] skipped " + __originalMethod.DeclaringType + "." + __originalMethod.Name
|
|
+ " - EasyAntiCheat's native client never starts, so its module scanner never runs.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class EacHealthQueryLiePatch
|
|
{
|
|
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
|
{
|
|
var native = EacGate.Find("EasyAntiCheat.Client.Hydra.NativeClient");
|
|
if (native != null)
|
|
foreach (var method in native.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
|
if (method.Name == "IsStartedWithEasyAntiCheat" && method.ReturnType == typeof(bool))
|
|
yield return method;
|
|
|
|
var runtime = EacGate.Find("EasyAntiCheat.Client.Hydra.Runtime");
|
|
if (runtime != null)
|
|
foreach (var method in runtime.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
|
if ((method.Name == "IsActive" || method.Name == "get_Initialized") && method.ReturnType == typeof(bool))
|
|
yield return method;
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Lie(ref bool __result, System.Reflection.MethodBase __originalMethod)
|
|
{
|
|
if (!EacGate.Enabled) return true;
|
|
|
|
__result = true;
|
|
EacGate.Log("[EacStub] " + __originalMethod.DeclaringType + "." + __originalMethod.Name + " answered true.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
[HarmonyPatch]
|
|
internal static class ToolCollisionResetGuardPatch
|
|
{
|
|
internal static bool Enabled => !_disabled;
|
|
|
|
private static readonly bool _disabled = IsDisabled();
|
|
|
|
private static bool IsDisabled()
|
|
{
|
|
try
|
|
{
|
|
var raw = Environment.GetEnvironmentVariable("RADICADE_TOOL_RESET");
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static System.Reflection.MethodBase TargetMethod()
|
|
{
|
|
var method = AccessTools.Method(typeof(Tool), "ResetToDefaultCollisionLayer", Type.EmptyTypes);
|
|
if (method == null)
|
|
throw new MissingMethodException(
|
|
"Tool.ResetToDefaultCollisionLayer() not found - the room crash guard is NOT armed.");
|
|
return method;
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Skip(Tool __instance)
|
|
{
|
|
if (!Enabled) return true;
|
|
ToolCollisionLayerRestore.Restore(__instance);
|
|
return false;
|
|
}
|
|
|
|
private static string Describe(Tool tool)
|
|
{
|
|
try
|
|
{
|
|
if (tool == null) return "<null tool>";
|
|
var owner = tool.gameObject;
|
|
return (owner == null) ? "<no gameObject>" : "'" + owner.name + "'";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return "<describe failed: " + ex.GetType().Name + ">";
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class ExitStateVetoPatch
|
|
{
|
|
internal static bool Enabled => _enabled;
|
|
|
|
private static readonly bool _enabled = IsEnabled();
|
|
|
|
private static int _blocks;
|
|
|
|
private static bool IsEnabled()
|
|
{
|
|
try
|
|
{
|
|
var raw = Environment.GetEnvironmentVariable("RADICADE_EXIT_VETO");
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
raw = raw.Trim().ToLowerInvariant();
|
|
return raw is "1" or "on" or "true" or "yes";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
|
{
|
|
var property = AccessTools.PropertySetter(typeof(KJAMNFCDBIO), "JGKJEDKNFHN");
|
|
if (property != null) yield return property;
|
|
|
|
var raw = AccessTools.Method(typeof(KJAMNFCDBIO), "EHPDJDJLPKM",
|
|
new[] { typeof(KJAMNFCDBIO.CNNHHNMPLJE) });
|
|
if (raw != null) yield return raw;
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Block(KJAMNFCDBIO.CNNHHNMPLJE __0)
|
|
{
|
|
if (!Enabled) return true;
|
|
if (__0 == KJAMNFCDBIO.CNNHHNMPLJE.Running) return true;
|
|
|
|
var count = ++_blocks;
|
|
var caller = count == 1 ? ExitTraceHelper.SafeStack() : string.Empty;
|
|
if (count <= 3 || count % 25 == 0)
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
|
$"[ExitVeto] blocked AppExitState -> {__0} (block #{count}); the session keeps running. " +
|
|
"This is the anti-cheat-driven exit, not a user quit - the veto is armed by RADICADE_EXIT_VETO=1, set it to 0 to disable." + caller);
|
|
return false;
|
|
}
|
|
}
|
|
|