using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using BepInEx; using BepInEx.Logging; using HarmonyLib; using IronLabs.SharedLib; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("IronLabs.ModIntegrity")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("IronLabs.ModIntegrity")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("9B7B2B9F-38C2-4E87-A7BD-E29310479B42")] [assembly: AssemblyFileVersion("1.0.1")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.0.1.40292")] namespace IronLabs.ModIntegrity { internal sealed class ConnectionState { internal string Nonce { get; } internal bool IsValidated { get; set; } internal ConnectionState(string nonce) { Nonce = nonce; } } internal static class ConnectionValidator { private static readonly Dictionary ServerStates = new Dictionary(); private static readonly Dictionary ClientNonces = new Dictionary(); internal static void Register(ZRpc rpc) { rpc.Register("IronLabs.ModIntegrity.Challenge", (Action)ReceiveChallenge); rpc.Register("IronLabs.ModIntegrity.Response", (Action)ReceiveResponse); } internal static void SendChallenge(ZRpc rpc) { string nonce = CreateNonce(); ServerStates[rpc] = new ConnectionState(nonce); rpc.Invoke("IronLabs.ModIntegrity.Challenge", new object[1] { IntegrityProtocol.CreateChallenge(nonce) }); ModIntegrityPlugin.Log.LogDebug("Sent DLL integrity challenge to " + Describe(rpc) + "."); } internal static void SendResponse(ZRpc rpc) { if (!ClientNonces.TryGetValue(rpc, out var value)) { ModIntegrityPlugin.Log.LogError("Server did not send a DLL integrity challenge."); return; } IReadOnlyList readOnlyList = DllInventory.ReadClientInventory(); rpc.Invoke("IronLabs.ModIntegrity.Response", new object[1] { IntegrityProtocol.CreateResponse(value, readOnlyList) }); ModIntegrityPlugin.Log.LogDebug($"Sent {readOnlyList.Count} DLL hashes to the server."); } internal static bool IsValidated(ZRpc rpc) { ConnectionState value; return ServerStates.TryGetValue(rpc, out value) && value.IsValidated; } internal static void Remove(ZRpc rpc) { ServerStates.Remove(rpc); ClientNonces.Remove(rpc); } internal static void Clear() { ServerStates.Clear(); ClientNonces.Clear(); } private static void ReceiveChallenge(ZRpc rpc, ZPackage package) { try { ClientNonces[rpc] = IntegrityProtocol.ReadChallenge(package); ModIntegrityPlugin.Log.LogDebug("Received DLL integrity challenge from " + Describe(rpc) + "."); } catch (Exception ex) { ModIntegrityPlugin.Log.LogError("Invalid DLL integrity challenge: " + ex.Message); } } private static void ReceiveResponse(ZRpc rpc, ZPackage package) { try { ValidateResponse(rpc, package); } catch (Exception ex) { Reject(rpc, "Invalid integrity response: " + ex.Message); } } private static void ValidateResponse(ZRpc rpc, ZPackage package) { string nonce; IReadOnlyList readOnlyList = IntegrityProtocol.ReadResponse(package, out nonce); if (!ServerStates.TryGetValue(rpc, out var value) || value.Nonce != nonce) { Reject(rpc, "The challenge nonce is missing, expired, or incorrect."); return; } ServerDllPolicy policy = DllInventory.ReadServerPolicy(); InventoryDifference inventoryDifference = InventoryComparer.Compare(policy, readOnlyList); if (!inventoryDifference.IsMatch) { LogDifference(rpc, inventoryDifference); RejectConnection(rpc); } else { value.IsValidated = true; ModIntegrityPlugin.Log.LogInfo($"DLL integrity validated for {Describe(rpc)}: {readOnlyList.Count} exact matches."); } } private static void LogDifference(ZRpc rpc, InventoryDifference difference) { ModIntegrityPlugin.Log.LogError("DLL integrity validation failed for " + Describe(rpc) + "."); LogItems("Missing", difference.Missing); LogItems("Unexpected", difference.Unexpected); LogItems("SHA-256 mismatch", difference.Different); } private static void LogItems(string category, IEnumerable items) { foreach (string item in items) { ModIntegrityPlugin.Log.LogError("- " + category + ": " + item); } } private static void Reject(ZRpc rpc, string reason) { ModIntegrityPlugin.Log.LogError("DLL integrity validation failed for " + Describe(rpc) + ": " + reason); RejectConnection(rpc); } private static void RejectConnection(ZRpc rpc) { rpc.Invoke("Error", new object[1] { 3 }); } private static string CreateNonce() { byte[] array = new byte[32]; using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create()) { randomNumberGenerator.GetBytes(array); } return Convert.ToBase64String(array); } private static string Describe(ZRpc rpc) { object obj; if (rpc == null) { obj = null; } else { ISocket socket = rpc.GetSocket(); obj = ((socket != null) ? socket.GetEndPointString() : null); } if (obj == null) { obj = "unknown peer"; } return (string)obj; } } internal static class DllInventory { internal const string WhitelistDirectoryName = "ModIntegrity_Whitelist"; internal const string GreylistDirectoryName = "ModIntegrity_Greylist"; private static ServerDllPolicy _serverPolicy; internal static IReadOnlyList ReadClientInventory() { return ReadDirectory(Paths.PluginPath); } internal static ServerDllPolicy ReadServerPolicy() { if (_serverPolicy != null) { return _serverPolicy; } if (!ZNet.instance.IsDedicated()) { IReadOnlyList required = ReadDirectory(Paths.PluginPath); _serverPolicy = new ServerDllPolicy(required, new List()); return _serverPolicy; } _serverPolicy = ReadDedicatedPolicy(); ModIntegrityPlugin.Log.LogInfo($"Loaded {_serverPolicy.Required.Count} required and " + $"{_serverPolicy.Optional.Count} optional DLL hashes."); return _serverPolicy; } internal static void ClearServerReference() { _serverPolicy = null; } private static ServerDllPolicy ReadDedicatedPolicy() { string text = Path.Combine(Paths.ConfigPath, "ModIntegrity_Whitelist"); if (!Directory.Exists(text)) { throw new DirectoryNotFoundException("Required DLL directory not found: " + text); } string text2 = Path.Combine(Paths.ConfigPath, "ModIntegrity_Greylist"); IReadOnlyList readOnlyList2; if (!Directory.Exists(text2)) { IReadOnlyList readOnlyList = new List(); readOnlyList2 = readOnlyList; } else { readOnlyList2 = ReadDirectory(text2); } IReadOnlyList optional = readOnlyList2; return new ServerDllPolicy(ReadDirectory(text), optional); } private static IReadOnlyList ReadDirectory(string root) { return (from path in Directory.GetFiles(root, "*.dll", SearchOption.AllDirectories) select CreateRecord(root, path)).OrderBy((DllRecord record) => record.RelativePath, StringComparer.OrdinalIgnoreCase).ToList(); } private static DllRecord CreateRecord(string root, string path) { string text = path.Substring(root.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); text = text.Replace(Path.DirectorySeparatorChar, '/'); using SHA256 sHA = SHA256.Create(); using FileStream inputStream = File.OpenRead(path); return new DllRecord(text, ToHex(sHA.ComputeHash(inputStream))); } private static string ToHex(byte[] bytes) { return BitConverter.ToString(bytes).Replace("-", string.Empty); } } internal sealed class DllRecord { internal string RelativePath { get; } internal string Sha256 { get; } internal DllRecord(string relativePath, string sha256) { RelativePath = relativePath; Sha256 = sha256; } } internal static class IntegrityProtocol { internal const int Version = 1; internal const int MaximumDllCount = 1024; internal const string ChallengeRpc = "IronLabs.ModIntegrity.Challenge"; internal const string ResponseRpc = "IronLabs.ModIntegrity.Response"; internal static ZPackage CreateChallenge(string nonce) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(nonce); return val; } internal static ZPackage CreateResponse(string nonce, IReadOnlyList inventory) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(nonce); val.Write(inventory.Count); foreach (DllRecord item in inventory) { val.Write(item.RelativePath); val.Write(item.Sha256); } return val; } internal static string ReadChallenge(ZPackage package) { AssertProtocolVersion(package.ReadInt()); return package.ReadString(); } internal static IReadOnlyList ReadResponse(ZPackage package, out string nonce) { AssertProtocolVersion(package.ReadInt()); nonce = package.ReadString(); int num = package.ReadInt(); if (num < 0 || num > 1024) { throw new InvalidOperationException($"Invalid DLL count: {num}."); } List list = new List(num); for (int i = 0; i < num; i++) { list.Add(new DllRecord(package.ReadString(), package.ReadString())); } return list; } private static void AssertProtocolVersion(int version) { if (version != 1) { throw new InvalidOperationException($"Unsupported integrity protocol {version}; expected {1}."); } } } internal static class InventoryComparer { internal static InventoryDifference Compare(ServerDllPolicy policy, IReadOnlyList actual) { Dictionary dictionary = ToDictionary(policy.Required); Dictionary optional = ToDictionary(policy.Optional); Dictionary actual2 = ToDictionary(actual); EnsureDisjoint(dictionary, optional); InventoryDifference inventoryDifference = new InventoryDifference(); CompareRequired(dictionary, actual2, inventoryDifference); CompareOptional(optional, actual2, inventoryDifference); FindUnexpected(dictionary, optional, actual2, inventoryDifference); return inventoryDifference; } private static Dictionary ToDictionary(IReadOnlyList records) { return records.ToDictionary((DllRecord record) => record.RelativePath, StringComparer.OrdinalIgnoreCase); } private static void CompareRequired(Dictionary expected, Dictionary actual, InventoryDifference difference) { foreach (KeyValuePair item in expected.OrderBy((KeyValuePair pair) => pair.Key)) { if (!actual.TryGetValue(item.Key, out var value)) { difference.Missing.Add(item.Key); } else if (!string.Equals(item.Value.Sha256, value.Sha256, StringComparison.OrdinalIgnoreCase)) { difference.Different.Add(item.Key); } } } private static void CompareOptional(Dictionary optional, Dictionary actual, InventoryDifference difference) { foreach (KeyValuePair item in optional) { if (actual.TryGetValue(item.Key, out var value) && !string.Equals(item.Value.Sha256, value.Sha256, StringComparison.OrdinalIgnoreCase)) { difference.Different.Add(item.Key); } } } private static void FindUnexpected(Dictionary required, Dictionary optional, Dictionary actual, InventoryDifference difference) { foreach (string item in from path in actual.Keys where !required.ContainsKey(path) && !optional.ContainsKey(path) orderby path select path) { difference.Unexpected.Add(item); } } private static void EnsureDisjoint(Dictionary required, Dictionary optional) { string text = required.Keys.FirstOrDefault(optional.ContainsKey); if (text != null) { throw new InvalidOperationException("DLL is present in both whitelist and greylist: " + text); } } } internal sealed class InventoryDifference { internal List Missing { get; } = new List(); internal List Unexpected { get; } = new List(); internal List Different { get; } = new List(); internal bool IsMatch => Missing.Count == 0 && Unexpected.Count == 0 && Different.Count == 0; } [BepInPlugin("IronLabs.ModIntegrity", "IronLabs.ModIntegrity", "1.0.1")] public sealed class ModIntegrityPlugin : IronLabsPlugin { internal const string PluginGuid = "IronLabs.ModIntegrity"; internal const string PluginName = "IronLabs.ModIntegrity"; internal const string PluginVersion = "1.0.1"; internal static ModLog Log { get; private set; } private void Awake() { Log = InitializePlugin("IronLabs.ModIntegrity"); Log.LogInfo("IronLabs.ModIntegrity 1.0.1 is enforcing exact DLL integrity."); } private void OnDestroy() { ConnectionValidator.Clear(); DllInventory.ClearServerReference(); ShutdownPlugin(); Log = null; } } [HarmonyPatch] internal static class NetworkPatches { [HarmonyPrefix] [HarmonyPriority(800)] [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static void RegisterIntegrityRpcs(ZNetPeer peer) { ConnectionValidator.Register(peer.m_rpc); } [HarmonyPrefix] [HarmonyPriority(800)] [HarmonyPatch(typeof(ZNet), "RPC_ServerHandshake")] private static void SendServerChallenge(ZRpc rpc) { ConnectionValidator.SendChallenge(rpc); } [HarmonyPrefix] [HarmonyPriority(800)] [HarmonyPatch(typeof(ZNet), "RPC_ClientHandshake")] private static void SendClientInventory(ZRpc rpc) { ConnectionValidator.SendResponse(rpc); } [HarmonyPrefix] [HarmonyPriority(800)] [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private static bool RejectUnvalidatedPeer(ZNet __instance, ZRpc rpc) { if (!__instance.IsServer() || ConnectionValidator.IsValidated(rpc)) { return true; } ModIntegrityPlugin.Log.LogError("Rejected unvalidated peer " + rpc.GetSocket().GetEndPointString() + " before PeerInfo."); rpc.Invoke("Error", new object[1] { 3 }); return false; } [HarmonyPrefix] [HarmonyPatch(typeof(ZNet), "Disconnect")] private static void RemoveConnectionState(ZNetPeer peer) { if (peer != null) { ConnectionValidator.Remove(peer.m_rpc); } } } internal sealed class ServerDllPolicy { internal IReadOnlyList Required { get; } internal IReadOnlyList Optional { get; } internal ServerDllPolicy(IReadOnlyList required, IReadOnlyList optional) { Required = required; Optional = optional; } } } namespace IronLabs.SharedLib { public abstract class IronLabsPlugin : BaseUnityPlugin { private Harmony _harmony; private bool _patchesApplied; protected ModLog InitializePlugin(string pluginGuid) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected O, but got Unknown ModLog modLog = new ModLog(((BaseUnityPlugin)this).Logger); Version version = ((object)this).GetType().Assembly.GetName().Version; modLog.LogInfo($"AssemblyVersion: {version}."); _harmony = new Harmony(pluginGuid); PatchOwnNamespace(modLog); return modLog; } protected void PatchOwnNamespace(ModLog log) { if (_patchesApplied) { log.LogDebug("Harmony patches are already active; skipping registration."); return; } string text = ((object)this).GetType().Namespace; Type[] types = Assembly.GetExecutingAssembly().GetTypes(); foreach (Type type in types) { if (type.Namespace == text) { _harmony.CreateClassProcessor(type).Patch(); } } _patchesApplied = true; log.LogDebug("Harmony patches were applied for the plugin namespace."); } protected void ShutdownPlugin() { if (_patchesApplied) { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _patchesApplied = false; } } } public sealed class ModLog { private readonly ManualLogSource _logger; public ModLog(ManualLogSource logger) { _logger = logger; } public void LogFatal(object message) { Write((LogLevel)1, message); } public void LogError(object message) { Write((LogLevel)2, message); } public void LogWarning(object message) { Write((LogLevel)4, message); } public void LogMessage(object message) { Write((LogLevel)8, message); } public void LogInfo(object message) { Write((LogLevel)16, message); } public void LogDebug(object message) { Write((LogLevel)32, message); } public void Log(LogLevel level, object message) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) Write(level, message); } private void Write(LogLevel level, object message) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) string arg = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); _logger.Log(level, (object)$"[{arg}] {message}"); } } }