232 lines
10 KiB
C#
232 lines
10 KiB
C#
using System.Diagnostics;
|
|
using System.Diagnostics.Eventing.Reader;
|
|
using System.Runtime.Versioning;
|
|
using System.Text.Json;
|
|
|
|
namespace OCSentinelCli;
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
internal static class RansomwareBetaDetector
|
|
{
|
|
public static RansomwareBetaSummary Scan(ScannerConfiguration configuration, List<string> errors)
|
|
{
|
|
if (!configuration.RansomwareBetaEnabled)
|
|
{
|
|
return new RansomwareBetaSummary();
|
|
}
|
|
|
|
int lookbackMinutes = Math.Clamp(configuration.RansomwareLookbackMinutes, 1, 60);
|
|
DateTimeOffset since = DateTimeOffset.UtcNow.AddMinutes(-lookbackMinutes);
|
|
var signals = new List<RansomwareSignal>();
|
|
|
|
ScanSecurityProcesses(signals, errors, since, configuration);
|
|
ScanPowerShellScriptBlocks(signals, errors, since, configuration);
|
|
ScanSysmonProcesses(signals, errors, since, configuration);
|
|
RansomwareFileChurnSummary fileChurn = RansomwareFileChurnDetector.Scan(configuration, errors);
|
|
RansomwareSignal? fileChurnSignal = RansomwareFileChurnDetector.CreateSignal(fileChurn, configuration);
|
|
if (fileChurnSignal is not null)
|
|
{
|
|
signals.Add(fileChurnSignal);
|
|
}
|
|
|
|
List<RansomwareSignal> distinctSignals = signals
|
|
.OrderBy(signal => signal.Timestamp)
|
|
.GroupBy(signal => $"{signal.Category}\u001f{signal.Process}", StringComparer.OrdinalIgnoreCase)
|
|
.Select(group => group.First())
|
|
.Take(20)
|
|
.ToList();
|
|
int strongSignals = distinctSignals.Count(signal => signal.Confidence == "high");
|
|
|
|
string state = distinctSignals.Count >= Math.Max(2, configuration.RansomwareCriticalSignalCount)
|
|
? "critical"
|
|
: strongSignals > 0 || distinctSignals.Count >= Math.Max(2, configuration.RansomwareWarningSignalCount)
|
|
? "warning"
|
|
: distinctSignals.Count > 0 ? "hint" : "ok";
|
|
string reason = state switch
|
|
{
|
|
"critical" => $"Ransomware beta detected {distinctSignals.Count} independent high-risk signals within {lookbackMinutes} minutes.",
|
|
"warning" => $"Ransomware beta detected {strongSignals} high-confidence and {distinctSignals.Count - strongSignals} low-confidence signals within {lookbackMinutes} minutes.",
|
|
"hint" => $"Ransomware beta observed an isolated low-confidence signal within {lookbackMinutes} minutes.",
|
|
_ => $"Ransomware beta found no suspicious process activity in the last {lookbackMinutes} minutes."
|
|
};
|
|
|
|
List<RansomwareSmbSession> smbSessions = state is "warning" or "critical" && configuration.RansomwareCaptureSmbSessions
|
|
? CaptureSmbSessions(errors)
|
|
: [];
|
|
return new RansomwareBetaSummary
|
|
{
|
|
Enabled = true,
|
|
AlertingEnabled = configuration.RansomwareBetaAlertingEnabled,
|
|
State = state,
|
|
Reason = reason,
|
|
LookbackMinutes = lookbackMinutes,
|
|
Signals = distinctSignals,
|
|
SmbSessions = smbSessions,
|
|
FileChurn = fileChurn
|
|
};
|
|
}
|
|
|
|
private static void ScanSecurityProcesses(List<RansomwareSignal> signals, List<string> errors, DateTimeOffset since, ScannerConfiguration configuration)
|
|
{
|
|
TryScan("Security", 4688, since, errors, record => AddSignal(signals, record, ReadProperty(record, 5), ReadProperty(record, 8), "Security", configuration));
|
|
}
|
|
|
|
private static void ScanPowerShellScriptBlocks(List<RansomwareSignal> signals, List<string> errors, DateTimeOffset since, ScannerConfiguration configuration)
|
|
{
|
|
TryScan("Microsoft-Windows-PowerShell/Operational", 4104, since, errors, record => AddSignal(signals, record, "powershell", FormatDescription(record), "PowerShell", configuration));
|
|
}
|
|
|
|
private static void ScanSysmonProcesses(List<RansomwareSignal> signals, List<string> errors, DateTimeOffset since, ScannerConfiguration configuration)
|
|
{
|
|
TryScan("Microsoft-Windows-Sysmon/Operational", 1, since, errors, record => AddSignal(signals, record, "sysmon-process", FormatDescription(record), "Sysmon", configuration));
|
|
}
|
|
|
|
private static void AddSignal(List<RansomwareSignal> signals, EventRecord record, string process, string commandLine, string source, ScannerConfiguration configuration)
|
|
{
|
|
if (!record.TimeCreated.HasValue || string.IsNullOrWhiteSpace(commandLine))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string processName = Path.GetFileName(process.Trim());
|
|
if (configuration.RansomwareExcludedProcesses.Any(item => string.Equals(item, processName, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RansomwareSignal? signal = Classify(record.TimeCreated.Value, record.Id, processName, commandLine, source);
|
|
if (signal is not null)
|
|
{
|
|
signals.Add(signal);
|
|
}
|
|
}
|
|
|
|
private static RansomwareSignal? Classify(DateTime timestamp, int eventId, string process, string commandLine, string source)
|
|
{
|
|
string value = commandLine.ToLowerInvariant();
|
|
string evidence = commandLine.Length > 512 ? commandLine[..512] : commandLine;
|
|
if (ContainsAll(value, "vssadmin", "delete", "shadow") || ContainsAll(value, "wmic", "shadowcopy", "delete") || ContainsAll(value, "win32_shadowcopy", "delete"))
|
|
{
|
|
return CreateSignal(timestamp, eventId, "shadow-copy-deletion", "high", process, source, evidence);
|
|
}
|
|
|
|
if (ContainsAll(value, "wbadmin", "delete") || ContainsAll(value, "catalog", "delete"))
|
|
{
|
|
return CreateSignal(timestamp, eventId, "backup-catalog-deletion", "high", process, source, evidence);
|
|
}
|
|
|
|
if (ContainsAll(value, "bcdedit", "recoveryenabled", "no") || ContainsAll(value, "bcdedit", "bootstatuspolicy", "ignoreallfailures"))
|
|
{
|
|
return CreateSignal(timestamp, eventId, "recovery-disable", "high", process, source, evidence);
|
|
}
|
|
|
|
if (ContainsAll(value, "wevtutil", " cl "))
|
|
{
|
|
return CreateSignal(timestamp, eventId, "event-log-clearing", "high", process, source, evidence);
|
|
}
|
|
|
|
return value.Contains("win32_shadowcopy", StringComparison.Ordinal)
|
|
? CreateSignal(timestamp, eventId, "shadow-copy-access", "low", process, source, evidence)
|
|
: null;
|
|
}
|
|
|
|
private static RansomwareSignal CreateSignal(DateTime timestamp, int eventId, string category, string confidence, string process, string source, string evidence)
|
|
{
|
|
return new RansomwareSignal
|
|
{
|
|
Timestamp = new DateTimeOffset(timestamp).ToLocalTime(),
|
|
Category = category,
|
|
Confidence = confidence,
|
|
Process = string.IsNullOrWhiteSpace(process) ? "[unknown]" : process,
|
|
Source = source,
|
|
EventId = eventId,
|
|
Evidence = evidence
|
|
};
|
|
}
|
|
|
|
private static bool ContainsAll(string value, params string[] needles) => needles.All(needle => value.Contains(needle, StringComparison.Ordinal));
|
|
|
|
private static void TryScan(string logName, int eventId, DateTimeOffset since, List<string> errors, Action<EventRecord> processRecord)
|
|
{
|
|
try
|
|
{
|
|
long milliseconds = Math.Max(1, (long)(DateTimeOffset.UtcNow - since).TotalMilliseconds);
|
|
string query = $"*[System[(EventID={eventId}) and TimeCreated[timediff(@SystemTime) <= {milliseconds}]]]";
|
|
using var reader = new EventLogReader(new EventLogQuery(logName, PathType.LogName, query));
|
|
for (EventRecord? record = reader.ReadEvent(); record is not null; record = reader.ReadEvent())
|
|
{
|
|
using (record)
|
|
{
|
|
processRecord(record);
|
|
}
|
|
}
|
|
}
|
|
catch (EventLogNotFoundException)
|
|
{
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
errors.Add($"Ransomware beta query failed for {logName}: {exception.Message}");
|
|
}
|
|
}
|
|
|
|
private static string ReadProperty(EventRecord record, int index) => index >= 0 && index < record.Properties.Count ? record.Properties[index].Value?.ToString()?.Trim() ?? string.Empty : string.Empty;
|
|
|
|
private static string FormatDescription(EventRecord record)
|
|
{
|
|
try
|
|
{
|
|
return record.FormatDescription() ?? string.Empty;
|
|
}
|
|
catch (EventLogException)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
private static List<RansomwareSmbSession> CaptureSmbSessions(List<string> errors)
|
|
{
|
|
try
|
|
{
|
|
using var process = Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = "powershell.exe",
|
|
Arguments = "-NoProfile -NonInteractive -Command \"Get-SmbSession | Select-Object ClientComputerName,ClientUserName,SessionId,NumOpens | ConvertTo-Json -Compress\"",
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
});
|
|
if (process is null || !process.WaitForExit(5000) || process.ExitCode != 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
string json = process.StandardOutput.ReadToEnd();
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
JsonElement root = JsonSerializer.Deserialize<JsonElement>(json, JsonOptions.Default);
|
|
IEnumerable<JsonElement> rows = root.ValueKind == JsonValueKind.Array ? root.EnumerateArray().ToArray() : [root];
|
|
return rows.Take(100).Select(row => new RansomwareSmbSession
|
|
{
|
|
ClientComputerName = GetJsonString(row, "ClientComputerName"),
|
|
ClientUserName = GetJsonString(row, "ClientUserName"),
|
|
SessionId = GetJsonLong(row, "SessionId"),
|
|
OpenFileCount = GetJsonLong(row, "NumOpens")
|
|
}).ToList();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
errors.Add($"Ransomware beta SMB snapshot failed: {exception.Message}");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private static string GetJsonString(JsonElement value, string name) => value.TryGetProperty(name, out JsonElement property) ? property.ToString() : string.Empty;
|
|
|
|
private static long GetJsonLong(JsonElement value, string name) => value.TryGetProperty(name, out JsonElement property) && property.TryGetInt64(out long result) ? result : 0;
|
|
}
|