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.1.9
koumodgp.dll
Decompiled 2 days 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 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; Plugin.Log.LogInfo((object)string.Format("[{0}] received {1} signatures.", "koumodgp", num)); } 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<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."); 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.Kick, "A managed assembly not loaded by Chainloader was found (injected tool)."); 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.1.9\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(string utc, string playerName, string steamId, string type, string detail) { string text = "Player: `" + Escape(playerName) + "`\nID: `" + Escape(steamId) + "`\nTime: " + utc + " UTC"; if (type == "ModMismatch") { Post("**Mod check failed**\n" + text + "\nIssue: " + Escape(detail)); } else { Post("**Player using external/internal cheat**\n" + text); } } 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.1.9")] 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.1.9", 1)); } 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.1.9"; public const string RpcClientToServer = "kou_gpsync_a"; public const string RpcServerToClient = "kou_gpsync_b"; public const int WireVersion = 1; } 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)1); 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 != 1) { Plugin.Log.LogWarning((object)string.Format("[{0}] wire mismatch (got {1}, want {2}) — ignored.", "koumodgp", num, 1)); 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; } internal static void Collect(List<Verdict.Finding> outList) { CollectForeignHarmony(outList); CollectInjectedAssemblies(outList); } 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 = "patch from '" + 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 = PluginAssemblyNames(); 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 = PluginAssemblyNames(); 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 + "'" }); } } } } 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.1.9)."); } 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); } }, key); } internal static void Apply(Session s, Verdict.Result v) { string utc = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"); ViolationLog.Write(utc, s.SteamId, s.PlayerName, v.Type, v.Confidence, v.Action.ToString(), v.Detail); switch (v.Action) { case Config.Action.Log: break; case Config.Action.Warn: case Config.Action.Flag: Plugin.Log.LogWarning((object)("[koumodgp] flag " + s.PlayerName + " (" + s.SteamId + "): " + v.Type + " [" + v.Confidence + "].")); break; case Config.Action.Kick: case Config.Action.Ban: Discord.PostDetection(utc, s.PlayerName, s.SteamId, v.Type, v.Detail); 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 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; } 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 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 }; } if (flag3) { return new Result { Type = "StateAnomaly", Action = Config.ActStateAnomaly.Value, Confidence = "single", Detail = detail3 }; } if (flag4) { return new Result { Type = "ProcessMatch", Action = Config.ActProcessMatch.Value, Confidence = "single", Detail = detail4 }; } if (flag) { return new Result { Type = "InjectedAssembly", Action = Config.ActInjectedAssembly.Value, Confidence = "single", Detail = detail }; } 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); } if (item.Value != "*" && !string.Equals(item.Value, value, StringComparison.OrdinalIgnoreCase)) { return Fail("version mismatch " + item.Key + ": has " + value + ", 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("disallowed mod " + item2.Key + " (" + item2.Value + ")"); } if (value2 != "*" && !string.Equals(value2, item2.Value, StringComparison.OrdinalIgnoreCase)) { return Fail("version mismatch " + item2.Key + ": has " + item2.Value + ", 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)); } } } }