using 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 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 [ver]|sig add/remove |kick |ban "); break; } string text = stringBuilder.ToString().TrimEnd(Array.Empty()); Plugin.Log.LogInfo((object)("[koumodgp] " + text)); return text; } private static void Status(StringBuilder sb) { List 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 allowedMod in Config.AllowedMods) { sb.AppendLine(" " + allowedMod.Key + " = " + allowedMod.Value); } sb.AppendLine("Optional mods:"); foreach (KeyValuePair 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 [version] | remove "); 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 | remove "); 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") + " "); 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 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") }.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 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 findings = new List(); RunScans(findings); byte[] selfHash = SelfHash.Compute(); Dictionary plugins = CollectPlugins(); Rpc.SendToServer(Protocol.MsgType.Manifest, delegate(ZPackage pkg) { pkg.Write(selfHash); pkg.Write(plugins.Count); foreach (KeyValuePair 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 findings = new List(); 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 findings) { ScanAssembly.Collect(findings); ScanState.Collect(findings); ScanProcess.Collect(findings); } private static Dictionary CollectPlugins() { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); try { foreach (KeyValuePair 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 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 Enabled; internal static ConfigEntry HandshakeTimeoutSeconds; internal static ConfigEntry HeartbeatIntervalSeconds; internal static ConfigEntry MaxMissedHeartbeats; internal static ConfigEntry ExemptAdmins; internal static ConfigEntry ActMissingCompanion; internal static ConfigEntry ActModMismatch; internal static ConfigEntry ActInjectedAssembly; internal static ConfigEntry ActForeignHarmony; internal static ConfigEntry ActStateAnomaly; internal static ConfigEntry ActProcessMatch; internal static ConfigEntry ActHeartbeatFail; internal static ConfigEntry DiscordWebhookUrl; internal static ConfigEntry DiscordPostLifecycle; internal static readonly Dictionary AllowedMods = new Dictionary(StringComparer.OrdinalIgnoreCase); internal static readonly Dictionary OptionalMods = new Dictionary(StringComparer.OrdinalIgnoreCase); internal static readonly List Signatures = new List(); private static string _cfgDir; internal static void Init(ConfigFile cfg, string configDir) { _cfgDir = configDir; Enabled = cfg.Bind("General", "Enabled", true, "Master switch."); HandshakeTimeoutSeconds = cfg.Bind("General", "HandshakeTimeoutSeconds", 20, "Seconds a joining client has to complete the handshake before being kicked."); HeartbeatIntervalSeconds = cfg.Bind("General", "HeartbeatIntervalSeconds", 15, "How often clients send a signed heartbeat."); MaxMissedHeartbeats = cfg.Bind("General", "MaxMissedHeartbeats", 2, "Consecutive missed heartbeats tolerated before kick."); ExemptAdmins = cfg.Bind("General", "ExemptAdmins", true, "Admins (adminlist.txt) are fully exempt and silent — never checked or logged."); ActMissingCompanion = cfg.Bind("Policy", "MissingCompanion", Action.Kick, "Client never completed the handshake (vanilla or stripped companion)."); ActModMismatch = cfg.Bind("Policy", "ModMismatch", Action.Kick, "Client mod list does not match the allowed list."); ActInjectedAssembly = cfg.Bind("Policy", "InjectedAssembly", Action.Kick, "A managed assembly not loaded by Chainloader was found (injected tool)."); ActForeignHarmony = cfg.Bind("Policy", "ForeignHarmony", Action.Kick, "A Harmony patch from a non-whitelisted owner id was found."); ActStateAnomaly = cfg.Bind("Policy", "StateAnomaly", Action.Kick, "A monitored game field held an impossible value (trainer edit)."); ActProcessMatch = cfg.Bind("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("Policy", "HeartbeatFail", Action.Kick, "Heartbeats stopped, went out of order, or failed their signature."); DiscordWebhookUrl = cfg.Bind("Logging", "DiscordWebhookUrl", "", "Discord webhook URL. Empty = disabled. Detection posts are generic ('External violation detected')."); DiscordPostLifecycle = cfg.Bind("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 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 enumerator = val.MainModule.Types.GetEnumerator(); try { while (enumerator.MoveNext()) { Enumerator 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 _queue = new BlockingCollection(); 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("kou_gpsync_a", (Action)OnClientToServer); ZRoutedRpc.instance.Register("kou_gpsync_b", (Action)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 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 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 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 PluginAssemblyNames() { HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); try { foreach (KeyValuePair 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 outList) { CollectForeignHarmony(outList); CollectInjectedAssemblies(outList); } private static void CollectForeignHarmony(List outList) { try { HashSet plugins = AllowedAssemblies(); HashSet hashSet = new HashSet(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 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 AllowedAssemblies() { HashSet hashSet = PluginAssemblyNames(); hashSet.Add("0Harmony"); hashSet.Add("HarmonyX"); hashSet.Add("MonoMod.RuntimeDetour"); return hashSet; } private static bool IsAllowed(string name, HashSet plugins) { if (plugins.Contains(name)) { return true; } return GoodPrefixes.Any((string pre) => name.StartsWith(pre, StringComparison.OrdinalIgnoreCase)); } private static void CollectInjectedAssemblies(List outList) { try { HashSet 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 outList) { string[] signatures = Signatures; if (signatures == null || signatures.Length == 0) { return; } HashSet hashSet = new HashSet(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 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 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 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 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 dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); List list = new List(); 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 list = new List(); 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 hashSet = new HashSet(); 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 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 _byUid = new Dictionary(); 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 Snapshot() { lock (_gate) { return new List(_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 plugins, List 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 reported) { Dictionary allowedMods = Config.AllowedMods; foreach (KeyValuePair 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 optionalMods = Config.OptionalMods; foreach (KeyValuePair 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)); } } } }