Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>1.8.1</Version>
<Version>1.8.2</Version>
<Company>Bars</Company>
<Product>MLVScan</Product>
</PropertyGroup>
Expand Down
10 changes: 8 additions & 2 deletions MLVScan.Core.Tests/Integration/NewSampleThreatFamilyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ 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")]
[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,
Expand Down Expand Up @@ -44,10 +47,13 @@ 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")]
[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)
Expand All @@ -72,7 +78,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;
Expand All @@ -81,7 +87,7 @@ private static string GetSamplePath(string relativePath)
current = Directory.GetParent(current)?.FullName;
}

Skip.If(true, $"Static sample not found in TO_ANALYZE: {relativePath}");
Skip.If(true, $"Static sample not found in QUARANTINE: {relativePath}");
return string.Empty;
}
}
18 changes: 17 additions & 1 deletion MLVScan.Core.Tests/Integration/ThreatFamilyQuarantineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ public class ThreatFamilyQuarantineTests
@"malware-suspicious-with-findings\RexonV5Menu.dll.di"
};

private static readonly HashSet<string> 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;

Expand Down Expand Up @@ -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;
}
Expand Down
162 changes: 160 additions & 2 deletions MLVScan.Core.Tests/Unit/Rules/CoordinatedPayloadDeliveryRuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,93 @@ 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: <dynamic via Replace> " +
"[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();
}

[Fact]
public void PostAnalysisRefine_FragmentedHiddenShellTarget_ReturnsFinding()
{
using var assembly = CreateRemoteTextShellLauncher(fragmentShellTarget: true);
var existing = new[]
{
ProcessFinding(
"Target: <dynamic via Dup>. Arguments: <unknown/no-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()
{
using var assembly = CreateEncodedEvmJavaStager();
var existing = new[]
{
ProcessFinding("Target: <dynamic>. Arguments: <dynamic> [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: <dynamic>. Arguments: <dynamic> [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(
Expand Down Expand Up @@ -81,6 +168,65 @@ private static AssemblyDefinition CreateArchiveInstaller(bool includeRegistryPer
return assembly;
}

private static AssemblyDefinition CreateRemoteTextShellLauncher(bool fragmentShellTarget = false)
{
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"));
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);
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(
Expand All @@ -89,11 +235,23 @@ 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"
};
}

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"
};
}
}
61 changes: 55 additions & 6 deletions MLVScan.Core.Tests/Unit/Services/NewThreatFamilyClassifierTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,60 @@ 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: <dynamic via Replace> [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);
}

[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: <dynamic via Dup>. Arguments: <unknown/no-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();

Expand All @@ -100,7 +148,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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions MLVScan.WASM/npm/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion MLVScan.WASM/npm/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading
Loading