Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of koumodgp v0.5.0
koumodgp.dll
Decompiled 9 hours agousing System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Mono.Cecil; using Mono.Collections.Generic; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace koumodgp { internal static class Admin { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleEvent <>9__2_0; internal void <Register>b__2_0(ConsoleEventArgs args) { Terminal context = args.Context; if (context != null) { context.AddString(Execute(args.Args)); } } } private static bool _registered; private static string _cfgDir; internal static void Register(string configDir) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown if (_registered) { return; } _cfgDir = configDir; try { object obj = <>c.<>9__2_0; if (obj == null) { ConsoleEvent val = delegate(ConsoleEventArgs args) { Terminal context = args.Context; if (context != null) { context.AddString(Execute(args.Args)); } }; <>c.<>9__2_0 = val; obj = (object)val; } new ConsoleCommand("kougp", "koumodgp admin: status|list|reload|allow|sig|kick|ban", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, true); _registered = true; Plugin.Log.LogInfo((object)"[koumodgp] admin command 'kougp' registered (server console)."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] could not register command: " + ex.Message)); } } internal static void SetConfigDir(string configDir) { _cfgDir = configDir; } internal static string Execute(string[] a) { StringBuilder stringBuilder = new StringBuilder(); switch ((a.Length > 1) ? a[1].ToLowerInvariant() : "status") { case "status": Status(stringBuilder); break; case "list": ListCmd(stringBuilder); break; case "reload": Config.LoadLists(); PushToAll(); stringBuilder.AppendLine("config + lists reloaded, pushed to clients."); Discord.PostLifecycle("koumodgp: config reloaded by admin."); break; case "allow": Allow(stringBuilder, a); break; case "sig": Sig(stringBuilder, a); break; case "kick": KickBan(stringBuilder, a, ban: false); break; case "ban": KickBan(stringBuilder, a, ban: true); break; default: stringBuilder.AppendLine("usage: kougp status|list|reload|allow add/remove <guid> [ver]|sig add/remove <text>|kick <name>|ban <name>"); break; } string text = stringBuilder.ToString().TrimEnd(Array.Empty<char>()); Plugin.Log.LogInfo((object)("[koumodgp] " + text)); return text; } private static void Status(StringBuilder sb) { List<Session> list = Sessions.Snapshot(); sb.AppendLine($"koumodgp: {list.Count} tracked peers, {Config.AllowedMods.Count} required, {Config.OptionalMods.Count} optional, {Config.Signatures.Count} signatures."); foreach (Session item in list) { sb.AppendLine($" {item.PlayerName} ({item.SteamId}) verified={item.Verified} lastHB={Time.time - item.LastHeartbeat:F0}s ago"); } } private static void ListCmd(StringBuilder sb) { sb.AppendLine("Required mods:"); foreach (KeyValuePair<string, string> allowedMod in Config.AllowedMods) { sb.AppendLine(" " + allowedMod.Key + " = " + allowedMod.Value); } sb.AppendLine("Optional mods:"); foreach (KeyValuePair<string, string> optionalMod in Config.OptionalMods) { sb.AppendLine(" " + optionalMod.Key + " = " + optionalMod.Value); } sb.AppendLine("Signatures:"); foreach (string signature in Config.Signatures) { sb.AppendLine(" " + signature); } } private static void Allow(StringBuilder sb, string[] a) { if (a.Length < 4) { sb.AppendLine("usage: kougp allow add <guid> [version] | remove <guid>"); return; } string text = a[2].ToLowerInvariant(); string guid = a[3]; string text2 = ((a.Length > 4) ? a[4] : "*"); string path = Path.Combine(_cfgDir, "koumodgp", "allowed_mods.txt"); if (text == "add") { File.AppendAllText(path, guid + "=" + text2 + Environment.NewLine); sb.AppendLine("added " + guid + "=" + text2); } else if (text == "remove") { string[] contents = (from l in File.ReadAllLines(path) where !l.Trim().StartsWith(guid + "=") select l).ToArray(); File.WriteAllLines(path, contents); sb.AppendLine("removed " + guid); } Config.LoadLists(); PushToAll(); Discord.PostLifecycle("koumodgp: allow list edited by admin (" + text + " " + guid + ")."); } private static void Sig(StringBuilder sb, string[] a) { if (a.Length < 4) { sb.AppendLine("usage: kougp sig add <text> | remove <text>"); return; } string text = a[2].ToLowerInvariant(); string text2 = string.Join(" ", a.Skip(3)).ToLowerInvariant(); string path = Path.Combine(_cfgDir, "koumodgp", "signatures.txt"); if (text == "add") { File.AppendAllText(path, text2 + Environment.NewLine); sb.AppendLine("added signature '" + text2 + "'"); } else if (text == "remove") { string[] contents = (from l in File.ReadAllLines(path) where l.Trim().ToLowerInvariant() != text2 select l).ToArray(); File.WriteAllLines(path, contents); sb.AppendLine("removed signature '" + text2 + "'"); } Config.LoadLists(); PushToAll(); Discord.PostLifecycle("koumodgp: signature list edited by admin (" + text + " " + text2 + ")."); } private static void KickBan(StringBuilder sb, string[] a, bool ban) { if (a.Length < 3) { sb.AppendLine("usage: kougp " + (ban ? "ban" : "kick") + " <player name>"); return; } string text = string.Join(" ", a.Skip(2)); ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { sb.AppendLine("no ZNet."); } else if (ban) { instance.Ban(text); sb.AppendLine("banned " + text + " (bannedlist.txt)."); Discord.PostLifecycle("koumodgp: admin banned " + text + "."); } else { instance.Kick(text); sb.AppendLine("kicked " + text + "."); Discord.PostLifecycle("koumodgp: admin kicked " + text + "."); } } private static void PushToAll() { foreach (Session item in Sessions.Snapshot()) { ServerModule.PushConfig(item.PeerUid, item.SessionKey); } } } internal static class AntiAnimCancel { [HarmonyPatch(typeof(Player), "StartEmote")] internal static class Player_StartEmote_Patch { private static bool Prefix(Player __instance, ref bool __result) { if (!Enabled) { return true; } if ((Object)(object)__instance == (Object)null || (Object)(object)__instance != (Object)(object)Player.m_localPlayer) { return true; } if (!ShouldBlockEmote(__instance)) { return true; } __result = false; return false; } } internal static bool Enabled = true; internal static float RecoveryWindow = 0.75f; private static readonly FieldInfo _currentAttack = AccessTools.Field(typeof(Humanoid), "m_currentAttack"); private static bool AttackButtonDown() { try { return ZInput.GetButton("Attack") || ZInput.GetButton("JoyAttack") || ZInput.GetButton("SecondaryAttack") || ZInput.GetButton("JoySecondaryAttack"); } catch { return false; } } internal static bool ShouldBlockEmote(Player p) { if ((Object)(object)p == (Object)null) { return false; } if (AttackButtonDown()) { return true; } if (((Character)p).InAttack()) { return true; } object? obj = _currentAttack?.GetValue(p); Attack val = (Attack)((obj is Attack) ? obj : null); if (val == null) { return false; } if (!val.IsDone()) { return true; } return ((Character)p).GetTimeSinceLastAttack() < RecoveryWindow; } } internal static class ClientModule { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleEvent <>9__6_0; internal void <RegisterInGameCommand>b__6_0(ConsoleEventArgs args) { Rpc.SendToServer(Protocol.MsgType.AdminCmd, new <>c__DisplayClass6_0 { cmd = ((args.Args.Length > 1) ? string.Join(" ", args.Args.Skip(1)) : "status") }.<RegisterInGameCommand>b__1, SessionKey); Terminal context = args.Context; if (context != null) { context.AddString("koumodgp: sent to server…"); } } } [CompilerGenerated] private sealed class <>c__DisplayClass6_0 { public string cmd; internal void <RegisterInGameCommand>b__1(ZPackage pkg) { pkg.Write(cmd); } } internal static volatile byte[] SessionKey; private static int _counter; private static float _lastBeat; private const float BeatInterval = 10f; private static bool _cmdRegistered; internal static void Init() { SessionKey = null; _counter = 0; _lastBeat = 0f; RegisterInGameCommand(); Plugin.Log.LogInfo((object)"[koumodgp] client module active — awaiting challenge."); } private static void RegisterInGameCommand() { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Expected O, but got Unknown if (_cmdRegistered) { return; } try { object obj = <>c.<>9__6_0; if (obj == null) { ConsoleEvent val = delegate(ConsoleEventArgs args) { string cmd = ((args.Args.Length > 1) ? string.Join(" ", args.Args.Skip(1)) : "status"); Rpc.SendToServer(Protocol.MsgType.AdminCmd, delegate(ZPackage pkg) { pkg.Write(cmd); }, SessionKey); Terminal context = args.Context; if (context != null) { context.AddString("koumodgp: sent to server…"); } }; <>c.<>9__6_0 = val; obj = (object)val; } new ConsoleCommand("kougp", "koumodgp admin (sent to server)", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, true); _cmdRegistered = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] client command register failed: " + ex.Message)); } } internal static void OnNonce(ZPackage inner) { try { byte[] nonce = inner.ReadByteArray(); byte[] salt = inner.ReadByteArray(); string identity = inner.ReadString(); SessionKey = Crypto.DeriveSessionKey(nonce, salt, identity); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] bad nonce: " + ex.Message)); return; } List<Verdict.Finding> findings = new List<Verdict.Finding>(); RunScans(findings); byte[] selfHash = SelfHash.Compute(); Dictionary<string, string> plugins = CollectPlugins(); Rpc.SendToServer(Protocol.MsgType.Manifest, delegate(ZPackage pkg) { pkg.Write(selfHash); pkg.Write(plugins.Count); foreach (KeyValuePair<string, string> item in plugins) { pkg.Write(item.Key); pkg.Write(item.Value); } WriteFindings(pkg, findings); }, SessionKey); _lastBeat = Time.time; Plugin.Log.LogInfo((object)string.Format("[{0}] manifest sent ({1} mods, {2} findings).", "koumodgp", plugins.Count, findings.Count)); } internal static void OnConfigPush(ZPackage inner) { try { int num = inner.ReadInt(); string[] array = new string[num]; for (int i = 0; i < num; i++) { array[i] = inner.ReadString(); } ScanProcess.Signatures = array; AntiAnimCancel.Enabled = inner.ReadBool(); AntiAnimCancel.RecoveryWindow = inner.ReadSingle(); Plugin.Log.LogInfo((object)(string.Format("[{0}] received {1} signatures; ", "koumodgp", num) + string.Format("anti-anim-cancel={0} ({1:0.##}s).", AntiAnimCancel.Enabled ? "on" : "off", AntiAnimCancel.RecoveryWindow))); } catch { } } internal static void Tick() { if (SessionKey != null && !(Time.time - _lastBeat < 10f)) { _lastBeat = Time.time; List<Verdict.Finding> findings = new List<Verdict.Finding>(); RunScans(findings); int counter = ++_counter; Rpc.SendToServer(Protocol.MsgType.Heartbeat, delegate(ZPackage pkg) { pkg.Write(counter); WriteFindings(pkg, findings); }, SessionKey); } } private static void RunScans(List<Verdict.Finding> findings) { ScanAssembly.Collect(findings); ScanState.Collect(findings); ScanProcess.Collect(findings); } private static Dictionary<string, string> CollectPlugins() { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); try { foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; BepInPlugin val = ((value != null) ? value.Metadata : null); if (val != null) { dictionary[val.GUID] = val.Version?.ToString() ?? "0"; } } } catch { } return dictionary; } private static void WriteFindings(ZPackage pkg, List<Verdict.Finding> findings) { pkg.Write(findings.Count); foreach (Verdict.Finding finding in findings) { pkg.Write((byte)finding.Cat); pkg.Write(finding.Detail ?? ""); } } } internal static class Config { internal enum Action { Log, Warn, Flag, Kick, Ban } internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<int> HandshakeTimeoutSeconds; internal static ConfigEntry<int> HeartbeatIntervalSeconds; internal static ConfigEntry<int> MaxMissedHeartbeats; internal static ConfigEntry<bool> ExemptAdmins; internal static ConfigEntry<bool> AutoBanDetections; internal static ConfigEntry<bool> PreventAnimationCancel; internal static ConfigEntry<float> AnimCancelRecoverySeconds; internal static ConfigEntry<Action> ActMissingCompanion; internal static ConfigEntry<Action> ActModMismatch; internal static ConfigEntry<Action> ActInjectedAssembly; internal static ConfigEntry<Action> ActForeignHarmony; internal static ConfigEntry<Action> ActStateAnomaly; internal static ConfigEntry<Action> ActProcessMatch; internal static ConfigEntry<Action> ActHeartbeatFail; internal static ConfigEntry<string> DiscordWebhookUrl; internal static ConfigEntry<bool> DiscordPostLifecycle; internal static readonly Dictionary<string, string> AllowedMods = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); internal static readonly Dictionary<string, string> OptionalMods = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); internal static readonly List<string> Signatures = new List<string>(); private static string _cfgDir; internal static void Init(ConfigFile cfg, string configDir) { _cfgDir = configDir; Enabled = cfg.Bind<bool>("General", "Enabled", true, "Master switch."); HandshakeTimeoutSeconds = cfg.Bind<int>("General", "HandshakeTimeoutSeconds", 20, "Seconds a joining client has to complete the handshake before being kicked."); HeartbeatIntervalSeconds = cfg.Bind<int>("General", "HeartbeatIntervalSeconds", 15, "How often clients send a signed heartbeat."); MaxMissedHeartbeats = cfg.Bind<int>("General", "MaxMissedHeartbeats", 2, "Consecutive missed heartbeats tolerated before kick."); ExemptAdmins = cfg.Bind<bool>("General", "ExemptAdmins", true, "Admins (adminlist.txt) are fully exempt and silent — never checked or logged."); AutoBanDetections = cfg.Bind<bool>("General", "AutoBanDetections", false, "When true, a client caught actively cheating — an internal OR external cheat: injected assembly, foreign Harmony patch, impossible game state, or a known cheat-program signature — is BANNED (added to bannedlist.txt) instead of only kicked. Compliance-only issues (mod mismatch, missing companion, heartbeat failure) are never auto-banned. Default false = kick only."); PreventAnimationCancel = cfg.Bind<bool>("Combat", "PreventAnimationCancel", true, "Prevent attack animation-cancelling (e.g. binding an emote to the attack key to interrupt a swing's recovery and attack faster). When true, clients running koumodgp cannot start an emote during an attack or its short recovery window. Pushed to clients on connect. Enabled by default."); AnimCancelRecoverySeconds = cfg.Bind<float>("Combat", "AnimCancelRecoverySeconds", 0.75f, "Length of the post-swing recovery window (seconds) during which emotes stay blocked — the animation-cancel window. Larger = stricter, but may briefly delay an emote used right after combat. Default 0.75."); ActMissingCompanion = cfg.Bind<Action>("Policy", "MissingCompanion", Action.Kick, "Client never completed the handshake (vanilla or stripped companion)."); ActModMismatch = cfg.Bind<Action>("Policy", "ModMismatch", Action.Kick, "Client mod list does not match the allowed list."); ActInjectedAssembly = cfg.Bind<Action>("Policy", "InjectedAssembly", Action.Flag, "An in-memory assembly not owned by any installed mod was found. Advisory only by default (Flag = log to violations.log, no kick): in-memory assemblies have many legitimate causes (embedded mod dependencies, runtime-generated code). Injected tools that actually patch the game are caught by ForeignHarmony instead. Set to Kick to enforce."); ActForeignHarmony = cfg.Bind<Action>("Policy", "ForeignHarmony", Action.Kick, "A Harmony patch from a non-whitelisted owner id was found."); ActStateAnomaly = cfg.Bind<Action>("Policy", "StateAnomaly", Action.Kick, "A monitored game field held an impossible value (trainer edit)."); ActProcessMatch = cfg.Bind<Action>("Policy", "ProcessMatch", Action.Kick, "A known cheat-program signature (process/window) matched. Kick on its own, even if open for another game."); ActHeartbeatFail = cfg.Bind<Action>("Policy", "HeartbeatFail", Action.Kick, "Heartbeats stopped, went out of order, or failed their signature."); DiscordWebhookUrl = cfg.Bind<string>("Logging", "DiscordWebhookUrl", "", "Discord webhook URL. Empty = disabled. Detection posts are generic ('External violation detected')."); DiscordPostLifecycle = cfg.Bind<bool>("Logging", "DiscordPostLifecycleEvents", true, "Also post server start/stop, config reloads and manual admin actions."); LoadLists(); } internal static void LoadLists() { AllowedMods.Clear(); OptionalMods.Clear(); Signatures.Clear(); string path = Path.Combine(_cfgDir, "koumodgp", "allowed_mods.txt"); string path2 = Path.Combine(_cfgDir, "koumodgp", "signatures.txt"); Directory.CreateDirectory(Path.GetDirectoryName(path)); if (!File.Exists(path)) { File.WriteAllText(path, "# koumodgp allowed mods — one per line: guid=version (or guid=* for any version)\n# Required on every client (missing => kick). Prefix with '?' to make it OPTIONAL\n# (allowed if present, version checked, but not required): ?guid=version\n# example:\n# com.modbykou.koumodgp=0.5.0\n# ?some.clientonly.mod=*\n"); } if (!File.Exists(path2)) { File.WriteAllText(path2, "# koumodgp signatures — process names or window-title fragments, one per line.\n# example:\n# wemod\n# cheat engine\n"); } string[] array = File.ReadAllLines(path); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0 || text.StartsWith("#")) { continue; } bool flag = text.StartsWith("?"); if (flag) { text = text.Substring(1).Trim(); } int num = text.IndexOf('='); if (num <= 0) { continue; } string text2 = text.Substring(0, num).Trim(); string value = text.Substring(num + 1).Trim(); if (text2.Length != 0) { if (flag) { OptionalMods[text2] = value; } else { AllowedMods[text2] = value; } } } array = File.ReadAllLines(path2); for (int i = 0; i < array.Length; i++) { string text3 = array[i].Trim(); if (text3.Length != 0 && !text3.StartsWith("#")) { Signatures.Add(text3.ToLowerInvariant()); } } LoadModFolder(Path.Combine(_cfgDir, "koumodgp", "required"), AllowedMods); LoadModFolder(Path.Combine(_cfgDir, "koumodgp", "allowed"), OptionalMods); Plugin.Log.LogInfo((object)string.Format("[{0}] lists loaded: {1} required, {2} optional, {3} signatures.", "koumodgp", AllowedMods.Count, OptionalMods.Count, Signatures.Count)); } private static void LoadModFolder(string folder, Dictionary<string, string> target) { try { Directory.CreateDirectory(folder); string path = Path.Combine(folder, "_README.txt"); if (!File.Exists(path)) { File.WriteAllText(path, "Paste mod .dll files here. koumodgp reads each mod's id and version\nautomatically — nothing else to configure.\n"); } string[] files = Directory.GetFiles(folder, "*.dll", SearchOption.AllDirectories); for (int i = 0; i < files.Length; i++) { var (text, text2) = ReadPluginInfo(files[i]); if (!string.IsNullOrEmpty(text)) { target[text] = text2 ?? "*"; } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] mod folder '" + folder + "' read error: " + ex.Message)); } } private static (string guid, string version) ReadPluginInfo(string dllPath) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) try { AssemblyDefinition val = AssemblyDefinition.ReadAssembly(dllPath); try { Enumerator<TypeDefinition> enumerator = val.MainModule.Types.GetEnumerator(); try { while (enumerator.MoveNext()) { Enumerator<CustomAttribute> enumerator2 = enumerator.Current.CustomAttributes.GetEnumerator(); try { while (enumerator2.MoveNext()) { CustomAttribute current = enumerator2.Current; if (!(((MemberReference)current.AttributeType).Name != "BepInPlugin") && current.ConstructorArguments.Count >= 3) { CustomAttributeArgument val2 = current.ConstructorArguments[0]; string item = ((CustomAttributeArgument)(ref val2)).Value as string; val2 = current.ConstructorArguments[2]; string item2 = ((CustomAttributeArgument)(ref val2)).Value as string; return (guid: item, version: item2); } } } finally { ((IDisposable)enumerator2/*cast due to .constrained prefix*/).Dispose(); } } } finally { ((IDisposable)enumerator/*cast due to .constrained prefix*/).Dispose(); } } finally { ((IDisposable)val)?.Dispose(); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] could not read '" + Path.GetFileName(dllPath) + "': " + ex.Message)); } return (guid: null, version: null); } } internal static class Crypto { private static readonly byte[] SharedSecret = Encoding.UTF8.GetBytes("kou-gp-shared-v1-8f2a1c9e7b40d5a3"); internal const int TagLen = 32; internal static byte[] Hmac(byte[] data, byte[] key = null) { using HMACSHA256 hMACSHA = new HMACSHA256(key ?? SharedSecret); return hMACSHA.ComputeHash(data); } internal static byte[] DeriveSessionKey(byte[] nonce, byte[] salt, string identity) { byte[] bytes = Encoding.UTF8.GetBytes(identity ?? ""); byte[] array = new byte[((nonce != null) ? nonce.Length : 0) + ((salt != null) ? salt.Length : 0) + bytes.Length]; int num = 0; if (nonce != null) { Buffer.BlockCopy(nonce, 0, array, num, nonce.Length); num += nonce.Length; } if (salt != null) { Buffer.BlockCopy(salt, 0, array, num, salt.Length); num += salt.Length; } Buffer.BlockCopy(bytes, 0, array, num, bytes.Length); return Hmac(array); } internal static byte[] RandomBytes(int n) { byte[] array = new byte[n]; using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create(); randomNumberGenerator.GetBytes(array); return array; } internal static bool FixedTimeEquals(byte[] a, byte[] b) { if (a == null || b == null || a.Length != b.Length) { return false; } int num = 0; for (int i = 0; i < a.Length; i++) { num |= a[i] ^ b[i]; } return num == 0; } } internal static class Discord { private static readonly BlockingCollection<string> _queue = new BlockingCollection<string>(); private static Thread _worker; private static volatile bool _running; internal static void Init() { if (_running) { return; } _running = true; _worker = new Thread(Drain) { IsBackground = true, Name = "koumodgp-webhook" }; _worker.Start(); try { ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; } catch { } } internal static void Shutdown() { _running = false; try { _queue.CompleteAdding(); } catch { } } internal static void PostDetection(long ts, string playerName, string steamId, string type, string detail) { string text = "Player: `" + Escape(playerName) + "`\nID: `" + Escape(steamId) + "`\nTime: " + Stamp(ts); string text2 = ((type == "ModMismatch") ? Escape(detail) : "using external/internal cheat"); Post("**The player has been kicked due to:** " + text2 + "\n" + text); } internal static void PostAudit(long ts, string playerName, string steamId, string type, string detail, string actionWord) { Post("**koumodgp — " + Escape(type) + "** (" + Escape(actionWord) + ")\nPlayer: `" + Escape(playerName) + "`\nID: `" + Escape(steamId) + "`\nDetail: " + Escape(detail) + "\nTime: " + Stamp(ts)); } private static string Stamp(long unixSeconds) { return $"<t:{unixSeconds}:F> (<t:{unixSeconds}:R>)"; } internal static void PostLifecycle(string message) { if (Config.DiscordPostLifecycle != null && Config.DiscordPostLifecycle.Value) { Post(message); } } private static void Post(string content) { if (string.IsNullOrEmpty(CleanUrl())) { return; } if (!_running) { Init(); } try { _queue.Add(content); } catch { } } private static string CleanUrl() { string text = Config.DiscordWebhookUrl?.Value; if (string.IsNullOrEmpty(text)) { return null; } text = text.Trim().Trim('"', '\'', '<', '>'); if (!text.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && !text.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { return null; } return text; } private static void Drain() { foreach (string item in _queue.GetConsumingEnumerable()) { string text = CleanUrl(); if (text == null) { continue; } for (int i = 0; i < 2; i++) { if (TrySend(text, item)) { break; } Thread.Sleep(1000); } } } private static bool TrySend(string url, string content) { try { Uri uri = new Uri(url); string s = "{\"content\":\"" + JsonEscape(content) + "\"}"; byte[] bytes = Encoding.UTF8.GetBytes(s); using TcpClient tcpClient = new TcpClient(); tcpClient.Connect(uri.Host, (uri.Port == -1) ? 443 : uri.Port); using SslStream sslStream = new SslStream(tcpClient.GetStream(), leaveInnerStreamOpen: false, (object obj, X509Certificate c, X509Chain ch, SslPolicyErrors e) => true); sslStream.AuthenticateAsClient(uri.Host, null, SslProtocols.Tls12, checkCertificateRevocation: false); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("POST ").Append(uri.PathAndQuery).Append(" HTTP/1.1\r\n"); stringBuilder.Append("Host: ").Append(uri.Host).Append("\r\n"); stringBuilder.Append("User-Agent: koumodgp\r\n"); stringBuilder.Append("Content-Type: application/json\r\n"); stringBuilder.Append("Content-Length: ").Append(bytes.Length).Append("\r\n"); stringBuilder.Append("Connection: close\r\n\r\n"); byte[] bytes2 = Encoding.ASCII.GetBytes(stringBuilder.ToString()); sslStream.Write(bytes2, 0, bytes2.Length); sslStream.Write(bytes, 0, bytes.Length); sslStream.Flush(); using StreamReader streamReader = new StreamReader(sslStream); string text = streamReader.ReadLine(); int num; if (text != null) { if (!text.Contains(" 200")) { num = (text.Contains(" 204") ? 1 : 0); if (num == 0) { goto IL_0190; } } else { num = 1; } goto IL_01a6; } num = 0; goto IL_0190; IL_0190: Plugin.Log.LogWarning((object)("[koumodgp] webhook status: " + text)); goto IL_01a6; IL_01a6: return (byte)num != 0; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] webhook post failed: " + ex.Message)); return false; } } private static string Escape(string s) { return (s ?? "").Replace("`", "'"); } private static string JsonEscape(string s) { return (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n") .Replace("\r", ""); } } [BepInPlugin("com.modbykou.koumodgp", "koumodgp", "0.5.0")] public class Plugin : BaseUnityPlugin { internal enum Role { Unknown, Server, Client } [HarmonyPatch(typeof(ZNet), "Awake")] private static class ZNet_Awake_Patch { private static void Postfix(ZNet __instance) { bool num = __instance.IsServer(); CurrentRole = (num ? Role.Server : Role.Client); Rpc.Reset(); Rpc.Register(); Log.LogInfo((object)(string.Format("[{0}] role resolved: {1} ", "koumodgp", CurrentRole) + $"(dedicated={__instance.IsDedicated()}).")); if (num) { ServerModule.Init(); Admin.Register(Paths.ConfigPath); } else { ClientModule.Init(); } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private static class ZNet_RPC_PeerInfo_Patch { private static void Postfix(ZNet __instance, ZRpc rpc) { if (CurrentRole != Role.Server || (Object)(object)__instance == (Object)null || rpc == null) { return; } ZNetPeer val = null; foreach (ZNetPeer peer in __instance.GetPeers()) { if (peer != null && peer.m_rpc == rpc) { val = peer; break; } } if (val != null && val.m_uid != 0L) { ServerModule.OnPeerJoined(__instance, val); } } } internal static ManualLogSource Log; private Harmony _harmony; internal static Role CurrentRole; private void Awake() { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; string configPath = Paths.ConfigPath; Config.Init(((BaseUnityPlugin)this).Config, configPath); ViolationLog.Init(configPath); Discord.Init(); _harmony = new Harmony("com.modbykou.koumodgp"); _harmony.PatchAll(typeof(Plugin).Assembly); Log.LogInfo((object)string.Format("[{0}] v{1} loaded (wire {2}) — awaiting role.", "koumodgp", "0.5.0", 2)); } private void OnDestroy() { Discord.Shutdown(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private void Update() { if (CurrentRole == Role.Server) { ServerModule.Tick(ZNet.instance); } else if (CurrentRole == Role.Client) { ClientModule.Tick(); } } } public static class Protocol { public enum MsgType : byte { Ping, Nonce, Manifest, Heartbeat, ConfigPush, Verdict, AdminCmd, AdminReply } public enum FindingCat : byte { InjectedAssembly, ForeignHarmony, StateAnomaly, ProcessMatch } public const string PluginGuid = "com.modbykou.koumodgp"; public const string PluginName = "koumodgp"; public const string PluginVersion = "0.5.0"; public const string RpcClientToServer = "kou_gpsync_a"; public const string RpcServerToClient = "kou_gpsync_b"; public const int WireVersion = 2; } internal static class Rpc { private static bool s_registered; private static readonly MethodInfo _getServerPeerID = AccessTools.Method(typeof(ZRoutedRpc), "GetServerPeerID", (Type[])null, (Type[])null); internal static void Register() { if (!s_registered && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.Register<ZPackage>("kou_gpsync_a", (Action<long, ZPackage>)OnClientToServer); ZRoutedRpc.instance.Register<ZPackage>("kou_gpsync_b", (Action<long, ZPackage>)OnServerToClient); s_registered = true; Plugin.Log.LogInfo((object)"[koumodgp] transport registered (kou_gpsync_a, kou_gpsync_b)."); } } internal static void Reset() { s_registered = false; } internal static void SendToServer(Protocol.MsgType type, Action<ZPackage> writeBody, byte[] key = null) { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null && !(_getServerPeerID == null)) { long num = (long)_getServerPeerID.Invoke(instance, null); ZPackage val = Build(type, writeBody, key); instance.InvokeRoutedRPC(num, "kou_gpsync_a", new object[1] { val }); } } internal static void SendToClient(long peerUid, Protocol.MsgType type, Action<ZPackage> writeBody, byte[] key = null) { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null && peerUid != 0L) { ZPackage val = Build(type, writeBody, key); instance.InvokeRoutedRPC(peerUid, "kou_gpsync_b", new object[1] { val }); } } private static ZPackage Build(Protocol.MsgType type, Action<ZPackage> writeBody, byte[] key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write((byte)2); val.Write((byte)type); writeBody?.Invoke(val); byte[] array = val.GetArray(); ZPackage val2 = new ZPackage(); val2.Write(array); val2.Write(Crypto.Hmac(array, key)); return val2; } private static ZPackage Open(ZPackage pkg, out Protocol.MsgType type, byte[] key) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown type = Protocol.MsgType.Ping; if (pkg == null) { return null; } byte[] array; byte[] a; try { array = pkg.ReadByteArray(); a = pkg.ReadByteArray(); } catch { return null; } if (!Crypto.FixedTimeEquals(a, Crypto.Hmac(array, key))) { Plugin.Log.LogWarning((object)"[koumodgp] dropped packet with bad tag."); return null; } ZPackage val = new ZPackage(array); try { int num = val.ReadByte(); if (num != 2) { Plugin.Log.LogWarning((object)string.Format("[{0}] wire mismatch (got {1}, want {2}) — ignored.", "koumodgp", num, 2)); return null; } type = (Protocol.MsgType)val.ReadByte(); return val; } catch { return null; } } private static void OnClientToServer(long sender, ZPackage pkg) { if (Plugin.CurrentRole != Plugin.Role.Server) { return; } byte[] key = Sessions.KeyFor(sender); Protocol.MsgType type; ZPackage val = Open(pkg, out type, key); if (val != null) { switch (type) { case Protocol.MsgType.Ping: Plugin.Log.LogInfo((object)string.Format("[{0}] server: ping reply from peer {1}. Round-trip OK.", "koumodgp", sender)); break; case Protocol.MsgType.Manifest: ServerModule.OnManifest(sender, val); break; case Protocol.MsgType.Heartbeat: ServerModule.OnHeartbeat(sender, val); break; case Protocol.MsgType.AdminCmd: HandleAdminCmd(sender, val); break; case Protocol.MsgType.Nonce: case Protocol.MsgType.ConfigPush: case Protocol.MsgType.Verdict: break; } } } private static void HandleAdminCmd(long sender, ZPackage inner) { string text; try { text = inner.ReadString(); } catch { return; } ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetPeer(sender) : null); string text2 = null; try { object obj2; if (val == null) { obj2 = null; } else { ISocket socket = val.m_socket; obj2 = ((socket != null) ? socket.GetHostName() : null); } text2 = (string)obj2; } catch { } if ((Object)(object)instance == (Object)null || string.IsNullOrEmpty(text2) || !instance.IsAdmin(text2)) { Plugin.Log.LogWarning((object)string.Format("[{0}] rejected admin command from non-admin {1}.", "koumodgp", sender)); return; } string[] a = ("kougp " + text).Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); string reply = Admin.Execute(a); byte[] key = Sessions.KeyFor(sender); SendToClient(sender, Protocol.MsgType.AdminReply, delegate(ZPackage pkg) { pkg.Write(reply ?? ""); }, key); } private static void OnServerToClient(long sender, ZPackage pkg) { if (Plugin.CurrentRole != Plugin.Role.Client) { return; } byte[] sessionKey = ClientModule.SessionKey; Protocol.MsgType type; ZPackage val = Open(pkg, out type, sessionKey); if (val == null) { return; } switch (type) { case Protocol.MsgType.Ping: Plugin.Log.LogInfo((object)"[koumodgp] client: ping from server. Replying."); SendToServer(Protocol.MsgType.Ping, null, ClientModule.SessionKey); break; case Protocol.MsgType.Nonce: ClientModule.OnNonce(val); break; case Protocol.MsgType.ConfigPush: ClientModule.OnConfigPush(val); break; case Protocol.MsgType.AdminReply: try { Console instance = Console.instance; if (instance != null) { ((Terminal)instance).AddString(val.ReadString()); } break; } catch { break; } } } } internal static class ScanAssembly { private static readonly string[] GoodPrefixes = new string[34] { "mscorlib", "System", "netstandard", "Microsoft.", "UnityEngine", "Unity.", "BepInEx", "0Harmony", "HarmonyX", "MonoMod", "Mono.", "assembly_valheim", "assembly_utils", "assembly_googleanalytics", "assembly_postprocessing", "assembly_sunshafts", "assembly_guiutils", "gui_framework", "SoftReferenceableAssets", "Splatform", "PlayFab", "com.rlabrecque.steamworks.net", "steamworks", "ConnectorSteamworks", "Newtonsoft.Json", "YamlDotNet", "ServerSync", "Jotunn", "SoftReference", "koumodgp", "Anonymously Hosted", "MMHOOK", "DOTween", "Splatform" }; private static HashSet<string> PluginAssemblyNames() { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); try { foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; BaseUnityPlugin val = ((value != null) ? value.Instance : null); if ((Object)(object)val != (Object)null) { hashSet.Add(((object)val).GetType().Assembly.GetName().Name); } } } catch { } return hashSet; } private static HashSet<string> TrustedAssemblyNames() { HashSet<string> hashSet = PluginAssemblyNames(); try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (assembly.IsDynamic) { continue; } string value; try { value = assembly.Location; } catch { value = null; } if (string.IsNullOrEmpty(value)) { continue; } try { AssemblyName[] referencedAssemblies = assembly.GetReferencedAssemblies(); foreach (AssemblyName assemblyName in referencedAssemblies) { hashSet.Add(assemblyName.Name); } } catch { } } } catch { } return hashSet; } internal static void Collect(List<Verdict.Finding> outList) { CollectForeignHarmony(outList); CollectInjectedAssemblies(outList); } internal static string DescribeOwner(string asmName) { if (string.IsNullOrEmpty(asmName)) { return "unknown"; } try { foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; BaseUnityPlugin val = ((value != null) ? value.Instance : null); if (!((Object)(object)val == (Object)null) && string.Equals(((object)val).GetType().Assembly.GetName().Name, asmName, StringComparison.OrdinalIgnoreCase)) { string[] obj = new string[5] { "mod '", null, null, null, null }; BepInPlugin metadata = pluginInfo.Value.Metadata; obj[1] = ((metadata != null) ? metadata.Name : null) ?? asmName; obj[2] = "' ("; obj[3] = pluginInfo.Key; obj[4] = ")"; return string.Concat(obj); } } foreach (KeyValuePair<string, PluginInfo> pluginInfo2 in Chainloader.PluginInfos) { PluginInfo value2 = pluginInfo2.Value; BaseUnityPlugin val2 = ((value2 != null) ? value2.Instance : null); if ((Object)(object)val2 == (Object)null) { continue; } try { AssemblyName[] referencedAssemblies = ((object)val2).GetType().Assembly.GetReferencedAssemblies(); for (int i = 0; i < referencedAssemblies.Length; i++) { if (string.Equals(referencedAssemblies[i].Name, asmName, StringComparison.OrdinalIgnoreCase)) { string[] obj2 = new string[5] { "dependency of mod '", null, null, null, null }; BepInPlugin metadata2 = pluginInfo2.Value.Metadata; obj2[1] = ((metadata2 != null) ? metadata2.Name : null) ?? pluginInfo2.Key; obj2[2] = "' ("; obj2[3] = pluginInfo2.Key; obj2[4] = ")"; return string.Concat(obj2); } } } catch { } } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (!assembly.IsDynamic && string.Equals(assembly.GetName().Name, asmName, StringComparison.OrdinalIgnoreCase)) { string text = null; try { text = assembly.Location; } catch { } return string.IsNullOrEmpty(text) ? "not owned by any installed mod" : ("file '" + Path.GetFileName(text) + "'"); } } } catch { } return "not owned by any installed mod"; } private static void CollectForeignHarmony(List<Verdict.Finding> outList) { try { HashSet<string> plugins = AllowedAssemblies(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (MethodBase allPatchedMethod in Harmony.GetAllPatchedMethods()) { Patches patchInfo = Harmony.GetPatchInfo(allPatchedMethod); if (patchInfo == null) { continue; } foreach (Patch item in AllPatches(patchInfo)) { string text = PatchAssembly(item); if (text != null && !IsAllowed(text, plugins) && hashSet.Add(text)) { outList.Add(new Verdict.Finding { Cat = Protocol.FindingCat.ForeignHarmony, Detail = "Harmony patch from '" + text + "' — " + DescribeOwner(text) }); } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] harmony scan error: " + ex.Message)); } } private static IEnumerable<Patch> AllPatches(Patches info) { foreach (Patch prefix in info.Prefixes) { yield return prefix; } foreach (Patch postfix in info.Postfixes) { yield return postfix; } foreach (Patch transpiler in info.Transpilers) { yield return transpiler; } foreach (Patch finalizer in info.Finalizers) { yield return finalizer; } } private static string PatchAssembly(Patch p) { try { return p.PatchMethod?.DeclaringType?.Assembly?.GetName().Name; } catch { return null; } } private static HashSet<string> AllowedAssemblies() { HashSet<string> hashSet = TrustedAssemblyNames(); hashSet.Add("0Harmony"); hashSet.Add("HarmonyX"); hashSet.Add("MonoMod.RuntimeDetour"); return hashSet; } private static bool IsAllowed(string name, HashSet<string> plugins) { if (plugins.Contains(name)) { return true; } return GoodPrefixes.Any((string pre) => name.StartsWith(pre, StringComparison.OrdinalIgnoreCase)); } private static void CollectInjectedAssemblies(List<Verdict.Finding> outList) { try { HashSet<string> plugins = TrustedAssemblyNames(); Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (assembly.IsDynamic) { continue; } string name = assembly.GetName().Name; if (!string.IsNullOrEmpty(name) && !IsGac(assembly) && !IsAllowed(name, plugins)) { string value; try { value = assembly.Location; } catch { value = null; } if (string.IsNullOrEmpty(value)) { outList.Add(new Verdict.Finding { Cat = Protocol.FindingCat.InjectedAssembly, Detail = "in-memory assembly '" + name + "' — " + DescribeOwner(name) }); } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] assembly scan error: " + ex.Message)); } } private static bool IsGac(Assembly a) { try { return a.GlobalAssemblyCache; } catch { return false; } } } internal static class ScanProcess { private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); internal static volatile string[] Signatures = new string[0]; internal static void Collect(List<Verdict.Finding> outList) { string[] signatures = Signatures; if (signatures == null || signatures.Length == 0) { return; } HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); CollectProcessNames(signatures, hashSet); CollectWindowTitles(signatures, hashSet); foreach (string item in hashSet) { outList.Add(new Verdict.Finding { Cat = Protocol.FindingCat.ProcessMatch, Detail = "sig '" + item + "'" }); } } private static void CollectProcessNames(string[] sigs, HashSet<string> matched) { try { Process[] processes = Process.GetProcesses(); foreach (Process process in processes) { string text; try { text = process.ProcessName?.ToLowerInvariant() ?? ""; } catch { continue; } finally { try { process.Dispose(); } catch { } } foreach (string text2 in sigs) { if (text.Length > 0 && text.Contains(text2)) { matched.Add(text2); } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] process scan error: " + ex.Message)); } } [DllImport("user32.dll")] private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); [DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] private static extern int GetWindowTextLength(IntPtr hWnd); private static void CollectWindowTitles(string[] sigs, HashSet<string> matched) { try { EnumWindows(delegate(IntPtr h, IntPtr l) { if (!IsWindowVisible(h)) { return true; } int windowTextLength = GetWindowTextLength(h); if (windowTextLength <= 0) { return true; } StringBuilder stringBuilder = new StringBuilder(windowTextLength + 1); GetWindowText(h, stringBuilder, stringBuilder.Capacity); string text = stringBuilder.ToString().ToLowerInvariant(); if (text.Length == 0) { return true; } string[] array = sigs; foreach (string text2 in array) { if (text.Contains(text2)) { matched.Add(text2); } } return true; }, IntPtr.Zero); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] window scan error: " + ex.Message)); } } } internal static class ScanState { private static readonly FieldInfo fGod = AccessTools.Field(typeof(Player), "m_godMode"); private static readonly FieldInfo fGhost = AccessTools.Field(typeof(Player), "m_ghostMode"); private static readonly FieldInfo fDebugFly = AccessTools.Field(typeof(Player), "m_debugFly"); private static readonly FieldInfo fDebug = AccessTools.Field(typeof(Player), "m_debugMode"); private static readonly FieldInfo fNoCost = AccessTools.Field(typeof(Player), "m_noPlacementCost"); private static readonly FieldInfo fFlying = AccessTools.Field(typeof(Character), "m_flying"); internal static void Collect(List<Verdict.Finding> outList) { Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null)) { Check(outList, fGod, localPlayer, "god mode"); Check(outList, fGhost, localPlayer, "ghost mode"); Check(outList, fDebugFly, localPlayer, "debug fly"); Check(outList, fDebug, localPlayer, "debug mode"); Check(outList, fNoCost, localPlayer, "no placement cost"); Check(outList, fFlying, localPlayer, "flying"); } } private static void Check(List<Verdict.Finding> outList, FieldInfo fi, object inst, string label) { if (fi == null) { return; } try { object value = fi.GetValue(inst); bool flag = default(bool); int num; if (value is bool) { flag = (bool)value; num = 1; } else { num = 0; } if (((uint)num & (flag ? 1u : 0u)) != 0) { outList.Add(new Verdict.Finding { Cat = Protocol.FindingCat.StateAnomaly, Detail = label }); } } catch { } } } internal static class SelfHash { private static byte[] _cached; internal static byte[] Compute() { if (_cached != null) { return _cached; } try { string location = Assembly.GetExecutingAssembly().Location; byte[] buffer = ((!string.IsNullOrEmpty(location) && File.Exists(location)) ? File.ReadAllBytes(location) : new byte[0]); using SHA256 sHA = SHA256.Create(); _cached = sHA.ComputeHash(buffer); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] self-hash failed: " + ex.Message)); _cached = new byte[0]; } return _cached; } } internal static class ServerModule { private static readonly MethodInfo _internalKick = AccessTools.Method(typeof(ZNet), "InternalKick", new Type[1] { typeof(ZNetPeer) }, (Type[])null); internal static void Init() { Plugin.Log.LogInfo((object)string.Format("[{0}] server module active. Allowed mods: {1}.", "koumodgp", Config.AllowedMods.Count)); Discord.PostLifecycle("koumodgp server online (v0.5.0)."); } internal static void OnPeerJoined(ZNet net, ZNetPeer peer) { if (!Config.Enabled.Value || peer == null || peer.m_uid == 0L) { return; } string text = SafeHostName(peer); if (Config.ExemptAdmins.Value && !string.IsNullOrEmpty(text) && net.IsAdmin(text)) { Plugin.Log.LogInfo((object)string.Format("[{0}] peer {1} is admin — exempt, silent.", "koumodgp", peer.m_uid)); return; } Session s = Sessions.Create(peer.m_uid); s.HostName = text; s.SteamId = SafeSteamId(peer); s.PlayerName = peer.m_playerName ?? ""; s.Nonce = Crypto.RandomBytes(16); s.Salt = Crypto.RandomBytes(16); s.JoinTime = Time.time; s.LastHeartbeat = Time.time; string identity = text ?? peer.m_uid.ToString(); s.SessionKey = Crypto.DeriveSessionKey(s.Nonce, s.Salt, identity); Rpc.SendToClient(peer.m_uid, Protocol.MsgType.Nonce, delegate(ZPackage pkg) { pkg.Write(s.Nonce); pkg.Write(s.Salt); pkg.Write(identity); }); Plugin.Log.LogInfo((object)string.Format("[{0}] peer {1} joined — challenge sent, awaiting manifest.", "koumodgp", peer.m_uid)); } internal static void OnManifest(long uid, ZPackage inner) { Session session = Sessions.Get(uid); if (session == null) { return; } Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); List<Verdict.Finding> list = new List<Verdict.Finding>(); try { inner.ReadByteArray(); int num = inner.ReadInt(); for (int i = 0; i < num; i++) { string key = inner.ReadString(); string value = inner.ReadString(); dictionary[key] = value; } ReadFindings(inner, list); } catch (Exception ex) { Plugin.Log.LogWarning((object)string.Format("[{0}] malformed manifest from {1}: {2}", "koumodgp", uid, ex.Message)); return; } session.Verified = true; session.LastHeartbeat = Time.time; Verdict.Result result = Verdict.Evaluate(dictionary, list); if (result.Action == Config.Action.Log || result.Type == null) { Plugin.Log.LogInfo((object)string.Format("[{0}] peer {1} ({2}) verified clean.", "koumodgp", uid, session.PlayerName)); PushConfig(uid, session.SessionKey); } else { Apply(session, result); } } internal static void OnHeartbeat(long uid, ZPackage inner) { Session session = Sessions.Get(uid); if (session == null) { return; } List<Verdict.Finding> list = new List<Verdict.Finding>(); int num; try { num = inner.ReadInt(); ReadFindings(inner, list); } catch { return; } if (num <= session.LastCounter) { Apply(session, new Verdict.Result { Type = "HeartbeatFail", Action = Config.ActHeartbeatFail.Value, Confidence = "single", Detail = $"counter regressed ({num} <= {session.LastCounter})" }); return; } session.LastCounter = num; session.LastHeartbeat = Time.time; session.MissedHeartbeats = 0; Verdict.Result result = Verdict.Evaluate(null, list); if (result.Type != null && result.Action != Config.Action.Log) { Apply(session, result); } } internal static void Tick(ZNet net) { if ((Object)(object)net == (Object)null) { return; } float time = Time.time; int value = Config.HandshakeTimeoutSeconds.Value; float num = Config.HeartbeatIntervalSeconds.Value * (Config.MaxMissedHeartbeats.Value + 1); HashSet<long> hashSet = new HashSet<long>(); foreach (ZNetPeer peer in net.GetPeers()) { if (peer != null) { hashSet.Add(peer.m_uid); } } foreach (Session item in Sessions.Snapshot()) { if (!hashSet.Contains(item.PeerUid)) { Sessions.Remove(item.PeerUid); } else if (!item.Verified && time - item.JoinTime > (float)value) { Apply(item, new Verdict.Result { Type = "MissingCompanion", Action = Config.ActMissingCompanion.Value, Confidence = "single", Detail = "no valid manifest before timeout" }); } else if (item.Verified && time - item.LastHeartbeat > num) { Apply(item, new Verdict.Result { Type = "HeartbeatFail", Action = Config.ActHeartbeatFail.Value, Confidence = "single", Detail = $"no heartbeat for {time - item.LastHeartbeat:F0}s" }); } } } internal static void PushConfig(long uid, byte[] key) { Rpc.SendToClient(uid, Protocol.MsgType.ConfigPush, delegate(ZPackage pkg) { pkg.Write(Config.Signatures.Count); foreach (string signature in Config.Signatures) { pkg.Write(signature); } pkg.Write(Config.PreventAnimationCancel.Value); pkg.Write(Config.AnimCancelRecoverySeconds.Value); }, key); } internal static void Apply(Session s, Verdict.Result v) { string utc = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"); long ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); ViolationLog.Write(utc, s.SteamId, s.PlayerName, v.Type, v.Confidence, v.Action.ToString(), v.Detail); bool flag = v.Type == "ForeignHarmony" || v.Type == "InjectedAssembly"; switch (v.Action) { case Config.Action.Log: break; case Config.Action.Warn: case Config.Action.Flag: if (s.AnnounceOnce(v.Type, v.Detail)) { Plugin.Log.LogWarning((object)("[koumodgp] flag " + s.PlayerName + " (" + s.SteamId + "): " + v.Type + " — " + v.Detail)); if (flag) { Discord.PostAudit(ts, s.PlayerName, s.SteamId, v.Type, v.Detail, "flagged for review"); } } break; case Config.Action.Kick: case Config.Action.Ban: { bool flag2 = Config.AutoBanDetections.Value && (v.Action == Config.Action.Ban || v.Detection); if (flag) { Discord.PostAudit(ts, s.PlayerName, s.SteamId, v.Type, v.Detail, flag2 ? "banned" : "kicked"); } else { Discord.PostDetection(ts, s.PlayerName, s.SteamId, v.Type, v.Detail); } if (flag2) { Plugin.Log.LogWarning((object)("[koumodgp] banning " + s.PlayerName + " (" + s.SteamId + "): " + v.Type + ".")); BanPeer(s); } else { Plugin.Log.LogWarning((object)("[koumodgp] kicking " + s.PlayerName + " (" + s.SteamId + "): " + v.Type + ".")); KickPeer(s.PeerUid); } Sessions.Remove(s.PeerUid); break; } } } private static void KickPeer(long uid) { ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null)) { ZNetPeer peer = instance.GetPeer(uid); if (peer != null && _internalKick != null) { _internalKick.Invoke(instance, new object[1] { peer }); } } } private static void BanPeer(Session s) { ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null)) { try { instance.Ban(s.SteamId); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] ban of " + s.PlayerName + " (" + s.SteamId + ") failed: " + ex.Message)); } KickPeer(s.PeerUid); } } private static void ReadFindings(ZPackage inner, List<Verdict.Finding> outList) { int num = inner.ReadInt(); for (int i = 0; i < num; i++) { Protocol.FindingCat cat = (Protocol.FindingCat)inner.ReadByte(); string detail = inner.ReadString(); outList.Add(new Verdict.Finding { Cat = cat, Detail = detail }); } } private static string SafeHostName(ZNetPeer peer) { try { object result; if (peer == null) { result = null; } else { ISocket socket = peer.m_socket; result = ((socket != null) ? socket.GetHostName() : null); } return (string)result; } catch { return null; } } private static string SafeSteamId(ZNetPeer peer) { return SafeHostName(peer) ?? peer?.m_uid.ToString() ?? "unknown"; } } internal class Session { internal long PeerUid; internal string HostName; internal string SteamId; internal string PlayerName; internal byte[] Nonce; internal byte[] Salt; internal byte[] SessionKey; internal float JoinTime; internal float LastHeartbeat; internal int LastCounter = -1; internal int MissedHeartbeats; internal bool Verified; private readonly HashSet<string> _announced = new HashSet<string>(); internal bool AnnounceOnce(string type, string detail) { return _announced.Add(type + "|" + detail); } } internal static class Sessions { private static readonly Dictionary<long, Session> _byUid = new Dictionary<long, Session>(); private static readonly object _gate = new object(); internal static int Count { get { lock (_gate) { return _byUid.Count; } } } internal static Session Create(long uid) { lock (_gate) { Session session = new Session { PeerUid = uid }; _byUid[uid] = session; return session; } } internal static Session Get(long uid) { lock (_gate) { _byUid.TryGetValue(uid, out var value); return value; } } internal static void Remove(long uid) { lock (_gate) { _byUid.Remove(uid); } } internal static byte[] KeyFor(long uid) { lock (_gate) { Session value; return _byUid.TryGetValue(uid, out value) ? value.SessionKey : null; } } internal static List<Session> Snapshot() { lock (_gate) { return new List<Session>(_byUid.Values); } } } internal static class Verdict { internal struct Finding { internal Protocol.FindingCat Cat; internal string Detail; } internal class Result { internal string Type; internal Config.Action Action; internal string Confidence = "single"; internal string Detail = ""; internal bool Detection; } internal static Result Evaluate(Dictionary<string, string> plugins, List<Finding> findings) { if (plugins != null) { Result result = CheckMods(plugins); if (result != null) { return result; } } bool flag = false; bool flag2 = false; bool flag3 = false; bool flag4 = false; string detail = ""; string detail2 = ""; string detail3 = ""; string detail4 = ""; foreach (Finding finding in findings) { switch (finding.Cat) { case Protocol.FindingCat.InjectedAssembly: flag = true; detail = finding.Detail; break; case Protocol.FindingCat.ForeignHarmony: flag2 = true; detail2 = finding.Detail; break; case Protocol.FindingCat.StateAnomaly: flag3 = true; detail3 = finding.Detail; break; case Protocol.FindingCat.ProcessMatch: flag4 = true; detail4 = finding.Detail; break; } } if (flag2) { return new Result { Type = "ForeignHarmony", Action = Config.ActForeignHarmony.Value, Confidence = "single", Detail = detail2, Detection = true }; } if (flag3) { return new Result { Type = "StateAnomaly", Action = Config.ActStateAnomaly.Value, Confidence = "single", Detail = detail3, Detection = true }; } if (flag4) { return new Result { Type = "ProcessMatch", Action = Config.ActProcessMatch.Value, Confidence = "single", Detail = detail4, Detection = true }; } if (flag) { return new Result { Type = "InjectedAssembly", Action = Config.ActInjectedAssembly.Value, Confidence = "single", Detail = detail, Detection = true }; } return new Result(); } private static Result CheckMods(Dictionary<string, string> reported) { Dictionary<string, string> allowedMods = Config.AllowedMods; foreach (KeyValuePair<string, string> item in allowedMods) { if (!reported.TryGetValue(item.Key, out var value)) { return Fail("Missing required mod: " + item.Key + " (version " + item.Value + "). Please install it."); } if (item.Value != "*" && !string.Equals(item.Value, value, StringComparison.OrdinalIgnoreCase)) { return Fail("Wrong version of " + item.Key + ": you have " + value + ", the server requires " + item.Value + "."); } } Dictionary<string, string> optionalMods = Config.OptionalMods; foreach (KeyValuePair<string, string> item2 in reported) { if (!allowedMods.ContainsKey(item2.Key)) { if (!optionalMods.TryGetValue(item2.Key, out var value2)) { return Fail("Remove this mod: " + item2.Key + " (version " + item2.Value + ") is not allowed on this server."); } if (value2 != "*" && !string.Equals(value2, item2.Value, StringComparison.OrdinalIgnoreCase)) { return Fail("Wrong version of " + item2.Key + ": you have " + item2.Value + ", the server allows " + value2 + "."); } } } return null; } private static Result Fail(string detail) { return new Result { Type = "ModMismatch", Action = Config.ActModMismatch.Value, Confidence = "single", Detail = detail }; } } internal static class ViolationLog { private static string _path; private static readonly object _gate = new object(); internal static void Init(string configDir) { _path = Path.Combine(configDir, "koumodgp", "violations.log"); Directory.CreateDirectory(Path.GetDirectoryName(_path)); } internal static void Write(string utc, string steamId, string name, string type, string confidence, string action, string detail) { if (_path == null) { return; } string text = utc + "\t" + steamId + "\t" + name + "\t" + type + "\t" + confidence + "\t" + action + "\t" + detail; try { lock (_gate) { File.AppendAllText(_path, text + Environment.NewLine); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[koumodgp] violation log write failed: " + ex.Message)); } } } }