Detection rules › Kusto
RITA Beacon Analyzer for Windows Firewall Events
Below queries analyze Windows Firewall logs and applies RITA beacon analyzer algorithm for C2 beaconing detection.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control | No specific technique |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Security-Auditing | Event ID 5156 | The Windows Filtering Platform has permitted a connection. |
Rule body kusto
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// https://posts.bluraven.io/implementing-rita-using-kql-8ccb0ee8eeae
//
// Read the blog to understand how this query works and how to analyze the results.
// This query may not be able to detect beacons that have large sleep values like 6h-1d.
//
// Query parameters:
let starttime = 1d;
let endtime = 1m;
let TotalEventsThresholdMin = 24; // A beacon should have at least 24 connections (1h sleep)
let DurationThreshold_minutes = 180; // only show beacons that had 180 minutes of duration
let ScoreThreshold = 0.85; // show beacons that have a score greater than 0.85 (max score is 1)
let MaxJitterInseconds = 30.0; // covers beacons that have max 30 seconds jitter(consider increasing this for better coverage)
// Define how many devices can have the same beacon.
let CompromisedDeviceCountMax = 5; // number of devices that can be compromised at the same time and have the same beacon. Increasing the value generates more results.
let AllBeacons = materialize (
SecurityEvent
| where TimeGenerated between (ago(starttime)..ago(endtime))
| where EventID == 5156
| where EventData has_cs '<Data Name="Protocol">6</Data>' // TCP connections only
| where EventData has_cs '<Data Name="Direction">%%14593</Data>' // Outbound direction
| where EventData !has_cs '<Data Name="DestAddress">127.0.0.1</Data>'
| where EventData !has_cs '<Data Name="DestAddress">169.254.'
| parse EventData with * 'DestAddress">' DestinationIP '<' *
| where not(ipv4_is_private(DestinationIP)) // analyze only outbound connections to the internet
| parse EventData with * '"ProcessID">' ProcessId '<' * 'Application">' Application '<' * 'Direction">' Direction '<' * 'SourceAddress">' SourceIP '<' * 'SourcePort">' SourcePort '<'
* 'DestPort">' DestinationPort '<' * 'Protocol">' Protocol '<' * 'LayerName">' LayerName '<' * 'RemoteUserID">' RemoteUserID '<' * 'RemoteMachineID">' RemoteMachineID '<' *
// Prepare data set for analysis.
| summarize hint.strategy=shuffle start=min(TimeGenerated), end=max(TimeGenerated), make_list(TimeGenerated)
by Computer, Application, Protocol, DestinationIP, DestinationPort
| where array_length(list_TimeGenerated) >= TotalEventsThresholdMin
| extend
duration_minutes=datetime_diff("minute", end, start),
duration_seconds=datetime_diff("second", end, start)
| where duration_minutes >= DurationThreshold_minutes
// Keep data set as small as possibble, remove unnecessary columns.
| project
duration_minutes,
duration_seconds,
ConnRate = toreal(array_length(list_TimeGenerated)) / toreal(duration_minutes),
Computer,
Application,
DestinationIP,
DestinationPort,
Protocol,
TimeGenerated = array_sort_asc(list_TimeGenerated),
ConnectionCount = array_length(list_TimeGenerated)
// Start analysis:
| mv-apply TimeGenerated to typeof(datetime) on
(
extend
nextTimeGenerated = next(TimeGenerated, 1),
nextComputer = next(Computer, 1),
nextApplication = next(Application, 1),
nextProtocol = next(Protocol, 1),
nextDestinationIP = next(DestinationIP, 1),
nextDestinationPort = next(DestinationPort, 1)
| extend TimeDeltaInSeconds = datetime_diff('second', nextTimeGenerated, TimeGenerated)
| where Computer == nextComputer
and nextProtocol == Protocol
and nextDestinationIP == DestinationIP
and nextDestinationPort == DestinationPort
and nextApplication == Application
| project
TimeGenerated,
TimeDeltaInSeconds,
Computer,
Application,
Protocol,
DestinationIP,
DestinationPort,
duration_minutes,
duration_seconds
// Calculate percentiles.
| summarize hint.strategy=shuffle count(), min(TimeGenerated), max(TimeGenerated),
percentiles(TimeDeltaInSeconds, 10, 25, 50, 75, 90),
TimeDeltaList=make_list(TimeDeltaInSeconds)
by
Computer,
Application,
Protocol,
DestinationIP,
DestinationPort,
duration_minutes,
duration_seconds
| extend
tsLow = (percentile_TimeDeltaInSeconds_10),
tsMid = (percentile_TimeDeltaInSeconds_50),
tsHigh = (percentile_TimeDeltaInSeconds_90)
// calculate Bowley variables
| extend tsBowleyNum = tsLow + tsHigh - 2 * tsMid, tsBowleyDen = tsHigh - tsLow
// calculate Bowley's skewness
| extend tsSkew = iif(tsBowleyDen != 0 and tsMid != tsLow and tsMid != tsHigh, toreal(tsBowleyNum) / toreal(tsBowleyDen), 0.0)
// calculate skewness scores
| extend tsSkewScore = 1.0 - toreal(abs(tsSkew))
// start of MADM calculation
| mv-expand TimeDeltaList
| extend temp_tsdelta = abs(TimeDeltaList - tsMid)
// calculate MADM of timedelta and sentbytes
| summarize hint.strategy=shuffle tsMadm = percentiles(temp_tsdelta, 50), TimeDeltaList = make_list(TimeDeltaList)
by
tsLow,
tsMid,
tsHigh,
Computer,
Application,
Protocol,
DestinationIP,
DestinationPort,
count_,
min_TimeGenerated,
max_TimeGenerated,
tsBowleyNum,
tsBowleyDen,
tsSkewScore,
duration_minutes,
duration_seconds
// calculate MADM, smallness(sentbytes) and connection count score
| extend tsMadmScore = iif((1.0 - toreal(tsMadm) / MaxJitterInseconds) < 0, 0.0, 1.0 - toreal(tsMadm) / MaxJitterInseconds)
| extend tsConnCountScore = iif(toreal(array_length(TimeDeltaList)) / (toreal(duration_seconds) / 90.0) > 1.0, 1.0, toreal(array_length(TimeDeltaList)) / (toreal(duration_seconds) / 90.0))
// calculate sum of the scores(timedelta and sentbytes)
| extend tsSum = tsSkewScore + tsMadmScore + tsConnCountScore
// calculate timedelta, sentbytes and the overall score.
| extend tsScore = ceiling((tsSum / 3.0) * 1000) / 1000
// filter results based on the score threshold
| where tsScore >= ScoreThreshold
// calculate jitter. not used for filtering for now.
| extend tsJitter=iif(tsMid > 0, toreal(tsMadm) / toreal(tsMid) * 100, 0.0)
)
)
;
let PotentialBeacons = materialize
(
AllBeacons
| summarize hint.strategy=shuffle dcount(Computer) by Protocol, DestinationIP, DestinationPort
| where dcount_Computer <= CompromisedDeviceCountMax
| join kind=inner AllBeacons on Protocol, DestinationIP, DestinationPort
| project-away *1
)
;
PotentialBeacons
| project-away TimeDeltaList
| sort by tsScore desc, ConnectionCount desc
| project-reorder tsScore, ConnectionCount, dcount_Computer, Protocol, Computer, Application
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// https://posts.bluraven.io/implementing-rita-using-kql-8ccb0ee8eeae
//
// Read the blog to understand how this query works and how to analyze the results.
// This query may not be able to detect beacons that have large sleep values like 6h-1d.
//
// Query parameters:
let starttime = 1d;
let endtime = 1m;
let TotalEventsThresholdMin = 24; // A beacon should have at least 24 connections (1h sleep)
let DurationThreshold_minutes = 180; // only show beacons that had 180 minutes of duration
let ScoreThreshold = 0.85; // show beacons that have a score greater than 0.85 (max score is 1)
let MaxJitterInseconds = 30.0; // covers beacons that have max 30 seconds jitter(consider increasing this for better coverage)
// Define how many devices can have the same beacon.
let CompromisedDeviceCountMax = 5; // number of devices that can be compromised at the same time and have the same beacon. Increasing the value generates more results.
let AllBeacons = materialize (
WindowsFirewall
| where TimeGenerated between (ago(starttime)..ago(endtime))
| where CommunicationDirection == "SEND"
| where FirewallAction == "ALLOW"
| where Protocol == "TCP"
| where DestinationIP <> '127.0.0.1'
| where not(ipv4_is_private(DestinationIP))
// Prepare data set for analysis.
| summarize hint.strategy=shuffle start=min(TimeGenerated), end=max(TimeGenerated), make_list(TimeGenerated)
by Computer, DestinationIP, Protocol, DestinationPort
| where array_length(list_TimeGenerated) >= TotalEventsThresholdMin
| extend
duration_minutes=datetime_diff("minute", end, start),
duration_seconds=datetime_diff("second", end, start)
| where duration_minutes >= DurationThreshold_minutes
// Keep data set as small as possibble, remove unnecessary columns.
| project
duration_minutes,
duration_seconds,
ConnRate = toreal(array_length(list_TimeGenerated)) / toreal(duration_minutes),
Computer,
DestinationIP,
DestinationPort,
Protocol,
TimeGenerated = array_sort_asc(list_TimeGenerated),
ConnectionCount = array_length(list_TimeGenerated)
// Start analysis:
| mv-apply TimeGenerated to typeof(datetime) on
(
extend
nextTimeGenerated = next(TimeGenerated, 1),
nextComputer = next(Computer, 1),
nextProtocol = next(Protocol, 1),
nextDestinationIP = next(DestinationIP, 1),
nextDestinationPort = next(DestinationPort, 1)
| extend TimeDeltaInSeconds = datetime_diff('second', nextTimeGenerated, TimeGenerated)
| where Computer == nextComputer
and nextProtocol == Protocol
and nextDestinationIP == DestinationIP
and nextDestinationPort == DestinationPort
| project
TimeGenerated,
TimeDeltaInSeconds,
Computer,
Protocol,
DestinationIP,
DestinationPort,
duration_minutes,
duration_seconds
// Calculate percentiles.
| summarize hint.strategy=shuffle count(), min(TimeGenerated), max(TimeGenerated),
percentiles(TimeDeltaInSeconds, 10, 25, 50, 75, 90),
TimeDeltaList=make_list(TimeDeltaInSeconds)
by
Computer,
Protocol,
DestinationIP,
DestinationPort,
duration_minutes,
duration_seconds
| extend
tsLow = (percentile_TimeDeltaInSeconds_10),
tsMid = (percentile_TimeDeltaInSeconds_50),
tsHigh = (percentile_TimeDeltaInSeconds_90)
// calculate Bowley variables
| extend tsBowleyNum = tsLow + tsHigh - 2 * tsMid, tsBowleyDen = tsHigh - tsLow
// calculate Bowley's skewness
| extend tsSkew = iif(tsBowleyDen != 0 and tsMid != tsLow and tsMid != tsHigh, toreal(tsBowleyNum) / toreal(tsBowleyDen), 0.0)
// calculate skewness scores
| extend tsSkewScore = 1.0 - toreal(abs(tsSkew))
// start of MADM calculation
| mv-expand TimeDeltaList
| extend temp_tsdelta = abs(TimeDeltaList - tsMid)
// calculate MADM of timedelta and sentbytes
| summarize hint.strategy=shuffle tsMadm = percentiles(temp_tsdelta, 50), TimeDeltaList = make_list(TimeDeltaList)
by
tsLow,
tsMid,
tsHigh,
Computer,
Protocol,
DestinationIP,
DestinationPort,
count_,
min_TimeGenerated,
max_TimeGenerated,
tsBowleyNum,
tsBowleyDen,
tsSkewScore,
duration_minutes,
duration_seconds
// calculate MADM, smallness(sentbytes) and connection count score
| extend tsMadmScore = iif((1.0 - toreal(tsMadm) / MaxJitterInseconds) < 0, 0.0, 1.0 - toreal(tsMadm) / MaxJitterInseconds)
| extend tsConnCountScore = iif(toreal(array_length(TimeDeltaList)) / (toreal(duration_seconds) / 90.0) > 1.0, 1.0, toreal(array_length(TimeDeltaList)) / (toreal(duration_seconds) / 90.0))
// calculate sum of the scores(timedelta and sentbytes)
| extend tsSum = tsSkewScore + tsMadmScore + tsConnCountScore
// calculate timedelta, sentbytes and the overall score.
| extend tsScore = ceiling((tsSum / 3.0) * 1000) / 1000
// filter results based on the score threshold
| where tsScore >= ScoreThreshold
// calculate jitter. not used for filtering for now.
| extend tsJitter=iif(tsMid > 0, toreal(tsMadm) / toreal(tsMid) * 100, 0.0)
)
)
;
let PotentialBeacons = materialize
(
AllBeacons
| summarize hint.strategy=shuffle dcount(Computer) by Protocol, DestinationIP, DestinationPort
| where dcount_Computer <= CompromisedDeviceCountMax
| join kind=inner AllBeacons on Protocol, DestinationIP, DestinationPort
| project-away *1
)
;
PotentialBeacons
| project-away TimeDeltaList
| sort by tsScore desc, ConnectionCount desc
| project-reorder tsScore, ConnectionCount, dcount_Computer, Protocol, Computer
Stages and Predicates
Parameters
let starttime = 1d;
let endtime = 1m;
let TotalEventsThresholdMin = 24;
let DurationThreshold_minutes = 180;
let ScoreThreshold = 0.85;
let MaxJitterInseconds = 30.0;
let CompromisedDeviceCountMax = 5;
Stage 1: source
let AllBeacons
Stage 2: source
let PotentialBeacons
Stage 3: source
AllBeacons
Stage 4: summarize
summarize by Protocol, DestinationIP, DestinationPort
Stage 5: where
where dcount_Computer <= 5
Stage 6: join
join kind=inner (...)
Stage 7: project-away
project-away *1
Stage 8: project-away
project-away TimeDeltaList
Stage 9: sort
sort by ConnectionCount, tsScore
Stage 10: project-reorder
project-reorder
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
DestinationIP | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 |
EventData | match | <Data Name="DestAddress">127.0.0.1</Data> |
EventData | match | <Data Name="DestAddress">169.254. |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
Computer | eq |
|
EventData | match |
|
EventID | eq |
|
dcount_Computer | le |
|
duration_minutes | ge |
|
list_TimeGenerated | ge |
|
nextApplication | eq |
|
nextDestinationIP | eq |
|
nextDestinationPort | eq |
|
nextProtocol | eq |
|
tsScore | ge |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
DestinationIP | summarize |
DestinationPort | summarize |
Protocol | summarize |