重构大版本更新
一口气全面统一博客样式,移除部分冗余导航页面,更专注内容展示。
通过 dnSpy 反编译剖析 FinalSuspect 阻止其他模组共存的三重检测机制(Harmony 拦截插件加载、目录扫描强制闪退、伪装类加密暗桩),给出逐层解除方法,以及共存后缺失 YamlDotNet 依赖与更新检查卡死的修复方案。

FinalSuspect 是一款 Among Us 的”原版体验”模组。它有一个让人头疼的特性:只要检测到 BepInEx/plugins 目录里存在其他模组,游戏就会直接闪退。本文从反编译角度剖析它的三重检测机制,并说明如何解除限制。
**声明:**本文仅用于技术研究与个人学习。FinalSuspect 是开源项目,修改后仅供自己使用;共存后产生的模组兼容性问题需自行承担。
通过 dnSpy 反编译,FinalSuspect 把”反其他模组”逻辑归进了 FinalAntiCheat(把装其他模组视为作弊行为)。一共有三处,采用”双保险 + 暗桩”设计:
它给 BepInEx 的 IL2CPPChainloader.LoadPlugin 打了一个 Harmony 前置补丁。Prefix 返回 false 会跳过原方法,也就是阻止该插件被加载。逻辑上只放行 UnityExplorer,其余插件一律拦截:
[HarmonyPatch(typeof(IL2CPPChainloader), "LoadPlugin")]public static class DisableOtherPlugins{ public static bool Prefix( [HarmonyArgument(0)] PluginInfo pluginInfo, [HarmonyArgument(1)] Assembly pluginAssembly) { // 仅当插件是 UnityExplorer 时返回 true(放行) // 其他所有插件返回 false → 跳过原方法 → 不加载 return pluginInfo.Metadata.GUID == "com.sinai.unityexplorer"; }}**解除方法:**让 Prefix 永远返回 true,放行所有插件。在 dnSpy 里编辑 IL,把方法体替换为两条指令:
ldc.i4.1 // 压入 trueret // 返回这是闪退的直接来源。它在启动时扫描插件目录,只要发现 FinalSuspect.dll 和 PolarNight.dll 以外的任何文件,就调用 Application.Quit(1) 强制退出;另外还检查游戏根目录是否存在外挂文件(这部分属于正常反作弊)。
public static class DllChecker{ [PluginModuleInitializer(InitializePriority.High)] internal static void OnInitialization() { string[] blacklist = { "SickoMenu.dll", "version.dll" }; string pluginDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string gameDir = Environment.CurrentDirectory;
// ① 扫描 plugins 目录:白名单之外一律闪退 if (pluginDir != null) { foreach (string file in Directory.EnumerateFiles(pluginDir, "*.*")) { string fileName = Path.GetFileName(file); bool isWhitelisted = fileName == "FinalSuspect.dll" || fileName == "PolarNight.dll"; if (!isWhitelisted) { FinalLogger.Error("检测到非法/模组文件: " + fileName + "!游戏将被强制终止。"); Application.Quit(1); // ← 闪退元凶 } } }
// ② 检查游戏根目录的外挂文件(反作弊,建议保留) foreach (string dll in blacklist) { if (File.Exists(Path.Combine(gameDir, dll))) { FinalLogger.Error("检测到非法文件: " + dll + "!游戏将被强制终止。"); Application.Quit(1); } } }}**解除方法:**在 dnSpy 里把 OnInitialization 的 IL 指令全部删除,只保留一条 ret。由于原方法含 foreach 循环,会生成 try/finally 异常处理块,必须同时在”异常处理程序”选项卡里删除所有条目,否则方法体无效、无法保存。
// 清空方法体,只保留:ret
// 并清空"异常处理程序"选项卡中的所有条目这是最隐蔽的一处。作者把同样的检测逻辑复制了一份,藏在一个名为 Git 的类里(起这个名字是为了迷惑查找者),并把所有敏感字符串用 XOR 加密隐藏。它作为 MainMenuManager.Start 的 Harmony 补丁,在进入主菜单后触发。
[HarmonyPatch(typeof(MainMenuManager), "Start")]internal static class Git{ // XOR 解密:字符串常量都用密钥异或隐藏,防止被直接搜索到 private static string Decrypt(byte[] data, byte key) { byte[] result = new byte[data.Length]; for (int i = 0; i < data.Length; i++) result[i] = (byte)(data[i] ^ key); return Encoding.UTF8.GetString(result); }
public static void Prefix(MainMenuManager __instance) { string[] rootBlacklist = { Git.A, Git.B }; // 解密后 = SickoMenu.dll / version.dll string pluginDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string gameDir = Environment.CurrentDirectory;
// ① 与 DllChecker 相同的 plugins 目录白名单检测(这里是杀模组的关键) if (pluginDir != null) { foreach (string file in Directory.EnumerateFiles(pluginDir, "*.*")) { string fileName = Path.GetFileName(file); // Git.C / Git.D 解密后 = FinalSuspect.dll / PolarNight.dll if (fileName != Git.C && fileName != Git.D) { FinalLogger.Error(Git.G, Git.J); // "检测到非法/模组文件..." Application.Quit(1); } } }
// ② 检测外挂进程、外挂窗口,并挂一个每分钟一次的定时器(反作弊部分) if (Process.GetProcessesByName(Git.E).Length != 0) Application.Quit(1); if (FindWindow(null, Git.F) != IntPtr.Zero) Application.Quit(1); // ...省略定时器逻辑 }}加密字符串用密钥 85(0x55)异或后可还原为:
| 字段 | 解密内容 | 用途 |
|---|---|---|
| Git.C / Git.D | FinalSuspect.dll / PolarNight.dll | plugins 目录白名单(杀模组) |
| Git.A / Git.B | SickoMenu.dll / version.dll | 外挂文件检测(反作弊) |
| Git.E / Git.F | 外挂进程名 / 窗口标题 | 运行中外挂检测(反作弊) |
| Git.J | FAC | 日志标签 |
**解除方法:**同样把 Git.Prefix 的 IL 清空为一条 ret,并清空异常处理程序选项卡。注意此处会连带移除反外挂检测——不开挂即无影响。
解除检测后,模组虽能共存,但会暴露两个隐藏问题,需要一并处理:
FinalSuspect 加载语言文件时依赖 YamlDotNet.dll。若使用自己搭建的 BepInEx 环境而非官方完整包,会因缺少依赖抛出异常、加载协程中断,表现为卡在加载界面。
FileNotFoundException: Could not load file or assembly 'YamlDotNet, Version=16.0.0.0' at FinalSuspect.Modules.Core.Plugin.Translator.LoadLangs() at FinalSuspect.Patches.System.LoadPatch.LoadEssentialResources(...)**解决:**从官方完整发布包中提取以下 DLL,放入 BepInEx/core/:
YamlDotNet.dll (必需,缺失会卡加载)NAudio.dllNAudio.Core.dllNAudio.Wasapi.dllNAudio.WinMM.dllNAudio.Flac.dllNAudio.Asio.dll (音乐功能所需)进入主菜单时,VersionChecker 会连接更新源检查新版本,而这些源大多失效,导致主线程阻塞、游戏未响应。好在作者内置了离线模式开关,无需改 DLL,只需改配置文件。
// 触发更新检查的判断逻辑bool flag = VersionChecker.FirstStart && !ConfigManager.OfflineMode.Value;if (flag){ VersionChecker.StartTasks(); // ← 联网检查更新,卡死源头}**解决:**编辑 BepInEx/config/cn.slok.finalsuspect.cfg(需在游戏关闭状态下修改):
Offline Mode = true # 跳过启动时的更新检查Language Update Bypass = LongTerm # 长期跳过语言文件更新检查FinalSuspect.dll,先备份原文件DisableOtherPlugins.Prefix → ldc.i4.1 + retDllChecker.OnInitialization → 仅留 ret,并清空异常处理程序Git.Prefix → 仅留 ret,并清空异常处理程序BepInEx/plugins/BepInEx/core/ 下的 YamlDotNet 与 NAudio 依赖