From fad7cc731a6b95bb889bc3261dd9653cb8ad1c3a Mon Sep 17 00:00:00 2001 From: ifBars Date: Fri, 18 Sep 2026 01:30:57 -0700 Subject: [PATCH 1/4] Close remote text execution detection gap --- .../Integration/NewSampleThreatFamilyTests.cs | 7 +- .../ThreatFamilyQuarantineTests.cs | 18 +++- .../CoordinatedPayloadDeliveryRuleTests.cs | 63 +++++++++++- .../NewThreatFamilyClassifierTests.cs | 39 ++++++-- .../ThreatDispositionClassifierTests.cs | 19 ++++ .../Rules/CoordinatedPayloadDeliveryRule.cs | 98 +++++++++++++++++++ .../ThreatDispositionClassifier.cs | 1 + .../ThreatFamilyCatalog.NewSamples.cs | 60 +++++++++++- Services/ThreatIntel/ThreatFamilyCatalog.cs | 1 + 9 files changed, 292 insertions(+), 14 deletions(-) diff --git a/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs b/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs index bc713ae..61a60f8 100644 --- a/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs +++ b/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs @@ -8,6 +8,7 @@ namespace MLVScan.Core.Tests.Integration; public class NewSampleThreatFamilyTests { [SkippableTheory] + [InlineData(@"AutoBarnCoopDoor\AutoBarnCoopDoor.dll.di", "family-remote-text-shell-exec-v1", "remote-text-hidden-shell-command")] [InlineData(@"BetterPatrols1\bin\Win64_Shipping_Client\BetterPatrols.dll.di", "family-pawns-app-dropper-v1", "archive-userprofile-runkey-hidden-launch")] [InlineData(@"BetterPatrols2\bin\Win64_Shipping_Client\BetterPatrols.dll.di", "family-pawns-app-dropper-v1", "archive-userprofile-runkey-hidden-launch")] [InlineData(@"BloodAndBanners\bin\Win64_Shipping_Client\BloodAndBanners.Core.dll.di", "family-pawns-app-dropper-v1", "archive-userprofile-runkey-hidden-launch")] @@ -44,6 +45,7 @@ public void Scan_BloodAndBannersWrapper_WithoutCoreCompanion_ShouldRemainClean() } [SkippableTheory] + [InlineData(@"AutoBarnCoopDoor\AutoBarnCoopDoor.dll.di", "family-remote-text-shell-exec-v1")] [InlineData(@"BetterPatrols1\bin\Win64_Shipping_Client\BetterPatrols.dll.di", "family-pawns-app-dropper-v1")] [InlineData(@"BetterPatrols2\bin\Win64_Shipping_Client\BetterPatrols.dll.di", "family-pawns-app-dropper-v1")] [InlineData(@"BloodAndBanners\bin\Win64_Shipping_Client\BloodAndBanners.Core.dll.di", "family-pawns-app-dropper-v1")] @@ -72,7 +74,7 @@ private static string GetSamplePath(string relativePath) while (current != null) { - string candidate = Path.Combine(current, "TO_ANALYZE", relativePath); + string candidate = Path.Combine(current, "QUARANTINE", relativePath); if (File.Exists(candidate)) { return candidate; @@ -81,7 +83,6 @@ private static string GetSamplePath(string relativePath) current = Directory.GetParent(current)?.FullName; } - Skip.If(true, $"Static sample not found in TO_ANALYZE: {relativePath}"); - return string.Empty; + throw new SkipException($"Static sample not found in QUARANTINE: {relativePath}"); } } diff --git a/MLVScan.Core.Tests/Integration/ThreatFamilyQuarantineTests.cs b/MLVScan.Core.Tests/Integration/ThreatFamilyQuarantineTests.cs index ad407de..6f4689a 100644 --- a/MLVScan.Core.Tests/Integration/ThreatFamilyQuarantineTests.cs +++ b/MLVScan.Core.Tests/Integration/ThreatFamilyQuarantineTests.cs @@ -47,6 +47,21 @@ public class ThreatFamilyQuarantineTests @"malware-suspicious-with-findings\RexonV5Menu.dll.di" }; + private static readonly HashSet RecursiveNonMaliciousPackageCompanions = new(StringComparer.OrdinalIgnoreCase) + { + @"BetterPatrols1\bin\Win64_Shipping_Client\0Harmony.dll.di", + @"BetterPatrols1\bin\Win64_Shipping_Client\MCMv5.dll.di", + @"BetterPatrols1\bin\Win64_Shipping_Client\Newtonsoft.Json.dll.di", + @"BetterPatrols1\bin\Win64_Shipping_Client\System.Management.dll.di", + @"BetterPatrols1\bin\Win64_Shipping_Client\System.Numerics.Vectors.dll.di", + @"BetterPatrols2\bin\Win64_Shipping_Client\0Harmony.dll.di", + @"BetterPatrols2\bin\Win64_Shipping_Client\MCMv5.dll.di", + @"BetterPatrols2\bin\Win64_Shipping_Client\Newtonsoft.Json.dll.di", + @"BetterPatrols2\bin\Win64_Shipping_Client\System.Management.dll.di", + @"BetterPatrols2\bin\Win64_Shipping_Client\System.Numerics.Vectors.dll.di", + @"BloodAndBanners\bin\Win64_Shipping_Client\BloodAndBanners.dll.di" + }; + private readonly ITestOutputHelper _output; private readonly string? _quarantineFolder; @@ -253,7 +268,8 @@ public void Scan_RecursiveQuarantineSamplesWithBehaviorEvidence_ShouldNotClassif foreach (var path in samplePaths) { var relativePath = Path.GetRelativePath(_quarantineFolder!, path); - if (RecursiveSamplesAwaitingBehaviorModel.Contains(relativePath)) + if (RecursiveSamplesAwaitingBehaviorModel.Contains(relativePath) || + RecursiveNonMaliciousPackageCompanions.Contains(relativePath)) { continue; } diff --git a/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs b/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs index 46aff88..5e6d20e 100644 --- a/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs +++ b/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs @@ -52,6 +52,42 @@ public void PostAnalysisRefine_ArchiveInstallerWithVisibleLaunch_ReturnsNoFindin findings.Should().BeEmpty(); } + [Fact] + public void PostAnalysisRefine_RemoteTextPassedToHiddenShell_ReturnsFinding() + { + using var assembly = CreateRemoteTextShellLauncher(); + var existing = new[] + { + ProcessFinding( + "Target: \"powershell.exe\". Arguments: " + + "[Evasion: UseShellExecute set, CreateNoWindow=true]", + "Test.Sample.RemoteLauncher.Run:42") + }; + + var findings = _rule.PostAnalysisRefine(assembly.MainModule, existing).ToList(); + + findings.Should().ContainSingle(finding => + finding.RuleId == "CoordinatedPayloadDeliveryRule" && + finding.Description.Contains("remote text", StringComparison.OrdinalIgnoreCase) && + finding.Description.Contains("shell command", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void PostAnalysisRefine_RemoteVersionCheckAndVisibleShell_ReturnsNoFinding() + { + using var assembly = CreateRemoteTextShellLauncher(); + var existing = new[] + { + ProcessFinding( + "Target: \"powershell.exe\". Arguments: -File show-version.ps1", + "Test.Sample.RemoteLauncher.Run:42") + }; + + var findings = _rule.PostAnalysisRefine(assembly.MainModule, existing).ToList(); + + findings.Should().BeEmpty(); + } + private static AssemblyDefinition CreateArchiveInstaller(bool includeRegistryPersistence) { var assembly = AssemblyDefinition.CreateAssembly( @@ -81,6 +117,29 @@ private static AssemblyDefinition CreateArchiveInstaller(bool includeRegistryPer return assembly; } + private static AssemblyDefinition CreateRemoteTextShellLauncher() + { + var assembly = AssemblyDefinition.CreateAssembly( + new AssemblyNameDefinition("RemoteTextShellLauncher", new Version(1, 0)), + "RemoteTextShellLauncher", + ModuleKind.Dll); + var module = assembly.MainModule; + var type = new TypeDefinition("Test.Sample", "RemoteLauncher", TypeAttributes.Public, module.TypeSystem.Object); + module.Types.Add(type); + + var method = new MethodDefinition("Run", MethodAttributes.Public | MethodAttributes.Static, module.TypeSystem.Void); + method.Body = new MethodBody(method); + type.Methods.Add(method); + var il = method.Body.GetILProcessor(); + il.Emit(OpCodes.Ldstr, "https://example.test/profile"); + il.Emit(OpCodes.Call, Method(module, "System.Net.Http", "HttpClient", "GetStringAsync")); + il.Emit(OpCodes.Call, Method(module, "System.Text.RegularExpressions", "Regex", "Match")); + il.Emit(OpCodes.Call, Method(module, "System.Diagnostics", "ProcessStartInfo", "set_Arguments")); + il.Emit(OpCodes.Call, Method(module, "System.Diagnostics", "Process", "Start")); + il.Emit(OpCodes.Ret); + return assembly; + } + private static MethodReference Method(ModuleDefinition module, string ns, string type, string name) { return new MethodReference( @@ -89,9 +148,9 @@ private static MethodReference Method(ModuleDefinition module, string ns, string new TypeReference(ns, type, module, module.TypeSystem.CoreLibrary)); } - private static ScanFinding ProcessFinding(string description) + private static ScanFinding ProcessFinding(string description, string location = "Test.Sample.Installer.Install") { - return new ScanFinding("Test.Sample.Installer.Install", description, Severity.High, description) + return new ScanFinding(location, description, Severity.High, description) { RuleId = "ProcessStartRule" }; diff --git a/MLVScan.Core.Tests/Unit/Services/NewThreatFamilyClassifierTests.cs b/MLVScan.Core.Tests/Unit/Services/NewThreatFamilyClassifierTests.cs index 48e7a7e..971ab0d 100644 --- a/MLVScan.Core.Tests/Unit/Services/NewThreatFamilyClassifierTests.cs +++ b/MLVScan.Core.Tests/Unit/Services/NewThreatFamilyClassifierTests.cs @@ -85,12 +85,38 @@ public void Classify_OrdinaryRpcJavaClient_DoesNotReturnBlockchainJavaFamily() matches.Should().NotContain(match => match.FamilyId == "family-blockchain-java-stager-v1"); } + [Fact] + public void Classify_RemoteTextHiddenShellExecution_ReturnsBehaviorFamily() + { + var findings = new[] + { + Finding( + "CoordinatedPayloadDeliveryRule", + "Detected remote text retrieval transformed into a runtime-computed hidden shell command in the same method."), + Finding( + "ProcessStartRule", + "Target: \"powershell.exe\". Arguments: [Evasion: CreateNoWindow=true]") + }; + + var matches = new ThreatFamilyClassifier().Classify(findings, sha256Hash: null); + + matches.Should().ContainSingle(match => + match.FamilyId == "family-remote-text-shell-exec-v1" && + match.VariantId == "remote-text-hidden-shell-command" && + match.MatchKind == ThreatMatchKind.BehaviorVariant && + !match.ExactHashMatch); + } + [Theory] - [InlineData("4f1f3bc0028d9059939c9218dc6d975974b656f4c540ee83a51a7a39278c9c8b", "family-pawns-app-dropper-v1")] - [InlineData("9dcc2c192b1b5e8bb9e9db99e03f58a385c4aff0bd117c8b60d93ff482d67516", "family-pawns-app-dropper-v1")] - [InlineData("7b96c506a062bc7a8deb99fb8429c7b72db5a7fbe86f85b46584fc7f4e3d48f7", "family-pawns-app-dropper-v1")] - [InlineData("3a6a9292767af6c4df205c766cda0e811b8ac12a61a7e2aa5c88d08a7a8de144", "family-blockchain-java-stager-v1")] - public void Classify_ConfirmedHash_ReturnsExactKnownThreat(string hash, string expectedFamily) + [InlineData("4f1f3bc0028d9059939c9218dc6d975974b656f4c540ee83a51a7a39278c9c8b", "family-pawns-app-dropper-v1", "2026-09-malware-pawns-app-dropper")] + [InlineData("9dcc2c192b1b5e8bb9e9db99e03f58a385c4aff0bd117c8b60d93ff482d67516", "family-pawns-app-dropper-v1", "2026-09-malware-pawns-app-dropper")] + [InlineData("7b96c506a062bc7a8deb99fb8429c7b72db5a7fbe86f85b46584fc7f4e3d48f7", "family-pawns-app-dropper-v1", "2026-09-malware-pawns-app-dropper")] + [InlineData("3a6a9292767af6c4df205c766cda0e811b8ac12a61a7e2aa5c88d08a7a8de144", "family-blockchain-java-stager-v1", "2026-09-malware-blockchain-java-stager")] + [InlineData("fe8c78fe1bde7e5edf114f22d554d2786058e0dbf8d697123714da0484cded64", "family-remote-text-shell-exec-v1", "2026-09-malware-autobarncoopdoor")] + public void Classify_ConfirmedHash_ReturnsExactKnownThreat( + string hash, + string expectedFamily, + string expectedAdvisorySlug) { var classifier = new ThreatFamilyClassifier(); @@ -100,7 +126,8 @@ public void Classify_ConfirmedHash_ReturnsExactKnownThreat(string hash, string e matches.Should().ContainSingle(match => match.FamilyId == expectedFamily && match.MatchKind == ThreatMatchKind.ExactSampleHash && - match.ExactHashMatch); + match.ExactHashMatch && + match.AdvisorySlugs.Contains(expectedAdvisorySlug)); disposition.Classification.Should().Be(ThreatDispositionClassification.KnownThreat); } diff --git a/MLVScan.Core.Tests/Unit/Services/ThreatDispositionClassifierTests.cs b/MLVScan.Core.Tests/Unit/Services/ThreatDispositionClassifierTests.cs index a0a16cf..ac6b135 100644 --- a/MLVScan.Core.Tests/Unit/Services/ThreatDispositionClassifierTests.cs +++ b/MLVScan.Core.Tests/Unit/Services/ThreatDispositionClassifierTests.cs @@ -444,6 +444,25 @@ public void Classify_WithKnownInfrastructureDownloadAndHiddenStagedLaunch_Return result.BlockingRecommended.Should().BeTrue(); } + [Fact] + public void Classify_WithCoordinatedRemoteTextShellExecution_ReturnsSuspicious() + { + var classifier = new ThreatDispositionClassifier(); + var finding = new ScanFinding( + "Suspicious.RemoteLauncher", + "Detected remote text retrieval transformed into a runtime-computed hidden shell command in the same method.", + Severity.Critical) + { + RuleId = "CoordinatedPayloadDeliveryRule" + }; + + var result = classifier.Classify(new[] { finding }, threatFamilies: null); + + result.Classification.Should().Be(ThreatDispositionClassification.Suspicious); + result.RelatedFindings.Should().ContainSingle().Which.Should().BeSameAs(finding); + result.BlockingRecommended.Should().BeTrue(); + } + private static ScanFinding CreateEmbeddedUpdaterFinding() { var dataFlow = new DataFlowChain( diff --git a/Models/Rules/CoordinatedPayloadDeliveryRule.cs b/Models/Rules/CoordinatedPayloadDeliveryRule.cs index 34bc36b..1cfbcb5 100644 --- a/Models/Rules/CoordinatedPayloadDeliveryRule.cs +++ b/Models/Rules/CoordinatedPayloadDeliveryRule.cs @@ -94,6 +94,22 @@ public IEnumerable PostAnalysisRefine( "execution: hidden or elevated Java child process" ])); } + + if (TryFindRemoteTextHiddenShellExecution( + calls, + scopedProcessFindings, + out var remoteTextCall, + out var shellFinding)) + { + findings.Add(CreateFinding( + namespaceGroup.Key, + "Detected remote text retrieval transformed into a runtime-computed hidden shell command in the same method.", + [ + $"source: {remoteTextCall.Called.DeclaringType?.FullName}.{remoteTextCall.Called.Name} in {remoteTextCall.Method.FullName}", + "transform: remote text parsed or rewritten before execution", + $"execution: {shellFinding.Description}" + ])); + } } return findings; @@ -157,6 +173,87 @@ instruction.Operand is MethodReference called && calls.Any(call => IsProcessStart(call.Called)); } + private static bool TryFindRemoteTextHiddenShellExecution( + IReadOnlyList<(MethodDefinition Method, MethodReference Called)> calls, + IReadOnlyList processFindings, + out (MethodDefinition Method, MethodReference Called) remoteTextCall, + out ScanFinding shellFinding) + { + foreach (var finding in processFindings) + { + if (!IsHiddenDynamicShellFinding(finding)) + { + continue; + } + + foreach (var methodGroup in calls.GroupBy(static call => call.Method)) + { + if (!FindingBelongsToMethod(finding, methodGroup.Key) || + !methodGroup.Any(call => IsProcessStart(call.Called)) || + !methodGroup.Any(call => IsRemoteTextTransform(call.Called))) + { + continue; + } + + var source = methodGroup.FirstOrDefault(call => IsNetworkTextRead(call.Called)); + if (source.Called == null) + { + continue; + } + + remoteTextCall = source; + shellFinding = finding; + return true; + } + } + + remoteTextCall = default; + shellFinding = null!; + return false; + } + + private static bool IsHiddenDynamicShellFinding(ScanFinding finding) + { + bool targetsShell = Contains(finding.Description, "powershell.exe") || + Contains(finding.Description, "cmd.exe") || + Contains(finding.Description, "wscript.exe") || + Contains(finding.Description, "cscript.exe") || + Contains(finding.Description, "mshta.exe"); + bool hidesExecution = Contains(finding.Description, "CreateNoWindow=true") || + Contains(finding.Description, "WindowStyle=Hidden") || + Contains(finding.Description, "UseShellExecute=true"); + bool hasDynamicArguments = Contains(finding.Description, "Arguments: StrongStandaloneRuleIds = new(StringComparer.Ordinal) { "DataFlowAnalysis", + "CoordinatedPayloadDeliveryRule", "EmbeddedResourceScriptRule", "ObfuscatedReflectiveExecutionRule" }; diff --git a/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs b/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs index 70b93f9..29256ac 100644 --- a/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs +++ b/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs @@ -11,7 +11,7 @@ private static ThreatFamilyDefinition CreatePawnsAppDropperFamily() FamilyId = "family-pawns-app-dropper-v1", DisplayName = "Pawns.app credential-seeded autorun dropper", Summary = "Downloads and extracts a per-user archive payload, seeds application state, installs Windows autorun persistence, and launches the payload hidden.", - AdvisorySlugs = [], + AdvisorySlugs = ["2026-09-malware-pawns-app-dropper"], ExactSampleHashes = [ "4f1f3bc0028d9059939c9218dc6d975974b656f4c540ee83a51a7a39278c9c8b", @@ -39,7 +39,7 @@ private static ThreatFamilyDefinition CreateBlockchainJavaStagerFamily() FamilyId = "family-blockchain-java-stager-v1", DisplayName = "Blockchain-resolved Java payload stager", Summary = "Uses blockchain RPC service discovery and fixed-key decoding to retrieve or embed a Java archive, then executes it through a concealed Java child process.", - AdvisorySlugs = [], + AdvisorySlugs = ["2026-09-malware-blockchain-java-stager"], ExactSampleHashes = [ "3a6a9292767af6c4df205c766cda0e811b8ac12a61a7e2aa5c88d08a7a8de144" @@ -66,6 +66,32 @@ private static ThreatFamilyDefinition CreateBlockchainJavaStagerFamily() }; } + private static ThreatFamilyDefinition CreateRemoteTextShellExecutionFamily() + { + return new ThreatFamilyDefinition + { + FamilyId = "family-remote-text-shell-exec-v1", + DisplayName = "Remote text hidden shell executor", + Summary = "Retrieves remote text, parses or rewrites it into runtime-computed command arguments, and executes the result through a concealed system shell.", + AdvisorySlugs = ["2026-09-malware-autobarncoopdoor"], + ExactSampleHashes = + [ + "fe8c78fe1bde7e5edf114f22d554d2786058e0dbf8d697123714da0484cded64" + ], + Variants = + [ + new ThreatFamilyVariantDefinition + { + VariantId = "remote-text-hidden-shell-command", + DisplayName = "Remote text -> hidden shell command", + Summary = "Downloads text and transforms it into dynamic arguments for a hidden PowerShell, CMD, Windows Script Host, or MSHTA process in the same method.", + Confidence = 0.98, + Matcher = MatchRemoteTextHiddenShellCommand + } + ] + }; + } + private static ThreatFamilyVariantMatch? MatchArchiveUserProfileRunKeyHiddenLaunch( ThreatFamilyAnalysisContext context) { @@ -99,6 +125,36 @@ private static ThreatFamilyDefinition CreateBlockchainJavaStagerFamily() }; } + private static ThreatFamilyVariantMatch? MatchRemoteTextHiddenShellCommand( + ThreatFamilyAnalysisContext context) + { + var coordinator = context.FindFinding( + "CoordinatedPayloadDeliveryRule", + "remote text retrieval", + "hidden shell command"); + var process = context.Findings.FirstOrDefault(finding => + string.Equals(finding.RuleId, "ProcessStartRule", StringComparison.Ordinal) && + FindingContainsAny(finding, "powershell.exe", "cmd.exe", "wscript.exe", "cscript.exe", "mshta.exe") && + FindingContainsAny(finding, "CreateNoWindow=true", "WindowStyle=Hidden", "UseShellExecute=true") && + FindingContainsAny(finding, " diff --git a/Services/ThreatIntel/ThreatFamilyCatalog.cs b/Services/ThreatIntel/ThreatFamilyCatalog.cs index d44290e..63aa9da 100644 --- a/Services/ThreatIntel/ThreatFamilyCatalog.cs +++ b/Services/ThreatIntel/ThreatFamilyCatalog.cs @@ -15,6 +15,7 @@ internal static partial class ThreatFamilyCatalog [ CreatePawnsAppDropperFamily(), CreateBlockchainJavaStagerFamily(), + CreateRemoteTextShellExecutionFamily(), new ThreatFamilyDefinition { FamilyId = "family-resource-shell32-tempcmd-v2", From cd2d16963fe5d0353f36c1d0600b0257b0d423e2 Mon Sep 17 00:00:00 2001 From: ifBars Date: Sat, 19 Sep 2026 19:25:52 -0700 Subject: [PATCH 2/4] Detect encoded EVM Java stager variant --- .../Integration/NewSampleThreatFamilyTests.cs | 4 + .../CoordinatedPayloadDeliveryRuleTests.cs | 74 +++++++++++++++++++ .../Rules/CoordinatedPayloadDeliveryRule.cs | 41 ++++++---- Models/Rules/EncodedStringPipelineRule.cs | 7 +- .../ThreatFamilyCatalog.NewSamples.cs | 9 ++- Services/ThreatIntel/ThreatFamilyCatalog.cs | 6 +- 6 files changed, 123 insertions(+), 18 deletions(-) diff --git a/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs b/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs index 61a60f8..9afc5b8 100644 --- a/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs +++ b/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs @@ -12,7 +12,9 @@ public class NewSampleThreatFamilyTests [InlineData(@"BetterPatrols1\bin\Win64_Shipping_Client\BetterPatrols.dll.di", "family-pawns-app-dropper-v1", "archive-userprofile-runkey-hidden-launch")] [InlineData(@"BetterPatrols2\bin\Win64_Shipping_Client\BetterPatrols.dll.di", "family-pawns-app-dropper-v1", "archive-userprofile-runkey-hidden-launch")] [InlineData(@"BloodAndBanners\bin\Win64_Shipping_Client\BloodAndBanners.Core.dll.di", "family-pawns-app-dropper-v1", "archive-userprofile-runkey-hidden-launch")] + [InlineData(@"Meowtopia\Meowtopia.dll.di", "family-webdownload-stage-exec-v3", "webdownload-temp-hidden-launch-generic")] [InlineData(@"Polygamy\ValleyPolygamy.dll.di", "family-blockchain-java-stager-v1", "evm-resolved-jar-runas")] + [InlineData(@"SunriseFurrowCoach\SunriseFurrowCoach.dll.di", "family-blockchain-java-stager-v1", "evm-resolved-jar-runas")] public void Scan_NewMaliciousSample_WithoutHashEvidence_ShouldMatchBehaviorFamily( string relativePath, string expectedFamilyId, @@ -49,7 +51,9 @@ public void Scan_BloodAndBannersWrapper_WithoutCoreCompanion_ShouldRemainClean() [InlineData(@"BetterPatrols1\bin\Win64_Shipping_Client\BetterPatrols.dll.di", "family-pawns-app-dropper-v1")] [InlineData(@"BetterPatrols2\bin\Win64_Shipping_Client\BetterPatrols.dll.di", "family-pawns-app-dropper-v1")] [InlineData(@"BloodAndBanners\bin\Win64_Shipping_Client\BloodAndBanners.Core.dll.di", "family-pawns-app-dropper-v1")] + [InlineData(@"Meowtopia\Meowtopia.dll.di", "family-webdownload-stage-exec-v3")] [InlineData(@"Polygamy\ValleyPolygamy.dll.di", "family-blockchain-java-stager-v1")] + [InlineData(@"SunriseFurrowCoach\SunriseFurrowCoach.dll.di", "family-blockchain-java-stager-v1")] public void Scan_NewMaliciousSample_WithHashEvidence_ShouldBeExactKnownThreat( string relativePath, string expectedFamilyId) diff --git a/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs b/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs index 5e6d20e..ecc9ae7 100644 --- a/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs +++ b/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs @@ -88,6 +88,38 @@ public void PostAnalysisRefine_RemoteVersionCheckAndVisibleShell_ReturnsNoFindin findings.Should().BeEmpty(); } + [Fact] + public void PostAnalysisRefine_EncodedEvmJavaPayloadMarkers_ReturnsFinding() + { + using var assembly = CreateEncodedEvmJavaStager(); + var existing = new[] + { + ProcessFinding("Target: . Arguments: [WindowStyle=Hidden]"), + EncodedFinding("decoded indicator(s): eth_call, 0x5e280f11, -cp , com.renderassist.Main") + }; + + var findings = _rule.PostAnalysisRefine(assembly.MainModule, existing).ToList(); + + findings.Should().ContainSingle(finding => + finding.RuleId == "CoordinatedPayloadDeliveryRule" && + finding.Description.Contains("blockchain-resolved Java archive stager", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void PostAnalysisRefine_EncodedEvmClientWithoutJavaPayloadMarkers_ReturnsNoFinding() + { + using var assembly = CreateEncodedEvmJavaStager(); + var existing = new[] + { + ProcessFinding("Target: . Arguments: [WindowStyle=Hidden]"), + EncodedFinding("decoded indicator(s): eth_call, 0x5e280f11, https://rpc.example.test") + }; + + var findings = _rule.PostAnalysisRefine(assembly.MainModule, existing).ToList(); + + findings.Should().BeEmpty(); + } + private static AssemblyDefinition CreateArchiveInstaller(bool includeRegistryPersistence) { var assembly = AssemblyDefinition.CreateAssembly( @@ -140,6 +172,36 @@ private static AssemblyDefinition CreateRemoteTextShellLauncher() return assembly; } + private static AssemblyDefinition CreateEncodedEvmJavaStager() + { + var assembly = AssemblyDefinition.CreateAssembly( + new AssemblyNameDefinition("EncodedEvmJavaStager", new Version(1, 0)), + "EncodedEvmJavaStager", + ModuleKind.Dll); + var module = assembly.MainModule; + var type = new TypeDefinition("Test.Sample", "Stager", TypeAttributes.Public, module.TypeSystem.Object); + module.Types.Add(type); + + var decode = new MethodDefinition("Decode", MethodAttributes.Public | MethodAttributes.Static, module.TypeSystem.String); + decode.Body = new MethodBody(decode); + type.Methods.Add(decode); + var decodeIl = decode.Body.GetILProcessor(); + decodeIl.Emit(OpCodes.Ldc_I4, 167); + decodeIl.Emit(OpCodes.Xor); + decodeIl.Emit(OpCodes.Call, Method(module, "System.Text", "Encoding", "GetString")); + decodeIl.Emit(OpCodes.Ret); + + var run = new MethodDefinition("Run", MethodAttributes.Public | MethodAttributes.Static, module.TypeSystem.Void); + run.Body = new MethodBody(run); + type.Methods.Add(run); + var runIl = run.Body.GetILProcessor(); + runIl.Emit(OpCodes.Call, Method(module, "System.Net.Http", "HttpClient", "PostAsync")); + runIl.Emit(OpCodes.Call, Method(module, "System.IO", "File", "WriteAllBytes")); + runIl.Emit(OpCodes.Call, Method(module, "System.Diagnostics", "Process", "Start")); + runIl.Emit(OpCodes.Ret); + return assembly; + } + private static MethodReference Method(ModuleDefinition module, string ns, string type, string name) { return new MethodReference( @@ -155,4 +217,16 @@ private static ScanFinding ProcessFinding(string description, string location = RuleId = "ProcessStartRule" }; } + + private static ScanFinding EncodedFinding(string codeSnippet) + { + return new ScanFinding( + "Test.Sample.Stager.Decode", + "Detected fixed-key byte-array XOR string reconstruction.", + Severity.High, + codeSnippet) + { + RuleId = "EncodedStringPipelineRule" + }; + } } diff --git a/Models/Rules/CoordinatedPayloadDeliveryRule.cs b/Models/Rules/CoordinatedPayloadDeliveryRule.cs index 1cfbcb5..e3079f5 100644 --- a/Models/Rules/CoordinatedPayloadDeliveryRule.cs +++ b/Models/Rules/CoordinatedPayloadDeliveryRule.cs @@ -81,7 +81,7 @@ public IEnumerable PostAnalysisRefine( ])); } - if (IsBlockchainResolvedJavaStager(methods, calls, literals, scopedProcessFindings)) + if (IsBlockchainResolvedJavaStager(methods, calls, literals, priorFindings, scopedProcessFindings)) { findings.Add(CreateFinding( namespaceGroup.Key, @@ -140,6 +140,7 @@ private static bool IsBlockchainResolvedJavaStager( IReadOnlyList methods, IReadOnlyList<(MethodDefinition Method, MethodReference Called)> calls, IReadOnlyList literals, + IReadOnlyList priorFindings, IReadOnlyList processFindings) { bool hasFixedKeyXorDecoder = methods.Any(method => @@ -148,17 +149,31 @@ private static bool IsBlockchainResolvedJavaStager( instruction.Operand is MethodReference called && called.DeclaringType?.FullName == "System.Text.Encoding" && called.Name == "GetString")); - bool hasEvmLookup = literals.Any(static literal => - literal.Equals("eth_call", StringComparison.OrdinalIgnoreCase)) && - literals.Any(static literal => - literal.StartsWith("0x", StringComparison.OrdinalIgnoreCase) && - literal.Length >= 10); - bool hasJavaArchiveMarkers = literals.Any(static literal => - literal.Contains(".jar", StringComparison.OrdinalIgnoreCase)) && - literals.Any(static literal => - literal.Contains("-cp ", StringComparison.OrdinalIgnoreCase) || - literal.Contains("MemJarBootstrap", StringComparison.OrdinalIgnoreCase) || - literal.Contains("com.renderassist.Main", StringComparison.OrdinalIgnoreCase)); + var decodedEvidence = priorFindings + .Where(static finding => string.Equals( + finding.RuleId, + "EncodedStringPipelineRule", + StringComparison.Ordinal)) + .Select(static finding => $"{finding.Description}{Environment.NewLine}{finding.CodeSnippet}") + .ToList(); + bool hasEvmLookup = + (literals.Any(static literal => literal.Equals("eth_call", StringComparison.OrdinalIgnoreCase)) && + literals.Any(static literal => + literal.StartsWith("0x", StringComparison.OrdinalIgnoreCase) && literal.Length >= 10)) || + decodedEvidence.Any(static evidence => + evidence.Contains("eth_call", StringComparison.OrdinalIgnoreCase) && + evidence.Contains("0x", StringComparison.OrdinalIgnoreCase)); + bool hasJavaPayloadMarkers = + (literals.Any(static literal => literal.Contains(".jar", StringComparison.OrdinalIgnoreCase)) && + literals.Any(static literal => + literal.Contains("-cp ", StringComparison.OrdinalIgnoreCase) || + literal.Contains("MemJarBootstrap", StringComparison.OrdinalIgnoreCase) || + literal.Contains("com.renderassist.Main", StringComparison.OrdinalIgnoreCase))) || + decodedEvidence.Any(static evidence => + evidence.Contains("-cp ", StringComparison.OrdinalIgnoreCase) && + (evidence.Contains("com.renderassist.", StringComparison.OrdinalIgnoreCase) || + evidence.Contains(".jar", StringComparison.OrdinalIgnoreCase) || + evidence.Contains("javaw", StringComparison.OrdinalIgnoreCase))); bool hasConcealedJavaLaunch = processFindings.Any(finding => Contains(finding.Description, "WindowStyle=Hidden") || Contains(finding.Description, "Redirected I/O") || @@ -166,7 +181,7 @@ instruction.Operand is MethodReference called && return hasFixedKeyXorDecoder && hasEvmLookup && - hasJavaArchiveMarkers && + hasJavaPayloadMarkers && hasConcealedJavaLaunch && calls.Any(call => IsNetworkSend(call.Called)) && calls.Any(call => IsFileWrite(call.Called)) && diff --git a/Models/Rules/EncodedStringPipelineRule.cs b/Models/Rules/EncodedStringPipelineRule.cs index e844798..7148736 100644 --- a/Models/Rules/EncodedStringPipelineRule.cs +++ b/Models/Rules/EncodedStringPipelineRule.cs @@ -116,7 +116,7 @@ instruction.Operand is MethodReference called && } string decoderLocations = string.Join(", ", xorDecoders.Select(static item => item.Method.FullName).Take(3)); - string indicators = string.Join(", ", decodedIndicators.OrderBy(static value => value, StringComparer.Ordinal).Take(6)); + string indicators = string.Join(", ", decodedIndicators.OrderBy(static value => value, StringComparer.Ordinal).Take(16)); return [ new ScanFinding( @@ -537,8 +537,13 @@ private static bool IsSecurityRelevantDecodedString(string value) { return value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || + value.Equals("eth_call", StringComparison.OrdinalIgnoreCase) || + (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase) && value.Length >= 10) || value.Contains(".jar", StringComparison.OrdinalIgnoreCase) || + value.Contains(".cache", StringComparison.OrdinalIgnoreCase) || value.Contains("javaw", StringComparison.OrdinalIgnoreCase) || + value.Contains("com.renderassist.", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("-cp ", StringComparison.OrdinalIgnoreCase) || value.Contains("--cookie", StringComparison.OrdinalIgnoreCase) || value.Contains("/api/", StringComparison.OrdinalIgnoreCase); } diff --git a/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs b/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs index 29256ac..43f6b6d 100644 --- a/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs +++ b/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs @@ -39,10 +39,15 @@ private static ThreatFamilyDefinition CreateBlockchainJavaStagerFamily() FamilyId = "family-blockchain-java-stager-v1", DisplayName = "Blockchain-resolved Java payload stager", Summary = "Uses blockchain RPC service discovery and fixed-key decoding to retrieve or embed a Java archive, then executes it through a concealed Java child process.", - AdvisorySlugs = ["2026-09-malware-blockchain-java-stager"], + AdvisorySlugs = + [ + "2026-09-malware-blockchain-java-stager", + "2026-09-malware-sunrise-furrow-coach" + ], ExactSampleHashes = [ - "3a6a9292767af6c4df205c766cda0e811b8ac12a61a7e2aa5c88d08a7a8de144" + "3a6a9292767af6c4df205c766cda0e811b8ac12a61a7e2aa5c88d08a7a8de144", + "df609f3702aa60149ad93a342f52069f70f160ffb4cd2f96818f3022e650c4f8" ], Variants = [ diff --git a/Services/ThreatIntel/ThreatFamilyCatalog.cs b/Services/ThreatIntel/ThreatFamilyCatalog.cs index 63aa9da..fce8a50 100644 --- a/Services/ThreatIntel/ThreatFamilyCatalog.cs +++ b/Services/ThreatIntel/ThreatFamilyCatalog.cs @@ -91,12 +91,14 @@ internal static partial class ThreatFamilyCatalog "2026-03-malware-storagehub", "2026-03-malware-unlimitedgraffiti", "2026-03-malware-vortex-backuprtilizer", - "2026-04-malware-dynamicorders" + "2026-04-malware-dynamicorders", + "2026-09-malware-meowtopia" ], ExactSampleHashes = [ "6cb8afc1bf0e504d6b95bc05a36142f81f42b200c178e0ce6988bdf1a2c6ec0e", - "b5133362b4327a1bfecd45fe651b841372a1394fcbb6f8906a6724990b50e8a4" + "b5133362b4327a1bfecd45fe651b841372a1394fcbb6f8906a6724990b50e8a4", + "5b0e8fee2066284729b02a0252abeea3b5b00b0958c386f746535d7812f2f632" ], Variants = [ From 42b2737d8b68acd2e6a3fd22462085d03b690869 Mon Sep 17 00:00:00 2001 From: ifBars Date: Sat, 19 Sep 2026 19:36:53 -0700 Subject: [PATCH 3/4] Release Core 1.8.2 --- Directory.Build.props | 2 +- .../CoordinatedPayloadDeliveryRuleTests.cs | 27 +++++++- .../NewThreatFamilyClassifierTests.cs | 22 +++++++ MLVScan.WASM/npm/package-lock.json | 4 +- MLVScan.WASM/npm/package.json | 2 +- MLVScanVersions.cs | 2 +- .../Rules/CoordinatedPayloadDeliveryRule.cs | 65 +++++++++++++++++-- .../ThreatFamilyCatalog.NewSamples.cs | 5 +- 8 files changed, 115 insertions(+), 14 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0896f1e..f56095e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.8.1 + 1.8.2 Bars MLVScan diff --git a/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs b/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs index ecc9ae7..59ca137 100644 --- a/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs +++ b/MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs @@ -88,6 +88,25 @@ public void PostAnalysisRefine_RemoteVersionCheckAndVisibleShell_ReturnsNoFindin findings.Should().BeEmpty(); } + [Fact] + public void PostAnalysisRefine_FragmentedHiddenShellTarget_ReturnsFinding() + { + using var assembly = CreateRemoteTextShellLauncher(fragmentShellTarget: true); + var existing = new[] + { + ProcessFinding( + "Target: . Arguments: " + + "[Evasion: UseShellExecute set, CreateNoWindow=true]", + "Test.Sample.RemoteLauncher.Run:42") + }; + + var findings = _rule.PostAnalysisRefine(assembly.MainModule, existing).ToList(); + + findings.Should().ContainSingle(finding => + finding.RuleId == "CoordinatedPayloadDeliveryRule" && + finding.Description.Contains("remote text", StringComparison.OrdinalIgnoreCase)); + } + [Fact] public void PostAnalysisRefine_EncodedEvmJavaPayloadMarkers_ReturnsFinding() { @@ -149,7 +168,7 @@ private static AssemblyDefinition CreateArchiveInstaller(bool includeRegistryPer return assembly; } - private static AssemblyDefinition CreateRemoteTextShellLauncher() + private static AssemblyDefinition CreateRemoteTextShellLauncher(bool fragmentShellTarget = false) { var assembly = AssemblyDefinition.CreateAssembly( new AssemblyNameDefinition("RemoteTextShellLauncher", new Version(1, 0)), @@ -166,6 +185,12 @@ private static AssemblyDefinition CreateRemoteTextShellLauncher() il.Emit(OpCodes.Ldstr, "https://example.test/profile"); il.Emit(OpCodes.Call, Method(module, "System.Net.Http", "HttpClient", "GetStringAsync")); il.Emit(OpCodes.Call, Method(module, "System.Text.RegularExpressions", "Regex", "Match")); + if (fragmentShellTarget) + { + il.Emit(OpCodes.Ldstr, "powe"); + il.Emit(OpCodes.Ldstr, "rshell.exe"); + il.Emit(OpCodes.Call, Method(module, "System.Diagnostics", "ProcessStartInfo", "set_CreateNoWindow")); + } il.Emit(OpCodes.Call, Method(module, "System.Diagnostics", "ProcessStartInfo", "set_Arguments")); il.Emit(OpCodes.Call, Method(module, "System.Diagnostics", "Process", "Start")); il.Emit(OpCodes.Ret); diff --git a/MLVScan.Core.Tests/Unit/Services/NewThreatFamilyClassifierTests.cs b/MLVScan.Core.Tests/Unit/Services/NewThreatFamilyClassifierTests.cs index 971ab0d..ba43703 100644 --- a/MLVScan.Core.Tests/Unit/Services/NewThreatFamilyClassifierTests.cs +++ b/MLVScan.Core.Tests/Unit/Services/NewThreatFamilyClassifierTests.cs @@ -107,6 +107,28 @@ public void Classify_RemoteTextHiddenShellExecution_ReturnsBehaviorFamily() !match.ExactHashMatch); } + [Fact] + public void Classify_RemoteTextFragmentedDynamicShellTarget_ReturnsBehaviorFamily() + { + var findings = new[] + { + Finding( + "CoordinatedPayloadDeliveryRule", + "Detected remote text retrieval transformed into a runtime-computed hidden shell command in the same method."), + Finding( + "ProcessStartRule", + "Target: . Arguments: [Evasion: CreateNoWindow=true]") + }; + + var matches = new ThreatFamilyClassifier().Classify(findings, sha256Hash: null); + + matches.Should().ContainSingle(match => + match.FamilyId == "family-remote-text-shell-exec-v1" && + match.VariantId == "remote-text-hidden-shell-command" && + match.MatchKind == ThreatMatchKind.BehaviorVariant && + !match.ExactHashMatch); + } + [Theory] [InlineData("4f1f3bc0028d9059939c9218dc6d975974b656f4c540ee83a51a7a39278c9c8b", "family-pawns-app-dropper-v1", "2026-09-malware-pawns-app-dropper")] [InlineData("9dcc2c192b1b5e8bb9e9db99e03f58a385c4aff0bd117c8b60d93ff482d67516", "family-pawns-app-dropper-v1", "2026-09-malware-pawns-app-dropper")] diff --git a/MLVScan.WASM/npm/package-lock.json b/MLVScan.WASM/npm/package-lock.json index c437b42..908d47e 100644 --- a/MLVScan.WASM/npm/package-lock.json +++ b/MLVScan.WASM/npm/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mlvscan/wasm-core", - "version": "1.8.1", + "version": "1.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mlvscan/wasm-core", - "version": "1.8.1", + "version": "1.8.2", "license": "GPL-3.0-or-later", "devDependencies": { "@types/node": "^18.0.0", diff --git a/MLVScan.WASM/npm/package.json b/MLVScan.WASM/npm/package.json index d3b9a16..c0bdf5c 100644 --- a/MLVScan.WASM/npm/package.json +++ b/MLVScan.WASM/npm/package.json @@ -1,6 +1,6 @@ { "name": "@mlvscan/wasm-core", - "version": "1.8.1", + "version": "1.8.2", "description": "WebAssembly core for MLVScan - scanning managed .NET assemblies and mod DLLs in the browser", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/MLVScanVersions.cs b/MLVScanVersions.cs index 91f7c61..f22233b 100644 --- a/MLVScanVersions.cs +++ b/MLVScanVersions.cs @@ -7,7 +7,7 @@ namespace MLVScan; /// public static class MLVScanVersions { - internal const string DeclaredCoreVersion = "1.8.1"; + internal const string DeclaredCoreVersion = "1.8.2"; /// /// The JSON schema version (semver). diff --git a/Models/Rules/CoordinatedPayloadDeliveryRule.cs b/Models/Rules/CoordinatedPayloadDeliveryRule.cs index e3079f5..062e63d 100644 --- a/Models/Rules/CoordinatedPayloadDeliveryRule.cs +++ b/Models/Rules/CoordinatedPayloadDeliveryRule.cs @@ -196,16 +196,13 @@ private static bool TryFindRemoteTextHiddenShellExecution( { foreach (var finding in processFindings) { - if (!IsHiddenDynamicShellFinding(finding)) - { - continue; - } - foreach (var methodGroup in calls.GroupBy(static call => call.Method)) { if (!FindingBelongsToMethod(finding, methodGroup.Key) || !methodGroup.Any(call => IsProcessStart(call.Called)) || - !methodGroup.Any(call => IsRemoteTextTransform(call.Called))) + !methodGroup.Any(call => IsRemoteTextTransform(call.Called)) || + (!IsHiddenDynamicShellFinding(finding) && + !IsFragmentedHiddenShellLauncher(methodGroup.Key, methodGroup))) { continue; } @@ -222,11 +219,67 @@ private static bool TryFindRemoteTextHiddenShellExecution( } } + // Compiler-generated async state machines can make a retained ProcessStart location + // diverge from Cecil's method identity. Keep the correlation namespace-scoped, but + // still require the complete remote-text, transform, fragmented-shell, and hidden-launch chain. + var structuralLauncher = calls + .GroupBy(static call => call.Method) + .FirstOrDefault(group => + group.Any(call => IsProcessStart(call.Called)) && + IsFragmentedHiddenShellLauncher(group.Key, group)); + var namespaceSource = calls.FirstOrDefault(call => IsNetworkTextRead(call.Called)); + var namespaceFinding = processFindings.FirstOrDefault(finding => + Contains(finding.Description, "CreateNoWindow=true") || + Contains(finding.Description, "WindowStyle=Hidden")); + if (structuralLauncher != null && + namespaceSource.Called != null && + namespaceFinding != null && + calls.Any(call => IsRemoteTextTransform(call.Called))) + { + remoteTextCall = namespaceSource; + shellFinding = namespaceFinding; + return true; + } + remoteTextCall = default; shellFinding = null!; return false; } + private static bool IsFragmentedHiddenShellLauncher( + MethodDefinition method, + IEnumerable<(MethodDefinition Method, MethodReference Called)> calls) + { + var methodCalls = calls.Select(static call => call.Called).ToList(); + bool configuresHiddenDynamicLaunch = + methodCalls.Any(static call => + call.DeclaringType?.FullName == "System.Diagnostics.ProcessStartInfo" && + call.Name == "set_CreateNoWindow") && + methodCalls.Any(static call => + call.DeclaringType?.FullName == "System.Diagnostics.ProcessStartInfo" && + call.Name == "set_Arguments"); + if (!configuresHiddenDynamicLaunch) + { + return false; + } + + var literals = method.Body.Instructions + .Where(static instruction => instruction.OpCode == OpCodes.Ldstr) + .Select(static instruction => instruction.Operand as string) + .Where(static literal => !string.IsNullOrEmpty(literal)) + .Cast() + .ToList(); + bool ContainsShellName(string candidate) => + candidate.Contains("powershell.exe", StringComparison.OrdinalIgnoreCase) || + candidate.Contains("cmd.exe", StringComparison.OrdinalIgnoreCase) || + candidate.Contains("wscript.exe", StringComparison.OrdinalIgnoreCase) || + candidate.Contains("cscript.exe", StringComparison.OrdinalIgnoreCase) || + candidate.Contains("mshta.exe", StringComparison.OrdinalIgnoreCase); + + return literals.Any(ContainsShellName) || + literals.Any(first => literals.Any(second => ContainsShellName(first + second))); + } + private static bool IsHiddenDynamicShellFinding(ScanFinding finding) { bool targetsShell = Contains(finding.Description, "powershell.exe") || diff --git a/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs b/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs index 43f6b6d..2a02c5c 100644 --- a/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs +++ b/Services/ThreatIntel/ThreatFamilyCatalog.NewSamples.cs @@ -139,9 +139,10 @@ private static ThreatFamilyDefinition CreateRemoteTextShellExecutionFamily() "hidden shell command"); var process = context.Findings.FirstOrDefault(finding => string.Equals(finding.RuleId, "ProcessStartRule", StringComparison.Ordinal) && - FindingContainsAny(finding, "powershell.exe", "cmd.exe", "wscript.exe", "cscript.exe", "mshta.exe") && FindingContainsAny(finding, "CreateNoWindow=true", "WindowStyle=Hidden", "UseShellExecute=true") && - FindingContainsAny(finding, " Date: Sat, 19 Sep 2026 19:41:37 -0700 Subject: [PATCH 4/4] Skip unavailable quarantine corpus in CI --- MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs b/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs index 9afc5b8..d1ee7cf 100644 --- a/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs +++ b/MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs @@ -87,6 +87,7 @@ private static string GetSamplePath(string relativePath) current = Directory.GetParent(current)?.FullName; } - throw new SkipException($"Static sample not found in QUARANTINE: {relativePath}"); + Skip.If(true, $"Static sample not found in QUARANTINE: {relativePath}"); + return string.Empty; } }