mirror of
https://github.com/djdevin/recnet-plugin.git
synced 2026-09-08 14:41:30 -07:00
Support for build 20230414 (#5)
* support for 20230331, remove need for metadata patching * build 20230414
This commit is contained in:
@@ -21,11 +21,13 @@ public static class DUIDMismatchPatch
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CheatManager), "CheckForDUIDMismatch")]
|
||||
private static bool Prefix(ref string ALOMDLLNIMD, ref bool __result)
|
||||
// __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)
|
||||
{
|
||||
ALOMDLLNIMD = SimulatedStoredDeviceId;
|
||||
__0 = SimulatedStoredDeviceId;
|
||||
__result = true;
|
||||
Plugin.Log.LogWarning($"[DUID] simulating mismatch, stored id = {SimulatedStoredDeviceId}");
|
||||
return false;
|
||||
@@ -33,7 +35,7 @@ public static class DUIDMismatchPatch
|
||||
|
||||
if (Plugin.SuppressDUIDMismatch.Value)
|
||||
{
|
||||
ALOMDLLNIMD = string.Empty;
|
||||
__0 = string.Empty;
|
||||
__result = false;
|
||||
Plugin.Log.LogInfo("[DUID] mismatch check forced to false (suppressed)");
|
||||
return false;
|
||||
|
||||
@@ -9,7 +9,10 @@ namespace RecNetPlugin.Patches;
|
||||
public static class EACPatches
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(EACManager), "FJLMLEPOKGE")]
|
||||
// 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;
|
||||
@@ -18,10 +21,11 @@ public static class EACPatches
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(EACManager), "GenerateChallengeResponse")]
|
||||
private static bool GenerateChallengeResponsePatch(string PGCINMIEBJP, ref string __result)
|
||||
// __0 = the challenge string (positional); obfuscated param names shift between game builds.
|
||||
private static bool GenerateChallengeResponsePatch(string __0, ref string __result)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(PGCINMIEBJP))
|
||||
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes(PGCINMIEBJP));
|
||||
if (!string.IsNullOrEmpty(__0))
|
||||
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes(__0));
|
||||
else
|
||||
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes("nothing"));
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
using System;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
using Il2CppSystem.Security.Cryptography;
|
||||
using Convert = Il2CppSystem.Convert;
|
||||
|
||||
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, between the literal and the verify.
|
||||
//
|
||||
// The stored value is 256 bytes once base64-decoded and does NOT start with 0x30, so it is a RAW
|
||||
// 2048-bit modulus, not a DER/SPKI blob. The client therefore has to base64-decode it and hand-build
|
||||
// an RSAParameters { Modulus, Exponent }. Both of those steps are plain framework calls with
|
||||
// unobfuscated names, which is why hooking here survives game rebuilds (unlike the obfuscated
|
||||
// PromisePatch this replaces).
|
||||
//
|
||||
// Two knobs, 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.
|
||||
// Signing Modulus Override -> secondary: swap in your own modulus and keep real verification,
|
||||
// for a deployment that DOES want signed images. Ignored (well,
|
||||
// redundant) while the verify is disabled.
|
||||
//
|
||||
// Both are belt-and-braces: we swap at Convert.FromBase64String (catches the value regardless of
|
||||
// which crypto stack consumes it) AND at ImportParameters (catches it if the client uses some other
|
||||
// base64 decoder). Whichever fires first wins; the second sees the already-swapped value and no-ops.
|
||||
//
|
||||
// If the log never shows "[SIG] stock modulus seen", the client is not using mscorlib RSA at all —
|
||||
// the fallback is BestHTTP.SecureProtocol.Org.BouncyCastle, where the equivalent hooks are
|
||||
// RsaKeyParameters..ctor(bool, BigInteger, BigInteger) and the concrete RsaDigestSigner/PssSigner
|
||||
// .VerifySignature (NOT the ISigner "interface", which is abstract and never dispatches).
|
||||
[HarmonyPatch]
|
||||
public static class ImageSigningPatch
|
||||
{
|
||||
// The stock 2048-bit public modulus baked into global-metadata.dat (base64, 344 chars).
|
||||
private const string StockModulusBase64 =
|
||||
"X07yXkxaaLcZ1wVXfkWjgFkkqdoLhDFm0GPODsF+Q47pSUlbLvtXGqStnyEJEIrQmgDiicAvCdGRq4lovr2l5sIP" +
|
||||
"MaoyizsbVHBdwLUrCsji0RvSBnmvN+8KqQ8STnB4DP4pAsPilfD35def4WuX/xMCXB5+hQUVhv27HPV8Dj9XzHuJ" +
|
||||
"AijIM9UwDZmvUcECyiO4wv+TaZi2+ELBtaLCQR8Gm1ZPeDEwP62Ch6MJy0jx5pkvvD0KdF9Wye+3/Wx31Zn/Trdo" +
|
||||
"9HL4sGFWPDM9H9kQhZd5wkTHuxpwGIIhlzIwvY2/pBGdZKP6fi1D2jROEmVkBDyhmYY9nO+s3/bndQ==";
|
||||
|
||||
private const int ModulusBytes = 256;
|
||||
|
||||
private static byte[] _stockModulus;
|
||||
private static byte[] _override;
|
||||
private static bool _overrideResolved;
|
||||
private static bool _loggedSeen;
|
||||
private static bool _loggedForced;
|
||||
|
||||
private static byte[] Stock => _stockModulus ??= System.Convert.FromBase64String(StockModulusBase64);
|
||||
|
||||
// Decoded lazily and cached, so a malformed config value is reported once instead of per call.
|
||||
private static byte[] Override
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_overrideResolved)
|
||||
return _override;
|
||||
_overrideResolved = true;
|
||||
|
||||
var raw = Plugin.SigningModulusOverride.Value;
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return _override = null;
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = System.Convert.FromBase64String(raw.Trim());
|
||||
if (bytes.Length != ModulusBytes)
|
||||
{
|
||||
Plugin.Log.LogError(
|
||||
$"[SIG] 'Signing Modulus Override' decoded to {bytes.Length} bytes, expected {ModulusBytes} " +
|
||||
"(a raw 2048-bit modulus). Ignoring it. Note this must be the bare modulus, NOT a PEM/DER key.");
|
||||
return _override = null;
|
||||
}
|
||||
|
||||
Plugin.Log.LogWarning("[SIG] signing modulus override active");
|
||||
return _override = bytes;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
Plugin.Log.LogError("[SIG] 'Signing Modulus Override' is not valid base64. Ignoring it.");
|
||||
return _override = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsStock(Il2CppStructArray<byte> value)
|
||||
{
|
||||
if (value == null || value.Length != ModulusBytes)
|
||||
return false;
|
||||
|
||||
var stock = Stock;
|
||||
for (var i = 0; i < ModulusBytes; i++)
|
||||
if (value[i] != stock[i])
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void NoteSeen(string where)
|
||||
{
|
||||
if (_loggedSeen)
|
||||
return;
|
||||
_loggedSeen = true;
|
||||
Plugin.Log.LogWarning($"[SIG] stock modulus seen at {where}");
|
||||
}
|
||||
|
||||
// Choke point 1: the base64 decode of the literal. Guarded by a length check first, because this
|
||||
// runs for every base64 decode in the game.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Convert), nameof(Convert.FromBase64String))]
|
||||
private static bool FromBase64StringPrefix(string __0, ref Il2CppStructArray<byte> __result)
|
||||
{
|
||||
if (__0 == null || __0.Length != StockModulusBase64.Length || __0 != StockModulusBase64)
|
||||
return true;
|
||||
|
||||
NoteSeen("Convert.FromBase64String");
|
||||
|
||||
var replacement = Override;
|
||||
if (replacement == null)
|
||||
return true;
|
||||
|
||||
var arr = new Il2CppStructArray<byte>(ModulusBytes);
|
||||
for (var i = 0; i < ModulusBytes; i++)
|
||||
arr[i] = replacement[i];
|
||||
|
||||
__result = arr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Choke point 2: the key import. Mutates the modulus in place — both are 2048-bit, so the array
|
||||
// is already the right size and no reallocation is needed.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RSACryptoServiceProvider), nameof(RSACryptoServiceProvider.ImportParameters))]
|
||||
private static void ImportParametersPrefix(RSAParameters __0)
|
||||
{
|
||||
var modulus = __0?.Modulus;
|
||||
if (!IsStock(modulus))
|
||||
return;
|
||||
|
||||
NoteSeen("RSACryptoServiceProvider.ImportParameters");
|
||||
|
||||
var replacement = Override;
|
||||
if (replacement == null)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < ModulusBytes; i++)
|
||||
modulus[i] = replacement[i];
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -7,7 +7,10 @@ namespace RecNetPlugin.Patches;
|
||||
/**
|
||||
Patches Photon to use the App IDs and server hostname/port specified in the plugin config.
|
||||
*/
|
||||
[HarmonyPatch(typeof(GPFPFDBGCEK), "AMOHMPKKGHL")]
|
||||
// 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]
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using HarmonyLib;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
/**
|
||||
* This allows the global-metadata.dat to be different on the client
|
||||
* patched to allow a different modulus so we can sign images.
|
||||
*/
|
||||
[HarmonyPatch(typeof(JAPJPGNBMNM), "JOKECJKBJGD")]
|
||||
public static class PromisePatch
|
||||
{
|
||||
public static bool Prefix(out HPHDJAFFHCN<JAPJPGNBMNM.AOFCCEACNNA> __result)
|
||||
{
|
||||
var result = JAPJPGNBMNM.AOFCCEACNNA.JGIHNLEFJEL();
|
||||
var promise = HAAHJPGNIMD.NMOOLKAJDOC(result);
|
||||
__result = promise;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user