mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
1431 lines
39 KiB
C#
1431 lines
39 KiB
C#
using BepInEx;
|
|
using BepInEx.Configuration;
|
|
using BepInEx.Logging;
|
|
using BepInEx.Unity.IL2CPP;
|
|
using HarmonyLib;
|
|
using RecRoom.AntiCheat;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
|
|
|
[BepInPlugin("com.radicade.eacbypass", "Radicade August Client", "2.0.0")]
|
|
public sealed class RadicadeEacBypassPlugin : BasePlugin
|
|
{
|
|
internal static ManualLogSource LogSource;
|
|
internal static ConfigEntry<string> ServerUrl, TlsAllowedHosts;
|
|
internal static Il2CppSystem.Threading.SynchronizationContext MainThreadContext;
|
|
internal static string ImageKeyId;
|
|
|
|
public override void Load()
|
|
{
|
|
LogSource = Log;
|
|
if (UnityEngine.Application.version != "20230825")
|
|
throw new NotSupportedException("This plugin requires Rec Room 20230825.");
|
|
MainThreadContext = Il2CppSystem.Threading.SynchronizationContext.Current;
|
|
Log.LogInfo($"[August] main-thread sync context: {(MainThreadContext != null ? MainThreadContext.GetType().Name : "NULL")}");
|
|
ServerUrl = Config.Bind("Connection", "ServerUrl", "https://localhost:3030", "Private server URL.");
|
|
if (!Uri.TryCreate(ServerUrl.Value, UriKind.Absolute, out var server) || server.Scheme != "https")
|
|
throw new InvalidOperationException("The August client requires an HTTPS server URL.");
|
|
TlsAllowedHosts = Config.Bind("TLS", "AllowedHosts", server.Host, "Exact private server hosts allowed by the embedded TLS integration.");
|
|
if (!EmbeddedTlsPatches.IsHostAllowed(server.Host))
|
|
throw new InvalidOperationException("TLS AllowedHosts must include the configured private server host.");
|
|
|
|
var metadata = Path.Combine(UnityEngine.Application.dataPath, "il2cpp_data", "Metadata", "global-metadata.interop.dat");
|
|
ImageKeyId = ImageSignatureRequest.DetectKeyId(File.ReadAllBytes(metadata));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (ModuleHidingPolicy.Enabled)
|
|
{
|
|
var hidden = PebModuleHider.HideModule("winhttp.dll");
|
|
Log.LogInfo(hidden
|
|
? "[ModuleHide] winhttp.dll unlinked from PEB module lists (scanner-invisible)"
|
|
: "[ModuleHide] WARNING: winhttp.dll not found in PEB lists - scanner will still see it");
|
|
|
|
|
|
var wiped = PeHeaderWiper.WipeSignatures("winhttp.dll");
|
|
Log.LogInfo(wiped
|
|
? "[ModuleHide] winhttp.dll PE signatures wiped (MZ+PE) - region scans cannot classify it"
|
|
: "[ModuleHide] WARNING: PE signature wipe did not apply (signatures missing or VirtualProtect failed)");
|
|
}
|
|
else
|
|
{
|
|
Log.LogInfo("[ModuleHide] disabled by default (RADICADE_MODULE_HIDE=1 re-arms runs 14/15): winhttp.dll keeps its loader-list entry and its PE headers, so a module scan has nothing to call obscured.");
|
|
}
|
|
|
|
Log.LogInfo(ModuleHidingPolicy.DescribeModuleList());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HileGate.Enabled = true;
|
|
Log.LogInfo("[HileGate] armed - anti-cheat report POSTs are " + (HileGate.DropEnabled
|
|
? "dropped at the HTTP layer (RADICADE_HILE_GATE=drop)"
|
|
: "captured in full and then allowed through, so the report body is on the record (RADICADE_HILE_GATE=drop restores the drop)") + ".");
|
|
|
|
var harmony = new Harmony("com.radicade.eacbypass");
|
|
|
|
EmbeddedTlsPatches.Apply(harmony);
|
|
harmony.PatchAll(typeof(NameserverConnectionPatches));
|
|
WebSocketRedirectPatches.Apply(harmony);
|
|
harmony.PatchAll(typeof(AugustChallengePatches));
|
|
harmony.PatchAll(typeof(AugustLaunchDiagnostics));
|
|
harmony.PatchAll(typeof(AugustLaunchValidatorPatch));
|
|
RsaVerifyNeutralizerPatches.Apply(harmony);
|
|
harmony.PatchAll(typeof(FileCheckEntryPatch));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
harmony.PatchAll(typeof(ExitInitiatorTracerPatch));
|
|
Log.LogInfo("[ExitInit] armed - managed exit initiators are blocked once a hile report has been dropped, and traced.");
|
|
|
|
|
|
|
|
try
|
|
{
|
|
harmony.PatchAll(typeof(ExitStateVetoPatch));
|
|
|
|
|
|
|
|
|
|
|
|
Log.LogInfo("[ExitVeto] patches applied; blockExitState enabled=" + ExitStateVetoPatch.Enabled
|
|
+ " (opt-in fallback only; a normal quit and server logouts work either way. RADICADE_EXIT_VETO=1 arms it)");
|
|
}
|
|
catch (Exception vetoEx)
|
|
{
|
|
|
|
|
|
Log.LogError("[ExitVeto] failed to apply - continuing without it: " + vetoEx);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
{
|
|
harmony.PatchAll(typeof(ExitSequenceTracePatch));
|
|
harmony.PatchAll(typeof(UnityQuitVetoPatch));
|
|
|
|
|
|
harmony.PatchAll(typeof(EacNativeClientStubPatch));
|
|
harmony.PatchAll(typeof(EacHealthQueryLiePatch));
|
|
harmony.PatchAll(typeof(CheatManagerTracePatch));
|
|
harmony.PatchAll(typeof(CheatScanSuppressPatch));
|
|
Log.LogInfo("[CheatScan] verdict suppression enabled=" + CheatScanSuppressPatch.Enabled);
|
|
Log.LogInfo("[CheatType] cheat-detection suppression enabled=" + CheatDetectionSuppressPatch.Enabled
|
|
+ " (RADICADE_CHEAT_TYPE=0 restores the detection and the dorm drop that follows it)");
|
|
Log.LogInfo("[CheatDrop] dorm-drop guard enabled=" + CheatDormDropGuardPatch.Enabled
|
|
+ " (RADICADE_CHEAT_DROP=0 lets every drop run)");
|
|
|
|
|
|
|
|
try
|
|
{
|
|
harmony.PatchAll(typeof(CheatDetectionSuppressPatch));
|
|
Log.LogInfo("[CheatType] verdict handlers guarded=" + CheatDetectionSuppressPatch.Guarded
|
|
+ (CheatDetectionSuppressPatch.Unguarded == null
|
|
? " (all three)"
|
|
: " - UNGUARDED, not found on CheatManager: " + CheatDetectionSuppressPatch.Unguarded));
|
|
}
|
|
catch (Exception cheatTypeEx)
|
|
{
|
|
Log.LogError("[CheatType] verdict suppression NOT armed: " + cheatTypeEx.Message);
|
|
}
|
|
|
|
try
|
|
{
|
|
harmony.PatchAll(typeof(CheatDormDropGuardPatch));
|
|
Log.LogInfo("[CheatDrop] dorm drop (EFLCHFDDPJP) held back for the anti-cheat reason; "
|
|
+ "every other reason still drops the player.");
|
|
}
|
|
catch (Exception cheatDropEx)
|
|
{
|
|
Log.LogError("[CheatDrop] dorm-drop guard NOT armed: " + cheatDropEx.Message);
|
|
}
|
|
|
|
Log.LogInfo("[EacStub] EasyAntiCheat Hydra runtime neutralisation enabled=" + EacGate.Enabled
|
|
+ " (RADICADE_EAC_NEUTRALIZE=0 before launch leaves EAC alone)");
|
|
Log.LogInfo("[ExitTrace] shutdown-request stack dump armed; [QuitVeto] wantsToQuit veto enabled="
|
|
+ UnityQuitVetoPatch.Enabled);
|
|
}
|
|
catch (Exception quitEx)
|
|
{
|
|
Log.LogError("[ExitTrace] failed to apply - continuing without them: " + quitEx);
|
|
}
|
|
|
|
|
|
|
|
try
|
|
{
|
|
CrashAddressMap.Start();
|
|
Log.LogInfo("[AddrMap] enabled=" + CrashAddressMap.Enabled
|
|
+ " target=0x" + CrashAddressMap.TargetRva.ToString("x")
|
|
+ " (RADICADE_ADDR_MAP=0 disables; RADICADE_ADDR_TARGET=0x... re-aims it)");
|
|
}
|
|
catch (Exception mapEx)
|
|
{
|
|
|
|
Log.LogError("[AddrMap] failed to start: " + mapEx);
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
{
|
|
harmony.PatchAll(typeof(ToolCollisionResetGuardPatch));
|
|
Log.LogInfo("[ToolGuard] Tool.ResetToDefaultCollisionLayer skipped=" + ToolCollisionResetGuardPatch.Enabled
|
|
+ " (RADICADE_TOOL_RESET=0 before launch restores the original reset)");
|
|
}
|
|
catch (Exception toolEx)
|
|
{
|
|
Log.LogError("[ToolGuard] failed to apply - the room crash is NOT guarded: " + toolEx);
|
|
}
|
|
Log.LogInfo($"[August] Connection patches loaded for {ServerUrl.Value}; image key {ImageKeyId ?? "unknown"}.");
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class AugustLaunchDiagnostics
|
|
{
|
|
[HarmonyPrefix, HarmonyPatch(typeof(NDIIPBKGGIG), "FIBOEONALKB")]
|
|
[HarmonyPatch(typeof(NDIIPBKGGIG), "IKHPLCCBENN")]
|
|
private static void Trace(JKBPKDAGEHN __0)
|
|
{
|
|
if (__0 == JKBPKDAGEHN.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), "MGEAHKJIIMO")]
|
|
private static bool Ready(ref bool __result)
|
|
{
|
|
__result = true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HarmonyPatch(typeof(ADJOODNBCKG.NFPDGJNDFOF), "GBAMNAKGEGD")]
|
|
internal static class AugustLaunchValidatorPatch
|
|
{
|
|
static readonly System.Threading.CancellationTokenSource Pump = new();
|
|
|
|
[HarmonyPrefix]
|
|
internal static bool Bypass(ADJOODNBCKG.NFPDGJNDFOF __instance)
|
|
{
|
|
var promise = __instance.promise;
|
|
if (promise == null)
|
|
return true;
|
|
RadicadeEacBypassPlugin.LogSource.LogInfo("[August] Bypassing native launch validator (hash check cannot pass in a patched install).");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var context = RadicadeEacBypassPlugin.MainThreadContext;
|
|
try
|
|
{
|
|
var before = Il2CppSystem.Threading.SynchronizationContext.Current;
|
|
RadicadeEacBypassPlugin.LogSource.LogInfo(
|
|
$"[August] validator thread context: {(before != null ? before.GetType().Name : "null")}; installing main-thread context");
|
|
if (context != null)
|
|
Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(context);
|
|
try
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogInfo(
|
|
$"[August] resolving: done={SafeState(promise, "EPCPPDDJCMO")} handlers={SafeHandlerCount(promise)}");
|
|
promise.OLKGFACPIPC();
|
|
RadicadeEacBypassPlugin.LogSource.LogInfo("[August] Launch-validation promise resolved; boot continues.");
|
|
}
|
|
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;
|
|
}
|
|
|
|
static int SafeHandlerCount(PFDOLDLEIBA promise)
|
|
{
|
|
try { return promise.HFPMLNDOGLF?.Count ?? 0; }
|
|
catch { return -1; }
|
|
}
|
|
|
|
static bool SafeState(PFDOLDLEIBA promise, string prop)
|
|
{
|
|
try
|
|
{
|
|
return prop switch
|
|
{
|
|
"EPCPPDDJCMO" => promise.EPCPPDDJCMO,
|
|
"MPJBGFFEEOP" => promise.MPJBGFFEEOP,
|
|
"NEIINCCHEPF" => promise.NEIINCCHEPF,
|
|
_ => false,
|
|
};
|
|
}
|
|
catch { 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))));
|
|
RadicadeEacBypassPlugin.LogSource.LogInfo(
|
|
$"[RsaVerify] patched {target.Name}.{methodName}({string.Join(", ", Array.ConvertAll(args, t => t.Name))})");
|
|
}
|
|
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(KKHDHILFOGO), "GGIFLDPHCIP");
|
|
yield return AccessTools.Method(typeof(KKHDHILFOGO), "GOCCLPJDJIC");
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
internal static bool Bypass(ref MCFFIFIPNKE<KKHDHILFOGO.BJGODLEIDGE> __result)
|
|
{
|
|
var result = KKHDHILFOGO.BJGODLEIDGE.NOMPJGOIAEB("radicade: file check bypassed");
|
|
__result = PFDOLDLEIBA.PLJINLOCHGM(result);
|
|
RadicadeEacBypassPlugin.LogSource.LogInfo("[FileCheck] entry bypassed - resolved success promise returned");
|
|
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(CCKNJCHHBLL), "BEFDPLAELIH");
|
|
yield return AccessTools.Method(typeof(CCKNJCHHBLL), "GIEEBIOOFML");
|
|
yield return AccessTools.Method(typeof(CCKNJCHHBLL), "FNOEHNDLHHG");
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
internal static bool TraceAndBlock()
|
|
{
|
|
var stack = "<unavailable>";
|
|
try { stack = Environment.StackTrace; } catch { }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var armed = HileGate.DroppedCount > 0;
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(armed
|
|
? "[ExitInit] blocked managed exit initiator - caller trace:\n" + stack
|
|
: "[ExitInit] allowed managed exit initiator (no report filed yet) - 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(CCKNJCHHBLL), "GBGMBMGDGGD", Type.EmptyTypes);
|
|
if (noArg != null) yield return noArg;
|
|
|
|
var flag = AccessTools.Method(typeof(CCKNJCHHBLL), "GBGMBMGDGGD", 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; }
|
|
}
|
|
|
|
internal static string DescribeModuleList()
|
|
{
|
|
try
|
|
{
|
|
using (var self = System.Diagnostics.Process.GetCurrentProcess())
|
|
{
|
|
var seen = false;
|
|
var path = string.Empty;
|
|
|
|
foreach (System.Diagnostics.ProcessModule module in self.Modules)
|
|
{
|
|
if (!string.Equals(module.ModuleName, "winhttp.dll", StringComparison.OrdinalIgnoreCase)) continue;
|
|
seen = true;
|
|
path = module.FileName ?? string.Empty;
|
|
break;
|
|
}
|
|
|
|
return seen
|
|
? "[ModuleList] winhttp.dll is enumerated by a Toolhelp module walk: " + path
|
|
: "[ModuleList] winhttp.dll is NOT enumerated by a Toolhelp module walk - a loaded image the loader lists do not know about is the most literal definition of obscured.";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return "[ModuleList] enumeration failed: " + ex.Message;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[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, "MJLAGDJLOBC", System.Type.EmptyTypes);
|
|
if (method == null) throw new System.MissingMethodException("CheatManager.MJLAGDJLOBC() 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.MJLAGDJLOBC() (#" + 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 EFLCHFDDPJP 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, "EFLCHFDDPJP", new[] { typeof(string) });
|
|
if (method == null)
|
|
throw new System.MissingMethodException("CheatManager.EFLCHFDDPJP(string) is gone - the dorm drop moved or was renamed, so the guard is NOT armed.");
|
|
|
|
return method;
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Skip(string JELLDPBCBLG)
|
|
{
|
|
if (!Enabled) return true;
|
|
|
|
|
|
if (string.IsNullOrEmpty(JELLDPBCBLG) || JELLDPBCBLG.IndexOf(AntiCheatReason, StringComparison.Ordinal) < 0)
|
|
return true;
|
|
|
|
var count = ++_skips;
|
|
if (count <= 5 || count % 25 == 0)
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatDrop] skipped CheatManager.EFLCHFDDPJP('" + JELLDPBCBLG + "') (#" + 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
[HarmonyPrefix]
|
|
private static void Trace(System.Reflection.MethodBase __originalMethod)
|
|
{
|
|
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 int _skips;
|
|
|
|
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;
|
|
|
|
var count = ++_skips;
|
|
|
|
|
|
if (count <= 5 || count % 50 == 0)
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
|
"[ToolGuard] skipped Tool.ResetToDefaultCollisionLayer on " + Describe(__instance)
|
|
+ " (#" + count + ") - this method is the frame directly above the access violation "
|
|
+ "at GameAssembly.dll+0x470728d, which walks the tool's ECS collision-layer components. "
|
|
+ "Tool resets still apply; only the collision layer is left as it was.");
|
|
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(CCKNJCHHBLL), "GLNIKJNLCIB");
|
|
if (property != null) yield return property;
|
|
|
|
|
|
|
|
var raw = AccessTools.Method(typeof(CCKNJCHHBLL), "KALEFEDAHCC",
|
|
new[] { typeof(CCKNJCHHBLL.CCFIKLBABNB) });
|
|
if (raw != null) yield return raw;
|
|
}
|
|
|
|
|
|
|
|
[HarmonyPrefix]
|
|
private static bool Block(CCKNJCHHBLL.CCFIKLBABNB __0)
|
|
{
|
|
if (!Enabled) return true;
|
|
if (__0 == CCKNJCHHBLL.CCFIKLBABNB.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;
|
|
}
|
|
}
|
|
|