Detection rules › Kusto

RITA Beacon Analyzer for Windows Firewall Events

Group by
Application, Computer, DestinationIP, DestinationPort, Protocol
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Below queries analyze Windows Firewall logs and applies RITA beacon analyzer algorithm for C2 beaconing detection.

MITRE ATT&CK coverage

TacticTechniques
Command & ControlNo specific technique

Telemetry coverage

Rule body

// 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;

let AllBeacons and let PotentialBeacons are inlined into the numbered stages below.

Stage 1: source

let AllBeacons

Stage 2: source

let PotentialBeacons

Stage 3: source

SecurityEvent

Stage 4: where

where TimeGenerated between (ago(86400s) .. ago(60s))

Stage 5: where

where EventID == 5156

Stage 6: where

where EventData contains "<Data Name=\"Protocol\">6</Data>"

Stage 7: where

where EventData contains "<Data Name=\"Direction\">%%14593</Data>"

Stage 8: where

where not (EventData contains "<Data Name=\"DestAddress\">127.0.0.1</Data>")

Stage 9: where

where not (EventData contains "<Data Name=\"DestAddress\">169.254.")

Stage 10: parse

parse

Stage 11: where

where not ((ipv4_is_in_range(DestinationIP, "10.0.0.0/8") or ipv4_is_in_range(DestinationIP, "172.16.0.0/12") or ipv4_is_in_range(DestinationIP, "192.168.0.0/16") or ipv4_is_in_range(DestinationIP, "169.254.0.0/16") or ipv4_is_in_range(DestinationIP, "127.0.0.0/8")))

Stage 12: parse

parse

Stage 13: summarize

summarize end, start by Computer, Application, Protocol, DestinationIP, DestinationPort

Stage 14: where

where list_TimeGenerated >= 24

Stage 15: extend

extend duration_minutes, duration_seconds

Stage 16: where

where duration_minutes >= 180

Stage 17: project

project Application, Computer, ConnRate, ConnectionCount, DestinationIP, DestinationPort, Protocol, TimeGenerated, duration_minutes, duration_seconds

Stage 18: kusto:mv-apply

kusto:mv-apply Computer =~ nextComputer and nextApplication =~ Application and nextDestinationIP =~ DestinationIP and nextDestinationPort =~ DestinationPort and nextProtocol =~ Protocol and tsScore >= ScoreThreshold

Stage 19: summarize

summarize by Protocol, DestinationIP, DestinationPort

Stage 20: where

where dcount_Computer <= 5

Stage 21: join

join kind=inner (AllBeacons) on Protocol, DestinationIP, DestinationPort

Stage 22: project-away

project-away *1

Stage 23: project-away

project-away TimeDeltaList

Stage 24: sort

sort by ConnectionCount, tsScore

Stage 25: project-reorder

project-reorder

Stage 26: source

WindowsFirewall

Stage 27: where

where TimeGenerated between (ago(86400s) .. ago(60s))

Stage 28: where

where CommunicationDirection =~ "SEND"

Stage 29: where

where FirewallAction =~ "ALLOW"

Stage 30: where

where Protocol =~ "TCP"

Stage 31: where

where DestinationIP !~ "127.0.0.1"

Stage 32: where

where not ((ipv4_is_in_range(DestinationIP, "10.0.0.0/8") or ipv4_is_in_range(DestinationIP, "172.16.0.0/12") or ipv4_is_in_range(DestinationIP, "192.168.0.0/16") or ipv4_is_in_range(DestinationIP, "169.254.0.0/16") or ipv4_is_in_range(DestinationIP, "127.0.0.0/8")))

Stage 33: summarize

summarize end, start by Computer, DestinationIP, Protocol, DestinationPort

Stage 34: where

where list_TimeGenerated >= 24

Stage 35: extend

extend duration_minutes, duration_seconds

Stage 36: where

where duration_minutes >= 180

Stage 37: project

project Computer, ConnRate, ConnectionCount, DestinationIP, DestinationPort, Protocol, TimeGenerated, duration_minutes, duration_seconds

Stage 38: kusto:mv-apply

kusto:mv-apply Computer =~ nextComputer and nextDestinationIP =~ DestinationIP and nextDestinationPort =~ DestinationPort and nextProtocol =~ Protocol and tsScore >= ScoreThreshold

Stage 39: summarize

summarize by Protocol, DestinationIP, DestinationPort

Stage 40: where

where dcount_Computer <= 5

Stage 41: join

join kind=inner (AllBeacons) on Protocol, DestinationIP, DestinationPort

Stage 42: project-away

project-away *1

Stage 43: project-away

project-away TimeDeltaList

Stage 44: sort

sort by ConnectionCount, tsScore

Stage 45: project-reorder

project-reorder

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
EventDatamatch<Data Name="DestAddress">127.0.0.1</Data>excludes:EventData
EventDatamatch<Data Name="DestAddress">169.254.excludes:EventData
DestinationIPcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:DestinationIP

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
DestinationIPsummarize
DestinationPortsummarize
Protocolsummarize