173 lines
7.7 KiB
C#
173 lines
7.7 KiB
C#
using System.Diagnostics.Eventing.Reader;
|
|
using System.Runtime.Versioning;
|
|
|
|
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);
|
|
|
|
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."
|
|
};
|
|
|
|
return new RansomwareBetaSummary
|
|
{
|
|
Enabled = true,
|
|
State = state,
|
|
Reason = reason,
|
|
LookbackMinutes = lookbackMinutes,
|
|
Signals = distinctSignals
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|