Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of MLVScan v1.6.6
Plugins/MLVScan.MelonLoader.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using MLVScan.Abstractions; using MLVScan.Adapters; using MLVScan.MelonLoader; using MLVScan.Models; using MLVScan.Models.CrossAssembly; using MLVScan.Models.Dto; using MLVScan.Models.Rules; using MLVScan.Models.Rules.Helpers; using MLVScan.Models.ThreatIntel; using MLVScan.Services; using MLVScan.Services.DeepBehavior; using MLVScan.Services.Helpers; using MLVScan.Services.ThreatIntel; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: MelonInfo(typeof(MelonLoaderPlugin), "MLVScan", "1.6.6", "Bars", null)] [assembly: MelonPriority(int.MinValue)] [assembly: MelonColor(255, 139, 0, 0)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("MLVScan.MelonLoader")] [assembly: AssemblyConfiguration("MelonLoader")] [assembly: AssemblyFileVersion("1.6.6")] [assembly: AssemblyInformationalVersion("1.6.6+0d9b47b6a7535ecb945e6d761817ffe54c11f50b")] [assembly: AssemblyProduct("MLVScan.MelonLoader")] [assembly: AssemblyTitle("MLVScan.MelonLoader")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.6.6.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 MLVScan { internal static class MLVScanBuildInfo { public const string PlatformVersion = "1.6.6"; } public static class PlatformConstants { public const string PlatformVersion = "1.6.6"; public const string PlatformName = "MLVScan.MelonLoader"; public static string GetVersionString() { return "MLVScan.MelonLoader v1.6.6"; } public static string GetFullVersionInfo() { return "Engine: " + Constants.GetVersionString() + "\nPlatform: " + GetVersionString(); } } } namespace MLVScan.Adapters { public class MelonScanLogger : IScanLogger { private readonly Instance _logger; public MelonScanLogger(Instance logger) { _logger = logger ?? throw new ArgumentNullException("logger"); } public void Debug(string message) { _logger.Msg("[DEBUG] " + message); } public void Info(string message) { _logger.Msg(message); } public void Warning(string message) { _logger.Warning(message); } public void Error(string message) { _logger.Error(message); } public void Error(string message, Exception exception) { _logger.Error($"{message}: {exception}"); } } public class GameAssemblyResolverProvider : IAssemblyResolverProvider { public IAssemblyResolver CreateResolver() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown DefaultAssemblyResolver val = new DefaultAssemblyResolver(); try { string text = Path.Combine(MelonEnvironment.GameRootDirectory, Path.GetFileNameWithoutExtension(MelonEnvironment.GameExecutablePath) + "_Data", "Managed"); if (Directory.Exists(text)) { ((BaseAssemblyResolver)val).AddSearchDirectory(text); } string text2 = Path.Combine(MelonEnvironment.GameRootDirectory, "MelonLoader", "net35"); if (Directory.Exists(text2)) { ((BaseAssemblyResolver)val).AddSearchDirectory(text2); } string text3 = Path.Combine(MelonEnvironment.GameRootDirectory, "MelonLoader", "net6"); if (Directory.Exists(text3)) { ((BaseAssemblyResolver)val).AddSearchDirectory(text3); } string modsDirectory = MelonEnvironment.ModsDirectory; if (Directory.Exists(modsDirectory)) { ((BaseAssemblyResolver)val).AddSearchDirectory(modsDirectory); } string pluginsDirectory = MelonEnvironment.PluginsDirectory; if (Directory.Exists(pluginsDirectory)) { ((BaseAssemblyResolver)val).AddSearchDirectory(pluginsDirectory); } } catch (Exception) { } return (IAssemblyResolver)(object)val; } } } namespace MLVScan.MelonLoader { public class MelonLoaderPlugin : MelonPlugin { private MelonLoaderServiceFactory _serviceFactory; private MelonConfigManager _configManager; private MelonPlatformEnvironment _environment; private MelonPluginScanner _pluginScanner; private MelonPluginDisabler _pluginDisabler; private IlDumpService _ilDumpService; private DeveloperReportGenerator _developerReportGenerator; private ReportUploadService _reportUploadService; private bool _initialized = false; private bool _showUploadConsentPopup; private string _pendingUploadPath = string.Empty; private string _pendingUploadModName = string.Empty; private List<ScanFinding> _pendingUploadFindings; private static readonly string[] DefaultWhitelistedHashes = new string[4] { "3918e1454e05de4dd3ace100d8f4d53936c9b93694dbff5bcc0293d689cb0ab7", "8e6dd1943c80e2d1472a9dc2c6722226d961027a7ec20aab9ad8f1184702d138", "d47eb6eabd3b6e3b742c7d9693651bc3a61a90dcbe838f9a4276953089ee4951", "cfe43c0d285867a5701d96de1edd25cb02725fe2629b88386351dc07b11a08b5" }; public override void OnEarlyInitializeMelon() { try { ((MelonBase)this).LoggerInstance.Msg("Pre-scanning for malicious mods..."); _serviceFactory = new MelonLoaderServiceFactory(((MelonBase)this).LoggerInstance); _configManager = _serviceFactory.CreateConfigManager(); _environment = _serviceFactory.CreateEnvironment(); InitializeDefaultWhitelist(); _pluginScanner = _serviceFactory.CreatePluginScanner(); _pluginDisabler = _serviceFactory.CreatePluginDisabler(); _ilDumpService = _serviceFactory.CreateIlDumpService(); _developerReportGenerator = _serviceFactory.CreateDeveloperReportGenerator(); _reportUploadService = _serviceFactory.CreateReportUploadService(); _initialized = true; ScanAndDisableMods(force: true); } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Error("Error in pre-mod scan: " + ex.Message); ((MelonBase)this).LoggerInstance.Error(ex.StackTrace); } } public override void OnInitializeMelon() { try { ((MelonBase)this).LoggerInstance.Msg("MLVScan initialization complete"); if (_configManager.Config.WhitelistedHashes.Length != 0) { ((MelonBase)this).LoggerInstance.Msg($"{_configManager.Config.WhitelistedHashes.Length} mod(s) are whitelisted and won't be scanned"); ((MelonBase)this).LoggerInstance.Msg("To manage whitelisted mods, edit MelonPreferences.cfg"); } } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Error("Error initializing MLVScan: " + ex.Message); ((MelonBase)this).LoggerInstance.Error(ex.StackTrace); } } public override void OnGUI() { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) if (_showUploadConsentPopup) { float num = Math.Min(620f, (float)Screen.width - 40f); float num2 = 280f; float num3 = ((float)Screen.width - num) / 2f; float num4 = ((float)Screen.height - num2) / 2f; GUI.Box(new Rect(0f, 0f, (float)Screen.width, (float)Screen.height), string.Empty); GUI.Box(new Rect(num3, num4, num, num2), "MLVScan Upload Consent"); GUI.Label(new Rect(num3 + 20f, num4 + 40f, num - 40f, 140f), "MLVScan flagged " + _pendingUploadModName + " as suspicious and disabled it.\n\nWould you like to upload this file to the MLVScan API for human review?\n\nYes: upload this mod now and enable automatic uploads for future detections.\nNo: do not upload and do not show this prompt again."); if (GUI.Button(new Rect(num3 + 20f, num4 + num2 - 60f, (num - 60f) / 2f, 36f), "Yes, upload")) { HandleUploadConsentDecision(approved: true); } if (GUI.Button(new Rect(num3 + 40f + (num - 60f) / 2f, num4 + num2 - 60f, (num - 60f) / 2f, 36f), "No thanks")) { HandleUploadConsentDecision(approved: false); } } } private void InitializeDefaultWhitelist() { if (_configManager != null) { string[] whitelistedHashes = _configManager.GetWhitelistedHashes(); if (whitelistedHashes.Length == 0) { ((MelonBase)this).LoggerInstance.Msg("Initializing default whitelist"); _configManager.SetWhitelistedHashes(DefaultWhitelistedHashes); } } } public Dictionary<string, ScannedPluginResult> ScanAndDisableMods(bool force = false) { try { if (!_initialized) { ((MelonBase)this).LoggerInstance.Error("Cannot scan mods - MLVScan not properly initialized"); return new Dictionary<string, ScannedPluginResult>(); } ((MelonBase)this).LoggerInstance.Msg("Scanning for suspicious mods..."); Dictionary<string, ScannedPluginResult> source = _pluginScanner.ScanAllPlugins(force); Dictionary<string, ScannedPluginResult> dictionary = source.Where((KeyValuePair<string, ScannedPluginResult> kv) => kv.Value != null && kv.Value.Findings.Count > 0).ToDictionary((KeyValuePair<string, ScannedPluginResult> kv) => kv.Key, (KeyValuePair<string, ScannedPluginResult> kv) => kv.Value); if (dictionary.Count > 0) { List<DisabledPluginInfo> list = _pluginDisabler.DisableSuspiciousPlugins(dictionary, force); int count = list.Count; ((MelonBase)this).LoggerInstance.Msg($"Disabled {count} suspicious mods"); if (count <= 0) { return dictionary; } GenerateDetailedReports(list, dictionary); ((MelonBase)this).LoggerInstance.Msg("To whitelist any false positives, add their SHA256 hash to the MLVScan → WhitelistedHashes setting in MelonPreferences.cfg"); } else { ((MelonBase)this).LoggerInstance.Msg("No suspicious mods found"); } return dictionary; } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Error("Error scanning mods: " + ex.Message); return new Dictionary<string, ScannedPluginResult>(); } } private void GenerateDetailedReports(List<DisabledPluginInfo> disabledMods, Dictionary<string, ScannedPluginResult> scanResults) { bool valueOrDefault = (_configManager?.Config?.Scan?.DeveloperMode).GetValueOrDefault(); if (valueOrDefault) { ((MelonBase)this).LoggerInstance.Msg("Developer Mode: Enabled"); } ((MelonBase)this).LoggerInstance.Warning("======= DETAILED SCAN REPORT ======="); ((MelonBase)this).LoggerInstance.Msg(PlatformConstants.GetFullVersionInfo()); foreach (DisabledPluginInfo disabledMod in disabledMods) { string fileName = Path.GetFileName(disabledMod.OriginalPath); string fileHash = disabledMod.FileHash; string text = (File.Exists(disabledMod.DisabledPath) ? disabledMod.DisabledPath : disabledMod.OriginalPath); ((MelonBase)this).LoggerInstance.Warning("BLOCKED MOD: " + fileName); ((MelonBase)this).LoggerInstance.Msg("SHA256 Hash: " + fileHash); ((MelonBase)this).LoggerInstance.Msg("-------------------------------"); if (!scanResults.TryGetValue(disabledMod.OriginalPath, out var value)) { ((MelonBase)this).LoggerInstance.Error("Could not find scan results for disabled mod"); continue; } List<ScanFinding> list = value?.Findings ?? new List<ScanFinding>(); ThreatVerdictInfo threatVerdictInfo = disabledMod.ThreatVerdict ?? value?.ThreatVerdict ?? new ThreatVerdictInfo(); if (list.Count == 0) { ((MelonBase)this).LoggerInstance.Msg("No specific suspicious patterns were identified."); continue; } QueueConsentPromptIfNeeded(text, fileName, list); Dictionary<string, List<ScanFinding>> dictionary = (from f in list group f by f.Description).ToDictionary((IGrouping<string, ScanFinding> g) => g.Key, (IGrouping<string, ScanFinding> g) => g.ToList()); ((MelonBase)this).LoggerInstance.Warning($"Total suspicious patterns found: {list.Count}"); ((MelonBase)this).LoggerInstance.Warning("Verdict: " + ThreatVerdictTextFormatter.GetVerdictLabel(threatVerdictInfo)); ((MelonBase)this).LoggerInstance.Msg(threatVerdictInfo.Summary); string primaryFamilyLabel = ThreatVerdictTextFormatter.GetPrimaryFamilyLabel(threatVerdictInfo); if (!string.IsNullOrWhiteSpace(primaryFamilyLabel)) { ((MelonBase)this).LoggerInstance.Msg("Family: " + primaryFamilyLabel); } string confidenceLabel = ThreatVerdictTextFormatter.GetConfidenceLabel(threatVerdictInfo); if (!string.IsNullOrWhiteSpace(confidenceLabel)) { ((MelonBase)this).LoggerInstance.Msg("Confidence: " + confidenceLabel); } Dictionary<Severity, int> dictionary2 = (from f in list group f by f.Severity into g orderby (int)g.Key descending select g).ToDictionary((IGrouping<Severity, ScanFinding> g) => g.Key, (IGrouping<Severity, ScanFinding> g) => g.Count()); ((MelonBase)this).LoggerInstance.Warning("Severity breakdown:"); foreach (KeyValuePair<Severity, int> item in dictionary2) { string arg = FormatSeverityLabel(item.Key); ((MelonBase)this).LoggerInstance.Msg($" {arg}: {item.Value} issue(s)"); } ((MelonBase)this).LoggerInstance.Msg("-------------------------------"); List<string> topFindingSummaries = ThreatVerdictTextFormatter.GetTopFindingSummaries(list); if (topFindingSummaries.Count > 0) { ((MelonBase)this).LoggerInstance.Warning("Top signals:"); foreach (string item2 in topFindingSummaries) { ((MelonBase)this).LoggerInstance.Msg(" - " + item2); } } if (valueOrDefault) { ((MelonBase)this).LoggerInstance.Msg("Developer mode is enabled. Full remediation guidance is included in the report file."); } ((MelonBase)this).LoggerInstance.Msg("Full technical details were written to the saved report file for human review."); ((MelonBase)this).LoggerInstance.Msg("-------------------------------"); DisplaySecurityNotice(fileName, threatVerdictInfo); try { string text2 = Path.Combine(MelonEnvironment.UserDataDirectory, "MLVScan", "Reports"); if (!Directory.Exists(text2)) { Directory.CreateDirectory(text2); } string text3 = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string text4 = Path.Combine(text2, fileName + "_" + text3 + ".report.txt"); string text5 = Path.Combine(text2, "Prompts"); if (!Directory.Exists(text5)) { Directory.CreateDirectory(text5); } MelonConfigManager configManager = _configManager; if (configManager != null && (configManager.Config?.DumpFullIlReports).GetValueOrDefault() && _ilDumpService != null) { string path = Path.Combine(text2, "IL"); string text6 = Path.Combine(path, fileName + "_" + text3 + ".il.txt"); if (_ilDumpService.TryDumpAssembly(text, text6)) { ((MelonBase)this).LoggerInstance.Msg("Full IL dump saved to: " + text6); } else { ((MelonBase)this).LoggerInstance.Warning("Failed to dump IL for this mod (see logs for details)."); } } PromptGeneratorService promptGeneratorService = _serviceFactory.CreatePromptGenerator(); using (StreamWriter streamWriter = new StreamWriter(text4)) { if (valueOrDefault && _developerReportGenerator != null) { string value2 = _developerReportGenerator.GenerateFileReport(fileName, fileHash, list, threatVerdictInfo); streamWriter.Write(value2); } else { streamWriter.WriteLine("MLVScan Security Report"); streamWriter.WriteLine(PlatformConstants.GetFullVersionInfo()); streamWriter.WriteLine($"Generated: {DateTime.Now}"); streamWriter.WriteLine("Mod File: " + fileName); streamWriter.WriteLine("SHA256 Hash: " + fileHash); streamWriter.WriteLine("Original Path: " + disabledMod.OriginalPath); streamWriter.WriteLine("Disabled Path: " + disabledMod.DisabledPath); streamWriter.WriteLine("Path Used For Analysis: " + text); streamWriter.WriteLine($"Total Suspicious Patterns: {list.Count}"); streamWriter.WriteLine(); ThreatVerdictTextFormatter.WriteThreatVerdictSection(streamWriter, threatVerdictInfo); streamWriter.WriteLine("\nSeverity Breakdown:"); foreach (KeyValuePair<Severity, int> item3 in dictionary2) { streamWriter.WriteLine($"- {item3.Key}: {item3.Value} issue(s)"); } streamWriter.WriteLine("=============================================="); foreach (KeyValuePair<string, List<ScanFinding>> item4 in dictionary) { streamWriter.WriteLine("\n== " + item4.Key + " =="); streamWriter.WriteLine($"Severity: {item4.Value[0].Severity}"); streamWriter.WriteLine($"Instances: {item4.Value.Count}"); streamWriter.WriteLine("\nLocations & Analysis:"); foreach (ScanFinding item5 in item4.Value) { streamWriter.WriteLine("- " + item5.Location); if (item5.HasCallChain && item5.CallChain != null) { streamWriter.WriteLine(" Call Chain Analysis:"); streamWriter.WriteLine(" " + item5.CallChain.Summary); streamWriter.WriteLine(" Attack Path:"); foreach (CallChainNode node in item5.CallChain.Nodes) { CallChainNodeType nodeType = node.NodeType; if (1 == 0) { } string text7 = nodeType switch { CallChainNodeType.EntryPoint => "[ENTRY]", CallChainNodeType.IntermediateCall => "[CALL]", CallChainNodeType.SuspiciousDeclaration => "[DECL]", _ => "[???]", }; if (1 == 0) { } string text8 = text7; streamWriter.WriteLine(" " + text8 + " " + node.Location); if (!string.IsNullOrEmpty(node.Description)) { streamWriter.WriteLine(" " + node.Description); } } } if (item5.HasDataFlow && item5.DataFlowChain != null) { streamWriter.WriteLine(" Data Flow Analysis:"); streamWriter.WriteLine($" Pattern: {item5.DataFlowChain.Pattern}"); streamWriter.WriteLine($" Confidence: {item5.DataFlowChain.Confidence * 100.0:F0}%"); streamWriter.WriteLine(" " + item5.DataFlowChain.Summary); if (item5.DataFlowChain.IsCrossMethod) { streamWriter.WriteLine($" Cross-method flow through {item5.DataFlowChain.InvolvedMethods.Count} methods"); } streamWriter.WriteLine(" Data Flow Chain:"); foreach (DataFlowNode node2 in item5.DataFlowChain.Nodes) { DataFlowNodeType nodeType2 = node2.NodeType; if (1 == 0) { } string text7 = nodeType2 switch { DataFlowNodeType.Source => "[SOURCE]", DataFlowNodeType.Transform => "[TRANSFORM]", DataFlowNodeType.Sink => "[SINK]", DataFlowNodeType.Intermediate => "[PASS]", _ => "[???]", }; if (1 == 0) { } string text9 = text7; streamWriter.WriteLine(" " + text9 + " " + node2.Operation + " (" + node2.DataDescription + ") @ " + node2.Location); } } if (!string.IsNullOrEmpty(item5.CodeSnippet)) { streamWriter.WriteLine(" Code Snippet (IL):"); string[] array = item5.CodeSnippet.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text10 in array) { streamWriter.WriteLine(" " + text10); } streamWriter.WriteLine(); } } } WriteSecurityNoticeToReport(streamWriter); } } if (promptGeneratorService.SavePromptToFile(text, list, text5)) { ((MelonBase)this).LoggerInstance.Msg("Detailed report saved to: " + text4); ((MelonBase)this).LoggerInstance.Msg("LLM analysis prompt saved to: " + Path.Combine(text5, fileName + ".prompt.md")); ((MelonBase)this).LoggerInstance.Msg("You can copy the contents of the prompt file into ChatGPT to help determine if this is malware or a false positive, although don't trust ChatGPT to be 100% accurate."); } else { ((MelonBase)this).LoggerInstance.Msg("Detailed report saved to: " + text4); } MelonConfigManager configManager2 = _configManager; if (configManager2 == null || !(configManager2.Config?.EnableReportUpload).GetValueOrDefault() || _reportUploadService == null) { continue; } try { string reportUploadApiBaseUrl = _configManager.GetReportUploadApiBaseUrl(); if (!string.IsNullOrWhiteSpace(reportUploadApiBaseUrl) && File.Exists(text)) { byte[] assemblyBytes = File.ReadAllBytes(text); SubmissionMetadata metadata = BuildSubmissionMetadata(fileName, list); _reportUploadService.UploadReportNonBlocking(assemblyBytes, fileName, metadata, reportUploadApiBaseUrl); } } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Warning("Report upload skipped for " + fileName + ": " + ex.Message); } } catch (Exception ex2) { ((MelonBase)this).LoggerInstance.Error("Failed to save detailed report: " + ex2.Message); } } ((MelonBase)this).LoggerInstance.Warning("====== END OF SCAN REPORT ======"); } private void QueueConsentPromptIfNeeded(string accessiblePath, string modName, List<ScanFinding> findings) { if (_configManager != null && !_showUploadConsentPopup) { MLVScanConfig config = _configManager.Config; if (!config.ReportUploadConsentAsked) { _showUploadConsentPopup = true; _pendingUploadPath = accessiblePath; _pendingUploadModName = modName; _pendingUploadFindings = findings; config.ReportUploadConsentPending = true; config.PendingReportUploadPath = accessiblePath ?? string.Empty; _configManager.SaveConfig(config); ((MelonBase)this).LoggerInstance.Warning("MLVScan is waiting for your upload consent decision in the in-game popup."); } } } private void HandleUploadConsentDecision(bool approved) { _showUploadConsentPopup = false; if (_configManager == null) { return; } MLVScanConfig config = _configManager.Config; config.ReportUploadConsentAsked = true; config.ReportUploadConsentPending = false; config.PendingReportUploadPath = string.Empty; config.EnableReportUpload = approved; _configManager.SaveConfig(config); if (!approved) { ((MelonBase)this).LoggerInstance.Msg("MLVScan report upload declined. You will not be prompted again."); _pendingUploadPath = string.Empty; _pendingUploadModName = string.Empty; _pendingUploadFindings = null; return; } ((MelonBase)this).LoggerInstance.Msg("MLVScan report upload enabled. Uploading the flagged mod now."); try { if (_reportUploadService != null && !string.IsNullOrWhiteSpace(_pendingUploadPath) && File.Exists(_pendingUploadPath)) { string reportUploadApiBaseUrl = _configManager.GetReportUploadApiBaseUrl(); if (!string.IsNullOrWhiteSpace(reportUploadApiBaseUrl)) { byte[] assemblyBytes = File.ReadAllBytes(_pendingUploadPath); SubmissionMetadata metadata = BuildSubmissionMetadata(_pendingUploadModName, _pendingUploadFindings ?? new List<ScanFinding>()); _reportUploadService.UploadReportNonBlocking(assemblyBytes, _pendingUploadModName, metadata, reportUploadApiBaseUrl); } } } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Warning("Report upload skipped for " + _pendingUploadModName + ": " + ex.Message); } finally { _pendingUploadPath = string.Empty; _pendingUploadModName = string.Empty; _pendingUploadFindings = null; } } private static SubmissionMetadata BuildSubmissionMetadata(string modName, List<ScanFinding> findings) { List<FindingSummaryItem> findingSummary = (from f in findings.Take(20) select new FindingSummaryItem { RuleId = f.RuleId, Description = f.Description, Severity = f.Severity.ToString(), Location = RedactionHelper.RedactLocation(f.Location) }).ToList(); return new SubmissionMetadata { LoaderType = "MelonLoader", LoaderVersion = null, PluginVersion = "1.6.6", ModName = RedactionHelper.RedactFilename(modName), FindingSummary = findingSummary, ConsentVersion = "1", ConsentTimestamp = DateTime.UtcNow.ToString("o") }; } private static string FormatSeverityLabel(Severity severity) { if (1 == 0) { } string result = severity switch { Severity.Critical => "CRITICAL", Severity.High => "HIGH", Severity.Medium => "MEDIUM", Severity.Low => "LOW", _ => severity.ToString().ToUpper(), }; if (1 == 0) { } return result; } private void DisplaySecurityNotice(string modName, ThreatVerdictInfo threatVerdict) { ((MelonBase)this).LoggerInstance.Warning("IMPORTANT SECURITY NOTICE"); ((MelonBase)this).LoggerInstance.Msg("MLVScan has detected and disabled " + modName + " before it was loaded."); if ((threatVerdict != null && threatVerdict.Kind == ThreatVerdictKind.KnownMaliciousSample) || (threatVerdict != null && threatVerdict.Kind == ThreatVerdictKind.KnownMalwareFamily)) { ((MelonBase)this).LoggerInstance.Msg("This block was reinforced by a match to previously analyzed malware."); ((MelonBase)this).LoggerInstance.Msg("If this is your first time running the game with this mod, your PC is likely safe."); ((MelonBase)this).LoggerInstance.Msg("However, if you've previously run the game with this mod, your system MAY be infected."); } else { ((MelonBase)this).LoggerInstance.Msg("This mod was blocked as a precaution based on suspicious behavior patterns."); ((MelonBase)this).LoggerInstance.Msg("Keep in mind that no detection system is perfect, and this mod may still be falsely flagged."); } ((MelonBase)this).LoggerInstance.Warning("Recommended security steps:"); ((MelonBase)this).LoggerInstance.Msg("1. Check with the modding community first - no detection is perfect"); ((MelonBase)this).LoggerInstance.Msg(" Join the modding Discord at: https://discord.gg/UD4K4chKak"); ((MelonBase)this).LoggerInstance.Msg(" Ask about this mod in the MLVScan thread of #mod-releases to confirm if it's actually malicious"); ((MelonBase)this).LoggerInstance.Msg("2. Run a full system scan with a trusted antivirus like Malwarebytes"); ((MelonBase)this).LoggerInstance.Msg(" Malwarebytes is recommended as a free and effective antivirus solution"); ((MelonBase)this).LoggerInstance.Msg("3. Use Microsoft Safety Scanner for a secondary scan"); ((MelonBase)this).LoggerInstance.Msg("4. Change important passwords if antivirus shows a threat"); ((MelonBase)this).LoggerInstance.Warning("Resources for malware removal:"); ((MelonBase)this).LoggerInstance.Msg("- Malwarebytes: https://www.malwarebytes.com/cybersecurity/basics/how-to-remove-virus-from-computer"); ((MelonBase)this).LoggerInstance.Msg("- Microsoft Safety Scanner: https://learn.microsoft.com/en-us/defender-endpoint/safety-scanner-download"); } private static void WriteSecurityNoticeToReport(StreamWriter writer) { writer.WriteLine("\n\n============== SECURITY NOTICE ==============\n"); writer.WriteLine("IMPORTANT: READ THIS SECURITY INFORMATION\n"); writer.WriteLine("MLVScan has detected and disabled this mod before it was loaded."); writer.WriteLine("This mod contains code patterns commonly associated with malware.\n"); writer.WriteLine("YOUR SYSTEM SECURITY STATUS:"); writer.WriteLine("- If this is your FIRST TIME starting the game with this mod installed:"); writer.WriteLine(" Your PC is likely SAFE as MLVScan prevented the mod from loading."); writer.WriteLine("\n- If you have PREVIOUSLY PLAYED the game with this mod loaded:"); writer.WriteLine(" Your system MAY BE INFECTED with malware. Take action immediately.\n"); writer.WriteLine("RECOMMENDED SECURITY STEPS:"); writer.WriteLine("1. Check with the modding community first - no detection system is perfect"); writer.WriteLine(" Join the modding Discord at: https://discord.gg/UD4K4chKak"); writer.WriteLine(" Ask about this mod in the #MLVScan or #report-mods channels to confirm if it's actually malicious"); writer.WriteLine("\n2. Run a full system scan with a reputable antivirus program"); writer.WriteLine(" Free option: Malwarebytes (https://www.malwarebytes.com/)"); writer.WriteLine(" Malwarebytes is recommended as a free and effective antivirus solution"); writer.WriteLine("\n3. Run Microsoft Safety Scanner as a secondary check"); writer.WriteLine(" Download: https://learn.microsoft.com/en-us/defender-endpoint/safety-scanner-download"); writer.WriteLine("\n4. Update all your software from official sources"); writer.WriteLine("\n5. Change passwords for important accounts (from a clean device if possible)"); writer.WriteLine("\n6. Monitor your accounts for any suspicious activity"); writer.WriteLine("\nDETAILED MALWARE REMOVAL GUIDES:"); writer.WriteLine("- Malwarebytes Guide: https://www.malwarebytes.com/cybersecurity/basics/how-to-remove-virus-from-computer"); writer.WriteLine("- Microsoft Safety Scanner: https://learn.microsoft.com/en-us/defender-endpoint/safety-scanner-download"); writer.WriteLine("- XWorm (Common Modding Malware) Removal Guide: https://www.pcrisk.com/removal-guides/27436-xworm-rat"); writer.WriteLine("\n============================================="); } } public class MelonLoaderServiceFactory { private readonly Instance _melonLogger; private readonly IScanLogger _scanLogger; private readonly IAssemblyResolverProvider _resolverProvider; private readonly MelonConfigManager _configManager; private readonly MelonPlatformEnvironment _environment; private readonly MLVScanConfig _fallbackConfig; public MelonLoaderServiceFactory(Instance logger) { _melonLogger = logger ?? throw new ArgumentNullException("logger"); _scanLogger = new MelonScanLogger(logger); _resolverProvider = new GameAssemblyResolverProvider(); _environment = new MelonPlatformEnvironment(); _fallbackConfig = new MLVScanConfig(); try { _configManager = new MelonConfigManager(logger); } catch (Exception ex) { _melonLogger.Error("Failed to create ConfigManager: " + ex.Message); _melonLogger.Msg("Using default configuration values"); } } public MelonConfigManager CreateConfigManager() { if (_configManager == null) { throw new InvalidOperationException("Configuration manager unavailable: failed to initialize during factory construction."); } return _configManager; } public MelonPlatformEnvironment CreateEnvironment() { return _environment; } public AssemblyScanner CreateAssemblyScanner() { MLVScanConfig mLVScanConfig = _configManager?.Config ?? _fallbackConfig; IReadOnlyList<IScanRule> rules = RuleFactory.CreateDefaultRules(); return new AssemblyScanner(rules, mLVScanConfig.Scan, _resolverProvider); } public MelonPluginScanner CreatePluginScanner() { MLVScanConfig config = _configManager?.Config ?? _fallbackConfig; return new MelonPluginScanner(_scanLogger, _resolverProvider, config, _configManager, _environment); } public MelonPluginDisabler CreatePluginDisabler() { MLVScanConfig config = _configManager?.Config ?? _fallbackConfig; return new MelonPluginDisabler(_scanLogger, config); } public PromptGeneratorService CreatePromptGenerator() { MLVScanConfig mLVScanConfig = _configManager?.Config ?? _fallbackConfig; return new PromptGeneratorService(mLVScanConfig.Scan, _scanLogger); } public IlDumpService CreateIlDumpService() { return new IlDumpService(_scanLogger, _environment); } public DeveloperReportGenerator CreateDeveloperReportGenerator() { return new DeveloperReportGenerator(_scanLogger); } public ReportUploadService CreateReportUploadService() { return new ReportUploadService(_configManager, delegate(string msg) { _melonLogger.Msg(msg); }, delegate(string msg) { _melonLogger.Warning(msg); }, delegate(string msg) { _melonLogger.Error(msg); }); } } public class MelonConfigManager : IConfigManager { private readonly Instance _logger; private readonly MelonPreferences_Category _category; private readonly MelonPreferences_Entry<bool> _enableAutoScan; private readonly MelonPreferences_Entry<bool> _enableAutoDisable; private readonly MelonPreferences_Entry<string> _minSeverityForDisable; private readonly MelonPreferences_Entry<string[]> _scanDirectories; private readonly MelonPreferences_Entry<int> _suspiciousThreshold; private readonly MelonPreferences_Entry<string[]> _whitelistedHashes; private readonly MelonPreferences_Entry<bool> _dumpFullIlReports; private readonly MelonPreferences_Entry<bool> _developerMode; private readonly MelonPreferences_Entry<bool> _enableReportUpload; private readonly MelonPreferences_Entry<bool> _reportUploadConsentAsked; private readonly MelonPreferences_Entry<bool> _reportUploadConsentPending; private readonly MelonPreferences_Entry<string> _pendingReportUploadPath; private readonly MelonPreferences_Entry<string> _reportUploadApiBaseUrl; private readonly MelonPreferences_Entry<string[]> _uploadedReportHashes; public MLVScanConfig Config { get; private set; } public MelonConfigManager(Instance logger) { _logger = logger ?? throw new ArgumentNullException("logger"); try { _category = MelonPreferences.CreateCategory("MLVScan"); _enableAutoScan = _category.CreateEntry<bool>("EnableAutoScan", true, (string)null, "Whether to scan mods at startup", false, false, (ValueValidator)null, (string)null); _enableAutoDisable = _category.CreateEntry<bool>("EnableAutoDisable", true, (string)null, "Whether to disable suspicious mods", false, false, (ValueValidator)null, (string)null); _minSeverityForDisable = _category.CreateEntry<string>("MinSeverityForDisable", "Medium", (string)null, "Minimum severity level to trigger disabling (Low, Medium, High, Critical)", false, false, (ValueValidator)null, (string)null); _scanDirectories = _category.CreateEntry<string[]>("ScanDirectories", new string[2] { "Mods", "Plugins" }, (string)null, "Directories to scan for mods", false, false, (ValueValidator)null, (string)null); _suspiciousThreshold = _category.CreateEntry<int>("SuspiciousThreshold", 1, (string)null, "How many suspicious findings required before disabling a mod", false, false, (ValueValidator)null, (string)null); _whitelistedHashes = _category.CreateEntry<string[]>("WhitelistedHashes", Array.Empty<string>(), (string)null, "List of mod SHA256 hashes to skip when scanning", false, false, (ValueValidator)null, (string)null); _dumpFullIlReports = _category.CreateEntry<bool>("DumpFullIlReports", false, (string)null, "When enabled, saves full IL dumps for scanned mods next to reports", false, false, (ValueValidator)null, (string)null); _developerMode = _category.CreateEntry<bool>("DeveloperMode", false, (string)null, "Developer mode: Shows remediation guidance to help mod developers fix false positives", false, false, (ValueValidator)null, (string)null); _enableReportUpload = _category.CreateEntry<bool>("EnableReportUpload", false, (string)null, "When enabled (and consent given), send reports to MLVScan API for false positive analysis", false, false, (ValueValidator)null, (string)null); _reportUploadConsentAsked = _category.CreateEntry<bool>("ReportUploadConsentAsked", false, (string)null, "Whether the first-run consent prompt has been shown (internal)", false, false, (ValueValidator)null, (string)null); _reportUploadConsentPending = _category.CreateEntry<bool>("ReportUploadConsentPending", false, (string)null, "Whether an upload consent popup is pending (internal)", false, false, (ValueValidator)null, (string)null); _pendingReportUploadPath = _category.CreateEntry<string>("PendingReportUploadPath", string.Empty, (string)null, "Suspicious mod path waiting for upload consent (internal)", false, false, (ValueValidator)null, (string)null); _reportUploadApiBaseUrl = _category.CreateEntry<string>("ReportUploadApiBaseUrl", "https://api.mlvscan.com", (string)null, "API base URL for report uploads", false, false, (ValueValidator)null, (string)null); _uploadedReportHashes = _category.CreateEntry<string[]>("UploadedReportHashes", Array.Empty<string>(), (string)null, "List of assembly SHA256 hashes already uploaded to the MLVScan API (internal)", false, false, (ValueValidator)null, (string)null); ((MelonEventBase<LemonAction<bool, bool>>)(object)_enableAutoScan.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_enableAutoDisable.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)_minSeverityForDisable.OnEntryValueChanged).Subscribe((LemonAction<string, string>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string[], string[]>>)(object)_scanDirectories.OnEntryValueChanged).Subscribe((LemonAction<string[], string[]>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)_suspiciousThreshold.OnEntryValueChanged).Subscribe((LemonAction<int, int>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string[], string[]>>)(object)_whitelistedHashes.OnEntryValueChanged).Subscribe((LemonAction<string[], string[]>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_dumpFullIlReports.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_developerMode.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_enableReportUpload.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_reportUploadConsentAsked.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)_reportUploadConsentPending.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)_pendingReportUploadPath.OnEntryValueChanged).Subscribe((LemonAction<string, string>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)_reportUploadApiBaseUrl.OnEntryValueChanged).Subscribe((LemonAction<string, string>)OnConfigChanged, 0, false); ((MelonEventBase<LemonAction<string[], string[]>>)(object)_uploadedReportHashes.OnEntryValueChanged).Subscribe((LemonAction<string[], string[]>)OnConfigChanged, 0, false); UpdateConfigFromPreferences(); _logger.Msg("Configuration loaded successfully"); } catch (Exception ex) { _logger.Error("Failed to initialize config system: " + ex.Message); _logger.Msg("Using fallback in-memory configuration"); Config = new MLVScanConfig(); } } public MLVScanConfig LoadConfig() { UpdateConfigFromPreferences(); return Config; } private void OnConfigChanged<T>(T oldValue, T newValue) { UpdateConfigFromPreferences(); _logger.Msg("Configuration updated"); } private void UpdateConfigFromPreferences() { Config = new MLVScanConfig { EnableAutoScan = _enableAutoScan.Value, EnableAutoDisable = _enableAutoDisable.Value, MinSeverityForDisable = ParseSeverity(_minSeverityForDisable.Value), ScanDirectories = _scanDirectories.Value, SuspiciousThreshold = _suspiciousThreshold.Value, WhitelistedHashes = _whitelistedHashes.Value, DumpFullIlReports = _dumpFullIlReports.Value, Scan = new ScanConfig { DeveloperMode = _developerMode.Value }, EnableReportUpload = _enableReportUpload.Value, ReportUploadConsentAsked = _reportUploadConsentAsked.Value, ReportUploadConsentPending = _reportUploadConsentPending.Value, PendingReportUploadPath = _pendingReportUploadPath.Value, ReportUploadApiBaseUrl = _reportUploadApiBaseUrl.Value, UploadedReportHashes = NormalizeHashes(_uploadedReportHashes.Value) }; } public void SaveConfig(MLVScanConfig newConfig) { try { _enableAutoScan.Value = newConfig.EnableAutoScan; _enableAutoDisable.Value = newConfig.EnableAutoDisable; _minSeverityForDisable.Value = FormatSeverity(newConfig.MinSeverityForDisable); _scanDirectories.Value = newConfig.ScanDirectories; _suspiciousThreshold.Value = newConfig.SuspiciousThreshold; _whitelistedHashes.Value = newConfig.WhitelistedHashes; _dumpFullIlReports.Value = newConfig.DumpFullIlReports; _developerMode.Value = newConfig.Scan?.DeveloperMode ?? false; _enableReportUpload.Value = newConfig.EnableReportUpload; _reportUploadConsentAsked.Value = newConfig.ReportUploadConsentAsked; _reportUploadConsentPending.Value = newConfig.ReportUploadConsentPending; _pendingReportUploadPath.Value = newConfig.PendingReportUploadPath ?? string.Empty; _reportUploadApiBaseUrl.Value = newConfig.ReportUploadApiBaseUrl; _uploadedReportHashes.Value = NormalizeHashes(newConfig.UploadedReportHashes); MelonPreferences.Save(); _logger.Msg("Configuration saved successfully"); } catch (Exception ex) { _logger.Error("Error saving configuration: " + ex.Message); Config = newConfig; } } public string[] GetWhitelistedHashes() { return _whitelistedHashes.Value; } public void SetWhitelistedHashes(string[] hashes) { if (hashes != null) { string[] array = (from h in hashes where !string.IsNullOrWhiteSpace(h) select h.ToLowerInvariant()).Distinct<string>(StringComparer.OrdinalIgnoreCase).ToArray(); _whitelistedHashes.Value = array; MelonPreferences.Save(); UpdateConfigFromPreferences(); _logger.Msg($"Updated whitelist with {array.Length} hash(es)"); } } public bool IsHashWhitelisted(string hash) { if (string.IsNullOrWhiteSpace(hash)) { return false; } return Config.WhitelistedHashes.Contains<string>(hash.ToLowerInvariant(), StringComparer.OrdinalIgnoreCase); } public string GetReportUploadApiBaseUrl() { return _reportUploadApiBaseUrl.Value; } public bool IsReportHashUploaded(string hash) { if (string.IsNullOrWhiteSpace(hash)) { return false; } return NormalizeHashes(_uploadedReportHashes.Value).Contains<string>(hash.ToLowerInvariant(), StringComparer.OrdinalIgnoreCase); } public void MarkReportHashUploaded(string hash) { if (HashUtility.IsValidHash(hash)) { string[] array = NormalizeHashes((_uploadedReportHashes.Value ?? Array.Empty<string>()).Append(hash)); int num = array.Length; string[] value = _uploadedReportHashes.Value; if (num != ((value != null) ? value.Length : 0) || !IsReportHashUploaded(hash)) { _uploadedReportHashes.Value = array; MelonPreferences.Save(); UpdateConfigFromPreferences(); _logger.Msg("Recorded uploaded report hash: " + hash); } } } private static Severity ParseSeverity(string severity) { if (string.IsNullOrWhiteSpace(severity)) { return Severity.Medium; } string text = severity.ToLower(); if (1 == 0) { } Severity result = text switch { "critical" => Severity.Critical, "high" => Severity.High, "medium" => Severity.Medium, "low" => Severity.Low, _ => Severity.Medium, }; if (1 == 0) { } return result; } private static string FormatSeverity(Severity severity) { if (1 == 0) { } string result = severity switch { Severity.Critical => "Critical", Severity.High => "High", Severity.Medium => "Medium", Severity.Low => "Low", _ => "Medium", }; if (1 == 0) { } return result; } private static string[] NormalizeHashes(IEnumerable<string> hashes) { return (from h in hashes ?? Array.Empty<string>() where !string.IsNullOrWhiteSpace(h) select h.ToLowerInvariant()).Distinct<string>(StringComparer.OrdinalIgnoreCase).ToArray(); } } public class MelonPlatformEnvironment : IPlatformEnvironment { private readonly string _gameRoot; private readonly string _dataDir; private readonly string _reportsDir; public string GameRootDirectory => _gameRoot; public string[] PluginDirectories => new string[2] { Path.Combine(_gameRoot, "Mods"), Path.Combine(_gameRoot, "Plugins") }; public string DataDirectory { get { if (!Directory.Exists(_dataDir)) { Directory.CreateDirectory(_dataDir); } return _dataDir; } } public string ReportsDirectory { get { if (!Directory.Exists(_reportsDir)) { Directory.CreateDirectory(_reportsDir); } return _reportsDir; } } public string ManagedDirectory { get { try { string[] directories = Directory.GetDirectories(_gameRoot, "*_Data"); string[] array = directories; foreach (string path in array) { string text = Path.Combine(path, "Managed"); if (Directory.Exists(text)) { return text; } } } catch { } string text2 = Path.Combine(_gameRoot, "MelonLoader", "Managed"); if (Directory.Exists(text2)) { return text2; } return string.Empty; } } public string SelfAssemblyPath { get { try { return typeof(MelonPlatformEnvironment).Assembly.Location; } catch { return string.Empty; } } } public string PlatformName => "MelonLoader"; public MelonPlatformEnvironment() { _gameRoot = MelonEnvironment.GameRootDirectory; _dataDir = Path.Combine(_gameRoot, "MLVScan"); _reportsDir = Path.Combine(_dataDir, "Reports"); } } public class MelonPluginScanner : PluginScannerBase { [CompilerGenerated] private sealed class <GetScanDirectories>d__2 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable { private int <>1__state; private string <>2__current; private int <>l__initialThreadId; public MelonPluginScanner <>4__this; private string[] <>s__1; private int <>s__2; private string <scanDir>5__3; string IEnumerator<string>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <GetScanDirectories>d__2(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { <>s__1 = null; <scanDir>5__3 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>s__1 = <>4__this.Config.ScanDirectories; <>s__2 = 0; break; case 1: <>1__state = -1; <scanDir>5__3 = null; <>s__2++; break; } if (<>s__2 < <>s__1.Length) { <scanDir>5__3 = <>s__1[<>s__2]; <>2__current = Path.Combine(<>4__this._environment.GameRootDirectory, <scanDir>5__3); <>1__state = 1; return true; } <>s__1 = null; return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator<string> IEnumerable<string>.GetEnumerator() { <GetScanDirectories>d__2 result; if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; result = this; } else { result = new <GetScanDirectories>d__2(0) { <>4__this = <>4__this }; } return result; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable<string>)this).GetEnumerator(); } } private readonly MelonPlatformEnvironment _environment; public MelonPluginScanner(IScanLogger logger, IAssemblyResolverProvider resolverProvider, MLVScanConfig config, IConfigManager configManager, MelonPlatformEnvironment environment) : base(logger, resolverProvider, config, configManager) { _environment = environment ?? throw new ArgumentNullException("environment"); } [IteratorStateMachine(typeof(<GetScanDirectories>d__2))] protected override IEnumerable<string> GetScanDirectories() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <GetScanDirectories>d__2(-2) { <>4__this = this }; } protected override bool IsSelfAssembly(string filePath) { try { string selfAssemblyPath = _environment.SelfAssemblyPath; if (string.IsNullOrEmpty(selfAssemblyPath)) { return false; } return Path.GetFullPath(filePath).Equals(Path.GetFullPath(selfAssemblyPath), StringComparison.OrdinalIgnoreCase); } catch { return false; } } protected override void OnScanComplete(Dictionary<string, ScannedPluginResult> results) { ScanThunderstoreModManager(results); } private void ScanThunderstoreModManager(Dictionary<string, ScannedPluginResult> results) { try { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); string path = Path.Combine(folderPath, "Thunderstore Mod Manager", "DataFolder"); if (!Directory.Exists(path)) { return; } string[] directories = Directory.GetDirectories(path); foreach (string path2 in directories) { string path3 = Path.Combine(path2, "profiles"); if (!Directory.Exists(path3)) { continue; } string[] directories2 = Directory.GetDirectories(path3); foreach (string path4 in directories2) { string text = Path.Combine(path4, "Mods"); if (Directory.Exists(text)) { Logger.Info("Scanning Thunderstore profile mods: " + text); ScanDirectory(text, results); } string text2 = Path.Combine(path4, "Plugins"); if (Directory.Exists(text2)) { Logger.Info("Scanning Thunderstore profile plugins: " + text2); ScanDirectory(text2, results); } } } } catch (Exception ex) { Logger.Error("Error scanning Thunderstore Mod Manager directories: " + ex.Message); } } } public class MelonPluginDisabler : PluginDisablerBase { private const string DisabledExtension = ".disabled"; public MelonPluginDisabler(IScanLogger logger, MLVScanConfig config) : base(logger, config) { } protected override string GetDisabledExtension() { return ".disabled"; } protected override string GetDisabledPath(string originalPath) { return Path.ChangeExtension(originalPath, ".disabled"); } } } namespace MLVScan.Services { public abstract class PluginScannerBase { protected readonly IScanLogger Logger; protected readonly IAssemblyResolverProvider ResolverProvider; protected readonly MLVScanConfig Config; protected readonly IConfigManager ConfigManager; protected readonly AssemblyScanner AssemblyScanner; protected readonly ThreatVerdictBuilder ThreatVerdictBuilder; protected PluginScannerBase(IScanLogger logger, IAssemblyResolverProvider resolverProvider, MLVScanConfig config, IConfigManager configManager) { Logger = logger ?? throw new ArgumentNullException("logger"); ResolverProvider = resolverProvider ?? throw new ArgumentNullException("resolverProvider"); Config = config ?? throw new ArgumentNullException("config"); ConfigManager = configManager ?? throw new ArgumentNullException("configManager"); IReadOnlyList<IScanRule> rules = RuleFactory.CreateDefaultRules(); AssemblyScanner = new AssemblyScanner(rules, Config.Scan, ResolverProvider); ThreatVerdictBuilder = new ThreatVerdictBuilder(); } protected abstract IEnumerable<string> GetScanDirectories(); protected abstract bool IsSelfAssembly(string filePath); protected virtual void OnScanComplete(Dictionary<string, ScannedPluginResult> results) { } public Dictionary<string, ScannedPluginResult> ScanAllPlugins(bool forceScanning = false) { Dictionary<string, ScannedPluginResult> dictionary = new Dictionary<string, ScannedPluginResult>(); if (!forceScanning && !Config.EnableAutoScan) { Logger.Info("Automatic scanning is disabled in configuration"); return dictionary; } foreach (string scanDirectory in GetScanDirectories()) { if (!Directory.Exists(scanDirectory)) { Logger.Warning("Directory not found: " + scanDirectory); } else { ScanDirectory(scanDirectory, dictionary); } } OnScanComplete(dictionary); return dictionary; } protected virtual void ScanDirectory(string directoryPath, Dictionary<string, ScannedPluginResult> results) { string[] files = Directory.GetFiles(directoryPath, "*.dll", SearchOption.AllDirectories); Logger.Info($"Found {files.Length} plugin files in {directoryPath}"); string[] array = files; foreach (string text in array) { try { ScanSingleFile(text, results); } catch (Exception ex) { Logger.Error("Error scanning " + Path.GetFileName(text) + ": " + ex.Message); } } } protected virtual void ScanSingleFile(string filePath, Dictionary<string, ScannedPluginResult> results) { string fileName = Path.GetFileName(filePath); string text = HashUtility.CalculateFileHash(filePath); if (IsSelfAssembly(filePath)) { Logger.Debug("Skipping self: " + fileName); return; } if (ConfigManager.IsHashWhitelisted(text)) { Logger.Debug("Skipping whitelisted: " + fileName); return; } List<ScanFinding> source = AssemblyScanner.Scan(filePath).ToList(); List<ScanFinding> list = source.Where((ScanFinding f) => f.Location != "Assembly scanning").ToList(); ScannedPluginResult scannedPluginResult = ThreatVerdictBuilder.Build(filePath, text, list); if (list.Count < Config.SuspiciousThreshold && !scannedPluginResult.ThreatVerdict.ShouldBypassThreshold) { return; } results[filePath] = scannedPluginResult; if (scannedPluginResult.ThreatVerdict.Kind == ThreatVerdictKind.KnownMaliciousSample || scannedPluginResult.ThreatVerdict.Kind == ThreatVerdictKind.KnownMalwareFamily) { string text2 = scannedPluginResult.ThreatVerdict.PrimaryFamily?.DisplayName; if (!string.IsNullOrWhiteSpace(text2)) { Logger.Warning($"Found {list.Count} suspicious pattern(s) in {fileName} - {scannedPluginResult.ThreatVerdict.Title}: {text2}"); } else { Logger.Warning($"Found {list.Count} suspicious pattern(s) in {fileName} - {scannedPluginResult.ThreatVerdict.Title}"); } } else { Logger.Warning($"Found {list.Count} suspicious pattern(s) in {fileName}"); } } } public class DisabledPluginInfo { public string OriginalPath { get; } public string DisabledPath { get; } public string FileHash { get; } public ThreatVerdictInfo ThreatVerdict { get; } public DisabledPluginInfo(string originalPath, string disabledPath, string fileHash, ThreatVerdictInfo threatVerdict) { OriginalPath = originalPath; DisabledPath = disabledPath; FileHash = fileHash; ThreatVerdict = threatVerdict ?? new ThreatVerdictInfo(); } } public abstract class PluginDisablerBase { protected readonly IScanLogger Logger; protected readonly MLVScanConfig Config; protected virtual string GetDisabledExtension() { return ".disabled"; } protected PluginDisablerBase(IScanLogger logger, MLVScanConfig config) { Logger = logger ?? throw new ArgumentNullException("logger"); Config = config ?? throw new ArgumentNullException("config"); } protected virtual string GetDisabledPath(string originalPath) { return Path.ChangeExtension(originalPath, GetDisabledExtension()); } protected virtual void OnPluginDisabled(string originalPath, string disabledPath, string hash) { } public List<DisabledPluginInfo> DisableSuspiciousPlugins(Dictionary<string, ScannedPluginResult> scanResults, bool forceDisable = false) { if (!forceDisable && !Config.EnableAutoDisable) { Logger.Info("Automatic disabling is turned off in configuration"); return new List<DisabledPluginInfo>(); } List<DisabledPluginInfo> list = new List<DisabledPluginInfo>(); foreach (KeyValuePair<string, ScannedPluginResult> scanResult in scanResults) { scanResult.Deconstruct(out var key, out var value); string text = key; ScannedPluginResult scannedPluginResult = value; List<ScanFinding> source = scannedPluginResult?.Findings ?? new List<ScanFinding>(); List<ScanFinding> list2 = source.Where((ScanFinding f) => f.Severity >= Config.MinSeverityForDisable).ToList(); bool flag = scannedPluginResult != null && (scannedPluginResult.ThreatVerdict?.ShouldBypassThreshold).GetValueOrDefault(); if (!flag && list2.Count == 0) { Logger.Info($"Plugin {Path.GetFileName(text)} has findings but none meet severity threshold ({Config.MinSeverityForDisable})"); continue; } if (!forceDisable && !flag && list2.Count < Config.SuspiciousThreshold) { Logger.Info("Plugin " + Path.GetFileName(text) + " below suspicious threshold"); continue; } try { DisabledPluginInfo disabledPluginInfo = DisablePlugin(text, scannedPluginResult?.ThreatVerdict); if (disabledPluginInfo != null) { list.Add(disabledPluginInfo); OnPluginDisabled(disabledPluginInfo.OriginalPath, disabledPluginInfo.DisabledPath, disabledPluginInfo.FileHash); } } catch (Exception ex) { Logger.Error("Failed to disable " + Path.GetFileName(text) + ": " + ex.Message); } } return list; } protected virtual DisabledPluginInfo DisablePlugin(string pluginPath, ThreatVerdictInfo threatVerdict) { string fileHash = HashUtility.CalculateFileHash(pluginPath); string disabledPath = GetDisabledPath(pluginPath); if (File.Exists(disabledPath)) { File.Delete(disabledPath); } File.Move(pluginPath, disabledPath); Logger.Warning("BLOCKED: " + Path.GetFileName(pluginPath)); return new DisabledPluginInfo(pluginPath, disabledPath, fileHash, threatVerdict); } } public class DeveloperReportGenerator { private readonly IScanLogger _logger; public DeveloperReportGenerator(IScanLogger logger) { _logger = logger ?? throw new ArgumentNullException("logger"); } public void GenerateConsoleReport(string modName, List<ScanFinding> findings) { if (findings == null || findings.Count == 0) { return; } _logger.Info("======= DEVELOPER SCAN REPORT ======="); _logger.Info(PlatformConstants.GetFullVersionInfo()); _logger.Info("Mod: " + modName); _logger.Info("--------------------------------------"); _logger.Info($"Total findings: {findings.Count}"); _logger.Info(""); IOrderedEnumerable<IGrouping<string, ScanFinding>> orderedEnumerable = from f in findings where f.RuleId != null group f by f.RuleId into g orderby g.Max((ScanFinding f) => f.Severity) descending select g; foreach (IGrouping<string, ScanFinding> item in orderedEnumerable) { ScanFinding scanFinding = item.First(); int num = item.Count(); _logger.Info($"[{scanFinding.Severity}] {scanFinding.Description}"); _logger.Info(" Rule: " + scanFinding.RuleId); _logger.Info($" Occurrences: {num}"); if (scanFinding.DeveloperGuidance != null) { _logger.Info(""); _logger.Info(" Developer Guidance:"); _logger.Info(" " + WrapText(scanFinding.DeveloperGuidance.Remediation, 2)); if (!string.IsNullOrEmpty(scanFinding.DeveloperGuidance.DocumentationUrl)) { _logger.Info(" Documentation: " + scanFinding.DeveloperGuidance.DocumentationUrl); } if (scanFinding.DeveloperGuidance.AlternativeApis != null && scanFinding.DeveloperGuidance.AlternativeApis.Length != 0) { _logger.Info(" Suggested APIs: " + string.Join(", ", scanFinding.DeveloperGuidance.AlternativeApis)); } if (!scanFinding.DeveloperGuidance.IsRemediable) { _logger.Warning(" No safe alternative - this pattern should not be used in mods."); } } else { _logger.Info(" (No developer guidance available for this rule)"); } _logger.Info(""); _logger.Info(" Findings:"); foreach (ScanFinding item2 in from f in item orderby f.Severity descending, f.Location select f) { _logger.Info(" - " + item2.Location); if (item2.HasCallChain && item2.CallChain != null) { _logger.Info(" Call Chain:"); foreach (CallChainNode node in item2.CallChain.Nodes) { CallChainNodeType nodeType = node.NodeType; if (1 == 0) { } string text = nodeType switch { CallChainNodeType.EntryPoint => "[ENTRY]", CallChainNodeType.IntermediateCall => "[CALL]", CallChainNodeType.SuspiciousDeclaration => "[DECL]", _ => "[???]", }; if (1 == 0) { } string text2 = text; _logger.Info(" " + text2 + " " + node.Location); if (!string.IsNullOrWhiteSpace(node.Description)) { _logger.Info(" " + node.Description); } } } if (!item2.HasDataFlow || item2.DataFlowChain == null) { continue; } _logger.Info($" Data Flow: {item2.DataFlowChain.Pattern} ({item2.DataFlowChain.Confidence * 100.0:F0}%)"); if (item2.DataFlowChain.IsCrossMethod) { _logger.Info($" Cross-method: {item2.DataFlowChain.InvolvedMethods.Count} methods"); } _logger.Info(" Data Flow Chain:"); foreach (DataFlowNode node2 in item2.DataFlowChain.Nodes) { DataFlowNodeType nodeType2 = node2.NodeType; if (1 == 0) { } string text = nodeType2 switch { DataFlowNodeType.Source => "[SOURCE]", DataFlowNodeType.Transform => "[TRANSFORM]", DataFlowNodeType.Sink => "[SINK]", DataFlowNodeType.Intermediate => "[PASS]", _ => "[???]", }; if (1 == 0) { } string text3 = text; _logger.Info(" " + text3 + " " + node2.Operation + " (" + node2.DataDescription + ") @ " + node2.Location); } } _logger.Info("--------------------------------------"); } _logger.Info(""); _logger.Info("For more information, visit: https://discord.gg/UD4K4chKak"); _logger.Info("====================================="); } public string GenerateFileReport(string modName, string hash, List<ScanFinding> findings, ThreatVerdictInfo threatVerdict = null) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("======= MLVScan Developer Report ======="); stringBuilder.AppendLine(PlatformConstants.GetFullVersionInfo()); stringBuilder.AppendLine("Mod: " + modName); stringBuilder.AppendLine("SHA256: " + hash); stringBuilder.AppendLine($"Scan Date: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); stringBuilder.AppendLine($"Total Findings: {findings.Count}"); stringBuilder.AppendLine(""); using (StringWriter writer = new StringWriter(stringBuilder)) { ThreatVerdictTextFormatter.WriteThreatVerdictSection(writer, threatVerdict); } IOrderedEnumerable<IGrouping<string, ScanFinding>> orderedEnumerable = from f in findings where f.RuleId != null group f by f.RuleId into g orderby g.Max((ScanFinding f) => f.Severity) descending select g; foreach (IGrouping<string, ScanFinding> item in orderedEnumerable) { ScanFinding scanFinding = item.First(); int num = item.Count(); stringBuilder.AppendLine("========================================="); stringBuilder.AppendLine("Rule: " + scanFinding.RuleId); stringBuilder.AppendLine($"Severity: {scanFinding.Severity}"); stringBuilder.AppendLine("Description: " + scanFinding.Description); stringBuilder.AppendLine($"Occurrences: {num}"); stringBuilder.AppendLine(""); if (scanFinding.DeveloperGuidance != null) { stringBuilder.AppendLine("--- DEVELOPER GUIDANCE ---"); stringBuilder.AppendLine("Remediation: " + scanFinding.DeveloperGuidance.Remediation); stringBuilder.AppendLine(""); if (!string.IsNullOrEmpty(scanFinding.DeveloperGuidance.DocumentationUrl)) { stringBuilder.AppendLine("Documentation: " + scanFinding.DeveloperGuidance.DocumentationUrl); } if (scanFinding.DeveloperGuidance.AlternativeApis != null && scanFinding.DeveloperGuidance.AlternativeApis.Length != 0) { stringBuilder.AppendLine("Suggested APIs:"); string[] alternativeApis = scanFinding.DeveloperGuidance.AlternativeApis; foreach (string text in alternativeApis) { stringBuilder.AppendLine(" - " + text); } } if (!scanFinding.DeveloperGuidance.IsRemediable) { stringBuilder.AppendLine(""); stringBuilder.AppendLine("WARNING: This pattern has no safe alternative and should not be used in mods."); } stringBuilder.AppendLine(""); } else { stringBuilder.AppendLine("(No developer guidance available for this rule)"); stringBuilder.AppendLine(""); } stringBuilder.AppendLine("--- FINDINGS ---"); foreach (ScanFinding item2 in item) { stringBuilder.AppendLine("Location: " + item2.Location); if (item2.HasCallChain && item2.CallChain != null) { stringBuilder.AppendLine(); stringBuilder.AppendLine("--- CALL CHAIN ANALYSIS ---"); stringBuilder.AppendLine(item2.CallChain.Summary); stringBuilder.AppendLine(); stringBuilder.AppendLine("Attack Path:"); foreach (CallChainNode node in item2.CallChain.Nodes) { CallChainNodeType nodeType = node.NodeType; if (1 == 0) { } string text2 = nodeType switch { CallChainNodeType.EntryPoint => "[ENTRY]", CallChainNodeType.IntermediateCall => "[CALL]", CallChainNodeType.SuspiciousDeclaration => "[DECL]", _ => "[???]", }; if (1 == 0) { } string text3 = text2; stringBuilder.AppendLine(" " + text3 + " " + node.Location); if (!string.IsNullOrEmpty(node.Description)) { stringBuilder.AppendLine(" " + node.Description); } } } if (item2.HasDataFlow && item2.DataFlowChain != null) { stringBuilder.AppendLine(); stringBuilder.AppendLine("--- DATA FLOW ANALYSIS ---"); stringBuilder.AppendLine($"Pattern: {item2.DataFlowChain.Pattern}"); stringBuilder.AppendLine($"Confidence: {item2.DataFlowChain.Confidence * 100.0:F0}%"); stringBuilder.AppendLine(item2.DataFlowChain.Summary); if (item2.DataFlowChain.IsCrossMethod) { stringBuilder.AppendLine(); stringBuilder.AppendLine("Cross-Method Flow:"); foreach (string involvedMethod in item2.DataFlowChain.InvolvedMethods) { stringBuilder.AppendLine(" - " + involvedMethod); } } stringBuilder.AppendLine(); stringBuilder.AppendLine("Data Flow Path:"); for (int j = 0; j < item2.DataFlowChain.Nodes.Count; j++) { DataFlowNode dataFlowNode = item2.DataFlowChain.Nodes[j]; string text4 = ((j > 0) ? " -> " : " "); DataFlowNodeType nodeType2 = dataFlowNode.NodeType; if (1 == 0) { } string text2 = nodeType2 switch { DataFlowNodeType.Source => "[SOURCE]", DataFlowNodeType.Transform => "[TRANSFORM]", DataFlowNodeType.Sink => "[SINK]", DataFlowNodeType.Intermediate => "[PASS]", _ => "[???]", }; if (1 == 0) { } string text5 = text2; stringBuilder.AppendLine(text4 + text5 + " " + dataFlowNode.Operation + " (" + dataFlowNode.DataDescription + ")"); stringBuilder.AppendLine(new string(' ', text4.Length) + " Location: " + dataFlowNode.Location); } } if (!string.IsNullOrEmpty(item2.CodeSnippet)) { stringBuilder.AppendLine("Code Snippet:"); stringBuilder.AppendLine(item2.CodeSnippet); } stringBuilder.AppendLine(); } } stringBuilder.AppendLine("========================================="); stringBuilder.AppendLine(""); stringBuilder.AppendLine("Need help? Join the community: https://discord.gg/UD4K4chKak"); return stringBuilder.ToString(); } private string WrapText(string text, int indent) { string text2 = new string(' ', indent); int num = 80 - indent; string[] array = text.Split(' '); List<string> list = new List<string>(); string text3 = ""; string[] array2 = array; foreach (string text4 in array2) { if (text3.Length + text4.Length + 1 > num) { if (!string.IsNullOrEmpty(text3)) { list.Add(text3); } text3 = text4; } else { text3 = text3 + ((text3.Length > 0) ? " " : "") + text4; } } if (!string.IsNullOrEmpty(text3)) { list.Add(text3); } return string.Join("\n " + text2, list); } } public class IlDumpService { private readonly IScanLogger _logger; private readonly IPlatformEnvironment _environment; private readonly DefaultAssemblyResolver _assemblyResolver; public IlDumpService(IScanLogger logger, IPlatformEnvironment environment) { _logger = logger ?? throw new ArgumentNullException("logger"); _environment = environment ?? throw new ArgumentNullException("environment"); _assemblyResolver = BuildResolver(); } public bool TryDumpAssembly(string assemblyPath, string outputPath) { //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_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrWhiteSpace(assemblyPath) || string.IsNullOrWhiteSpace(outputPath)) { return false; } try { Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); ReaderParameters val = new ReaderParameters { ReadWrite = false, InMemory = true, ReadSymbols = false, AssemblyResolver = (IAssemblyResolver)(object)_assemblyResolver }; AssemblyDefinition val2 = AssemblyDefinition.ReadAssembly(assemblyPath, val); using StreamWriter streamWriter = new StreamWriter(outputPath); streamWriter.WriteLine("; Full IL dump for " + Path.GetFileName(assemblyPath)); streamWriter.WriteLine($"; Generated: {DateTime.Now}"); streamWriter.WriteLine("; Platform: " + _environment.PlatformName); streamWriter.WriteLine(); Enumerator<ModuleDefinition> enumerator = val2.Modules.GetEnumerator(); try { while (enumerator.MoveNext()) { ModuleDefinition current = enumerator.Current; streamWriter.WriteLine(".module " + ((ModuleReference)current).Name); streamWriter.WriteLine(); Enumerator<TypeDefinition> enumerator2 = current.Types.GetEnumerator(); try { while (enumerator2.MoveNext()) { TypeDefinition current2 = enumerator2.Current; WriteType(current2, streamWriter); } } finally { ((IDisposable)enumerator2).Dispose(); } } } finally { ((IDisposable)enumerator).Dispose(); } _logger.Info("Saved IL dump to: " + outputPath); return true; } catch (Exception ex) { _logger.Error("Failed to dump IL for " + Path.GetFileName(assemblyPath) + ": " + ex.Message); return false; } } private DefaultAssemblyResolver BuildResolver() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown DefaultAssemblyResolver val = new DefaultAssemblyResolver(); string gameRootDirectory = _environment.GameRootDirectory; if (Directory.Exists(gameRootDirectory)) { ((BaseAssemblyResolver)val).AddSearchDirectory(gameRootDirectory); } string managedDirectory = _environment.ManagedDirectory; if (!string.IsNullOrEmpty(managedDirectory) && Directory.Exists(managedDirectory)) { ((BaseAssemblyResolver)val).AddSearchDirectory(managedDirectory); } string[] pluginDirectories = _environment.PluginDirectories; foreach (string text in pluginDirectories) { if (Directory.Exists(text)) { ((BaseAssemblyResolver)val).AddSearchDirectory(text); } } return val; } private static void WriteType(TypeDefinition type, StreamWriter writer) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) writer.WriteLine(".class " + ((MemberReference)type).FullName); Enumerator<MethodDefinition> enumerator = type.Methods.GetEnumerator(); try { while (enumerator.MoveNext()) { MethodDefinition current = enumerator.Current; WriteMethod(current, writer); } } finally { ((IDisposable)enumerator).Dispose(); } Enumerator<TypeDefinition> enumerator2 = type.NestedTypes.GetEnumerator(); try { while (enumerator2.MoveNext()) { TypeDefinition current2 = enumerator2.Current; WriteType(current2, writer); } } finally { ((IDisposable)enumerator2).Dispose(); } } private static void WriteMethod(MethodDefinition method, StreamWriter writer) { //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) try { string text = string.Join(", ", ((IEnumerable<ParameterDefinition>)((MethodReference)method).Parameters).Select((ParameterDefinition p) => ((MemberReference)((ParameterReference)p).ParameterType).FullName + " " + ((ParameterReference)p).Name)); writer.WriteLine(" .method " + ((MemberReference)((MethodReference)method).ReturnType).FullName + " " + ((MemberReference)method).Name + "(" + text + ")"); if (!method.HasBody) { writer.WriteLine(" // No body (abstract / external)"); writer.WriteLine(); return; } writer.WriteLine(" {"); Enumerator<Instruction> enumerator = method.Body.Instructions.GetEnumerator(); try { while (enumerator.MoveNext()) { Instruction current = enumerator.Current; string text2 = FormatOperand(current.Operand); string text3 = $" IL_{current.Offset:X4}: {current.OpCode}"; if (!string.IsNullOrEmpty(text2)) { text3 = text3 + " " + text2; } writer.WriteLine(text3); } } finally { ((IDisposable)enumerator).Dispose(); } writer.WriteLine(" }"); writer.WriteLine(); } catch (Exception ex) { writer.WriteLine(" // Failed to dump method " + ((MemberReference)method).Name + ": " + ex.Message); writer.WriteLine(); } } private static string FormatOperand(object operand) { if (1 == 0) { } string result; if (operand != null) { if (!(operand is string text)) { MethodReference val = (MethodReference)((operand is MethodReference) ? operand : null); if (val == null) { FieldReference val2 = (FieldReference)((operand is FieldReference) ? operand : null); if (val2 == null) { TypeReference val3 = (TypeReference)((operand is TypeReference) ? operand : null); if (val3 == null) { Instruction val4 = (Instruction)((operand is Instruction) ? operand : null); result = ((val4 != null) ? $"IL_{val4.Offset:X4}" : ((!(operand is Instruction[] source)) ? (operand.ToString() ?? string.Empty) : string.Join(", ", source.Select((Instruction t) => $"IL_{t.Offset:X4}")))); } else { result = ((MemberReference)val3).FullName; } } else { result = ((MemberReference)val2).FullName; } } else { result = ((MemberReference)val).FullName; } } else { result = "\"" + text + "\""; } } else { result = string.Empty; } if (1 == 0) { } return result; } } public class PromptGeneratorService { private readonly ScanConfig _config; private readonly IScanLogger _logger; public PromptGeneratorService(ScanConfig config, IScanLogger logger) { _config = config ?? throw new ArgumentNullException("config"); _logger = logger ?? throw new ArgumentNullException("logger"); } public string GeneratePrompt(string modPath, List<ScanFinding> findings) { if (findings == null || !findings.Any()) { return "No suspicious findings to analyze."; } string fileName = Path.GetFileName(modPath); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Mod Security Analysis Request"); stringBuilder.AppendLine(); stringBuilder.AppendLine("I need to determine if the following mod is malicious or a false positive. Below is a security scan report generated by MLVScan (a security tool for MelonLoader mods)."); stringBuilder.AppendLine(); stringBuilder.AppendLine("## Mod Information"); stringBuilder.AppendLine("- **Filename**: " + fileName); stringBuilder.AppendLine($"- **Scan Date**: {DateTime.Now}"); stringBuilder.AppendLine($"- **Total Suspicious Patterns**: {findings.Count}"); stringBuilder.AppendLine(); Dictionary<Severity, int> dictionary = (from f in findings group f by f.Severity into g orderby (int)g.Key descending select g).ToDictionary((IGrouping<Severity, ScanFinding> g) => g.Key, (IGrouping<Severity, ScanFinding> g) => g.Count()); stringBuilder.AppendLine("## Severity Breakdown"); foreach (KeyValuePair<Severity, int> item in dictionary) { stringBuilder.AppendLine($"- **{FormatSeverityLabel(item.Key)}**: {item.Value} issue(s)"); } stringBuilder.AppendLine(); stringBuilder.AppendLine("## Detailed Findings"); Dictionary<string, List<ScanFinding>> dictionary2 = (from f in findings group f by f.Description).ToDictionary((IGrouping<string, ScanFinding> g) => g.Key, (IGrouping<string, ScanFinding> g) => g.ToList()); var (dictionary5, dictionary6) = ExtractCodeBlocks(modPath, findings); foreach (KeyValuePair<string, List<ScanFinding>> item2 in dictionary2) { stringBuilder.AppendLine("### " + item2.Key); stringBuilder.AppendLine($"- **Severity**: {item2.Value[0].Severity}"); stringBuilder.AppendLine($"- **Occurrences**: {item2.Value.Count}"); stringBuilder.AppendLine("- **Locations & Snippets**:"); foreach (ScanFinding item3 in item2.Value.Take(5)) { stringBuilder.AppendLine(" - **Location**: " + item3.Location); if (!string.IsNullOrEmpty(item3.CodeSnippet)) { stringBuilder.AppendLine(" **IL Snippet (Exact location of suspicious call)**:"); stringBuilder.AppendLine(" ```"); string[] array = item3.CodeSnippet.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text in array) { stringBuilder.AppendLine(" " + text); } stringBuilder.AppendLine(" ```"); } if (dictionary5.TryGetValue(item3.Location, out var value) && !string.IsNullOrWhiteSpace(value)) { stringBuilder.AppendLine(" **Attempted C# Decompilation (Entire Method Context & Type Info)**:"); stringBuilder.AppendLine(" ```csharp"); string[] array2 = value.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text2 in array2) { stringBuilder.AppendLine(" " + text2); } stringBuilder.AppendLine(" ```"); } if (dictionary6.TryGetValue(item3.Location, out var value2) && !string.IsNullOrWhiteSpace(value2)) { stringBuilder.AppendLine(" **Surrounding Class Structure (Member Signatures Only)**:"); stringBuilder.AppendLine(" ```csharp"); string[] array3 = value2.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text3 in array3) { stringBuilder.AppendLine(" " + text3); } stringBuilder.AppendLine(" ```"); } stringBuilder.AppendLine(); } if (item2.Value.Count > 5) { stringBuilder.AppendLine($" - *And {item2.Value.Count - 5} more occurrences (details omitted for brevity)*"); } stringBuilder.AppendLine(); } string value3 = ExtractAssemblyMetadata(modPath); if (!string.IsNullOrEmpty(value3)) { stringBuilder.AppendLine("## Assembly Metadata"); stringBuilder.AppendLine(value3); stringBuilder.AppendLine(); } stringBuilder.AppendLine("## Request"); stringBuilder.AppendLine("Based on this scan report and code analysis, please help me determine:"); stringBuilder.AppendLine("1. Is this mod likely to be malicious or is it a false positive?"); stringBuilder.AppendLine("2. If potentially malicious, what specific security risks does it pose?"); stringBuilder.AppendLine("3. What is the intent of the suspicious code? Is there a benign explanation?"); stringBuilder.AppendLine("4. What further actions should I take? (Whitelist it, delete it, report it, etc.)"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Please explain your reasoning with reference to the specific code patterns and provide context about:"); stringBuilder.AppendLine("- Whether these patterns are common in legitimate mods or game utilities"); stringBuilder.AppendLine("- Alternative explanations for the suspicious patterns"); stringBuilder.AppendLine("- Your confidence level in the assessment"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Important context: This is a mod for a game using MelonLoader (a mod loading framework). Legitimate mods generally don't need to use system-level APIs like shell execution, registry access, etc."); return stringBuilder.ToString(); } private Tuple<Dictionary<string, string>, Dictionary<string, string>> ExtractCodeBlocks(string modPath, List<ScanFinding> findings) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Expected O, but got Unknown //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_007a: 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) Dictionary<string, string> dictionary = new Dictionary<string, string>(); Dictionary<string, string> dictionary2 = new Dictionary<string, string>(); try { if (!File.Exists(modPath)) { return Tuple.Create(dictionary, dictionary2); } ReaderParameters val = new ReaderParameters { ReadWrite = false, InMemory = true, ReadSymbols = false }; AssemblyDefinition val2 = AssemblyDefinition.ReadAssembly(modPath, val); try { Dictionary<string, List<string>> dictionary3 = new Dictionary<string, List<string>>(); Enumerator<ModuleDefinition> enumerator = val2.Modules.GetEnumerator(); try { while (enumerator.MoveNext()) { ModuleDefinition current = enumerator.Current; Enumerator<TypeDefinition> enumerator2 = current.Types.GetEnumerator(); try { while (enumerator2.MoveNext()) { TypeDefinition current2 = enumerator2.Current; CollectSuspiciousStrings(current2, dictionary3); } } finally { ((IDisposable)enumerator2).Dispose(); } } } finally { ((IDisposable)enumerator).Dispose(); } if (dictionary3.Any()) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("// Notable string literals found in the assembly:"); stringBuilder.AppendLine(); foreach (string item in dictionary3.Keys.OrderBy((string k) => k)) { stringBuilder.AppendLine("// Potential " + item + ":"); foreach (string item2 in dictionary3[item].Take(10)) { stringBuilder.AppendLine("// \"" + EscapeStringForCode(item2) + "\""); } if (dictionary3[item].Count > 10) { stringBuilder.AppendLine($"// ... and {dictionary3[item].Count - 10} more"); } stringBuilder.AppendLine(); } dictionary["SuspiciousStrings"] = stringBuilder.ToString(); } foreach (ScanFinding finding in findings) { try { string location = finding.Location; if (location == "Assembly scanning" || !location.Contains(".")) { continue; } string empty = string.Empty; string fullTypeName; string methodNameFromFinding; if (location.Contains(":")) { string[] array = location.Split(':'); string text = array[0]; empty = array[1]; int num = text.LastIndexOf('.'); if (num > 0) { fullTypeName = text.Substring(0, num); methodNameFromFinding = text.Substring(num + 1); goto IL_032f; } } else { int num2 = location.LastIndexOf('.'); if (num2 > 0) { fullTypeName = location.Substring(0, num2); methodNameFromFinding = location.Substring(num2 + 1); goto IL_032f; } } goto end_IL_0244; IL_032f: TypeDefinition val3 = FindType(val2.MainModule, fullTypeName); if (val3 == null) { continue; } if (!dictionary2.ContainsKey(finding.Location) || dictionary2[finding.Location] == string.Empty) { dictionary2[finding.Location] = GenerateClassStructure(val3, methodNameFromFinding); } MethodDefinition val4 = ((IEnumerable<MethodDefinition>)val3.Methods).FirstOrDefault((Func<MethodDefinition, bool>)((MethodDefinition m) => ((MemberReference)m).Name == methodNameFromFinding)); if (val4 == null) { if (!dictionary.ContainsKey(finding.Location)) { dictionary[finding.Location] = "// DllImport or external method: " + finding.Location + ". Primary context is the IL snippet and class structure."; } continue; } if (!val4.HasBody && !val4.IsAbstract) { if (!dictionary.ContainsKey(finding.Location)) { dictionary[finding.Location] = "// Method " + methodNameFromFinding + " has no body or is abstract."; } continue; } string text2 = DecompileMethod(val4); if (string.IsNullOrEmpty(text2)) { continue; } StringBuilder stringBuilder2 = new StringBuilder(); stringBuilder2.AppendLine("// Context: Method is part of " + ((MemberReference)val3).FullName); if (val3.HasCustomAttributes) { stringBuilder2.AppendLine("// Type attributes:"); foreach (CustomAttribute item3 in ((IEnumerable<CustomAttribute>)val3.CustomAttributes).Take(5)) { stringBuilder2.AppendLine("// - " + ((MemberReference)item3.AttributeType).Name); } if (val3.CustomAttributes.Count > 5) { stringBuilder2.AppendLine($"// - ...and {val3.CustomAttributes.Count - 5} more"); } } if (val3.BaseType != null && ((MemberReference)val3.BaseType).FullName != "System.Object") { stringBuilder2.AppendLine("// Inherits from: " + ((MemberReference)val3.BaseType).FullName); } List<MethodDefinition> list = FindRelatedSuspiciousMethods(val3, val4); if (list.Any()) { stringBuilder2.AppendLine("// Other suspicious methods in this class (names only):"); foreach (MethodDefinition item4 in list.Take(3)) { stringBuilder2.AppendLine("// - " + ((MemberReference)item4).Name); } if (list.Count > 3) { stringBuilder2.AppendLine($"// - ...and {list.Count - 3} more"); } } stringBuilder2.AppendLine(); stringBuilder2.AppendLine("// Finding Description: " + finding.Description); stringBuilder2.AppendLine($"// Severity: {finding.Severity}"); stringBuilder2.AppendLine(); dictionary[finding.Location] = stringBuilder2.ToString() + text2; end_IL_0244:; } catch (Exception ex) { _logger.Error("Failed to extract code for " + finding.Location + ": " + ex.Message); dictionary[finding.Location] = "// Error extracting detailed code for " + finding.Location + ": " + ex.Message; } } if (((IEnumerable<Resource>)val2.MainModule.Resources).Any()) { StringBuilder stringBuilder3 = new StringBuilder(); stringBuilder3.AppendLine("// Assembly Resources (could contain hidden payloads):"); foreach (Resource item5 in ((IEnumerable<Resource>)val2.MainModule.Resources).Take(20)) { stringBuilder3.AppendLine("// " + item5.Name + " - " + GetResourceTypeName(item5)); } if (val2.MainModule.Resources.Count > 20) { stringBuilder3.AppendLine($"// ...and {val2.MainModule.Resources.Count - 20} more resources"); } dictionary["Assembly.Resources"] = stringBuilder3.ToString(); } } finally { ((IDisposable)val2)?.Dispose(); } } catch (Exception ex2) { _logger.Error("Failed to extract code blocks from " + modPath + ": " + ex2.Message); } return Tuple.Create(dictionary, dictionary2); } private void CollectSuspiciousStrings(TypeDefinition type, Dictionary<string, List<string>> suspiciousStrings) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) Enumerator<MethodDefinition> enumerator = type.Methods.GetEnumerator(); try { while (enumerator.MoveNext()) { MethodDefinition current = enumerator.Current; if (!current.HasBody) { continue; } Enumerator<Instruction> enumerator2 = current.Body.Instructions.GetEnumerator(); try { while (enumerator2.MoveNext()) { Instruction current2 = enumerator2.Current; OpCode opCode = current2.OpCode; if (((OpCode)(ref opCode)).Name == "ldstr" && current2.Operand is string text && IsSuspiciousString(text, out var category)) { if (!suspiciousStrings.ContainsKey(category)) { suspiciousStrings[category] = new List<string>(); } if (!suspiciousStrings[category].Contains(text)) { suspiciousStrings[category].Add(text); } } } } finally { ((IDisposable)enumerator2).Dispose(); } } } finally { ((IDisposable)enumerator).Dispose(); } Enumerator<TypeDefinition> enumerator3 = type.NestedTypes.GetEnumerator(); try { while (enumerator3.MoveNext()) { TypeDefinition current3 = enumerator3.Current; CollectSuspiciousStrings(current3, suspiciousStrings); } } finally { ((IDisposable)enumerator3).Dispose(); } } private bool IsSuspiciousString(string value, out string category) { if (string.IsNullOrEmpty(value)) { category = string.Empty; return false; } if (value.Length > 20 && IsLikelyBase64(value)) { category = "Base64 encoded data"; return true; } if (value.StartsWith("http://") || value.StartsWith("https://") || value.StartsWith("ftp://") || value.StartsWith("ws://")) { category = "URL"; return true; } if (value.Contains(".exe") || value.Contains(".dll") || value.Contains(".bat") || value.Contains(".cmd") || value.Contains(".ps1") || value.Contains(".vbs")) { category = "executable file reference"; return true; } if (IsLikelyIPAddress(value)) { category = "IP address"; return true; } if (value.StartsWith("HKEY_") || value.Contains("\\Software\\") || value.Contains("\\Microsoft\\") || value.Contains("\\System\\")) { category = "registry path"; return true; } if (value.StartsWith("cmd ") || value.StartsWith("powershell ") || value.Contains(" /c ") || value.Contains(" /k ")) { category = "command line"; return true; } if (value.Contains("encrypt") || value.Contains("decrypt") || value.Contains("aes") || value.Contains("rsa") || value.Contains("md5") || value.Contains("sha") || value.Contains("hash")) { category = "cryptographic reference"; return true; } if ((value.Contains(".") && !value.Contains(" ") && !value.Contains("\\") && !value.EndsWith(".cs") && !value.EndsWith(".txt") && value.Length > 5) || value.Contains("pastebin") || value.Contains("discord") || value.Contains("webhook")) { category = "domain or web service"; return true; } category = string.Empty; return false; } private bool IsLikelyBase64(string value) { if (value.Length % 4 != 0) { return false; } foreach (char c2 in value) { if ((c2 < 'A' || c2 > 'Z') && (c2 < 'a' || c2 > 'z') && (c2 < '0' || c2 > '9') && c2 != '+' && c2 != '/' && c2 != '=') { return false; } } return value.Length > 20 && value.Any((char c) => c >= 'A' && c <= 'Z') && value.Any((char c) => c >= 'a' && c <= 'z') && value.Any((char c) => c >= '0' && c <= '9'); } private bool IsLikelyIPAddress(string value) { string pattern = "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"; string pattern2 = "^[0-9a-fA-F:]+"; return Regex.IsMatch(value, pattern) || Regex.IsMatch(value, pattern2); } private string GetResourceTypeName(Resource resource) { //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) EmbeddedResource val = (EmbeddedResource)(object)((resource is EmbeddedResource) ? resource : null); if (val != null) { using (Stream stream = val.GetResourceStream()) { if (stream.Length > 0) { byte[] array = new byte[Math.Min(stream.Length, 16L)]; stream.Read(array, 0, array.Length); if (array.Length >= 2 && array[0] == 77 && array[1] == 90) { return "PE File/DLL (MZ header)"; } if (array.Length >= 4 && array[0] == 127 && array[1] == 69 && array[2] == 76 && array[3] == 70) { return "ELF Binary"; } if (array.Length >= 4 && array[0] == 80 && array[1] == 75 && array[2] == 3 && array[3] == 4) { return "ZIP Archive"; } if (array.Length >= 2 && array[0] == byte.MaxValue && array[1] == 216) { return "JPEG Image"; } if (array.Length >= 3 && array[0] == 71 && array[1] == 73 && array[2] == 70) { return "GIF Image"; } if (array.Length >= 4 && ((array[0] == 137 && array[1] == 80 && array[2] == 78 && array[3] == 71) || (array[0] == 66 && array[1] == 77))) { return "PNG or BMP Image"; } return $"Binary data ({stream.Length} bytes)"; } return "Empty resource"; } } ResourceType resourceType = resource.ResourceType; return ((object)(ResourceType)(ref resourceType)).ToString(); } private List<MethodDefinition> FindRelatedSuspiciousMethods(TypeDefinition type, MethodDefinition currentMethod) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) List<MethodDefinition> list = new List<MethodDefinition>(); Enumerator<MethodDefinition> enumerator = type.Methods.GetEnumerator(); try { while (enumerator.MoveNext()) { MethodDefinition current = enumerator.Current; if (current == currentMethod || !current.HasBody) { continue; } Enumerator<Instruction> enumerator2 = current.Body.Instructions.GetEnumerator(); try { while (enumerator2.MoveNext()) { Instruction current2 = enumerator2.Current; OpCode opCode = current2.OpCode; if (!(((OpCode)(ref opCode)).Name == "call")) { opCode = current2.OpCode; if (!(((OpCode)(ref opCode)).Name == "callvirt")) { opCode = current2.OpCode; if (!(((OpCode)(ref opCode)).Name == "newobj")) { continue; } } } object operand = current2.Operand; MethodReference val = (MethodReference)((operand is MethodReference) ? operand : null); if (val != null && IsSuspiciousMethodCall(val)) { list.Add(current); break; } } } finally { ((IDisposable)enumerator2).Dispose(); } } } finally { ((IDisposable)enumerator).Dispose(); } return list; } private TypeDefinition FindType(ModuleDefinition module, string fullTypeName) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) Enumerator<TypeDefinition> enumerator = module.Types.GetEnumerator(); try { while (enumerator.MoveNext()) { TypeDefinition current = enumerator.Current; if (((MemberReference)current).FullName == fullTypeName) {