Hunting network beaconing using standard deviation from Sysmon or Defender for Endpoint events

Occasional Contributor

I've found that these KQL queries (one for Sysmon events, the other for MDE DeviceNetworkEvents) have been very helpful to identifying beaconing patterns in network connections. In the environments I'm hunting in, it catches a few websites that use Javascript to refresh every 60 seconds or fetch more data, but if you have many of those you can tune out web browsers from the results.

What's more important is that these were highly effective at identifying Cobalt Strike beacons (even with jitter settings), IcedID, and a VBA RAT that communicated with Google Scripts to relay its C2 beacons.

Sysmon Query (requires a Sysmon parsing custom function)

 

let starttime = 48h; 
let endtime = 1m; 
let  TimeDeltaThreshold = 2; // don't count anything under 2 seconds between connections
let TotalEventsThreshold = 15; // only show beaconing with least this many connections
let DurationThreshold = 900; // only show beaconing that lasted at least this # seconds
let StandardDeviationThreshold = 100; // Set to filter out false positives: lower number is tighter filtering/fewer results
Sysmon
| where EventID==3
| where TimeGenerated between (ago(starttime)..ago(endtime))
| project TimeGenerated, Computer, process_path, src_ip, src_port, dst_ip, dst_port
| sort by src_ip asc, dst_ip asc, TimeGenerated asc // sort to put all connections between two hosts next to each other in time order
| serialize 
| extend nextTimeGenerated = next(TimeGenerated, 1), nextDeviceId = next(Computer, 1), nextDstIP = next(dst_ip, 1) 
| extend TimeDeltaInSeconds = datetime_diff("second", nextTimeGenerated, TimeGenerated) // compute time difference between subsequent connections
| where Computer == nextDeviceId and nextDstIP == dst_ip // only compute time difference if next host pair is the same as current
| where TimeDeltaInSeconds > TimeDeltaThreshold // filter out connections that happen too close together
| project TimeGenerated, TimeDeltaInSeconds, Computer, process_path, src_ip, src_port, dst_ip, dst_port
| summarize avg(TimeDeltaInSeconds), count(), min(TimeGenerated), max(TimeGenerated),  // compute statistics including standard deviation
Duration=datetime_diff("second", max(TimeGenerated), min(TimeGenerated)), 
StandardDeviation=stdev(TimeDeltaInSeconds), TimeDeltaList=make_list(TimeDeltaInSeconds) by Computer, src_ip, dst_ip, process_path
| where count_ > TotalEventsThreshold 
// comment out the next line if you don't want to filter out short-term beacons that aren't still active
//| where count_ > datetime_diff("second", ago(endtime), min_TimeGenerated) / (avg_TimeDeltaInSeconds*2)
| where StandardDeviation < StandardDeviationThreshold
| where Duration >= DurationThreshold
| order by StandardDeviation asc

 

 

Defender for Endpoint (MDE) query

 

let starttime = 2d;
let endtime = 1m;
let TimeDeltaThreshold = 2;
let TotalEventsThreshold = 15;
let DurationThreshold = 1200;
let StandardDeviationThreshold = 100;
DeviceNetworkEvents
| where RemoteIPType !in ("Reserved", "Private", "LinkLocal", "Loopback")
| where isnotempty(RemoteIP) and RemoteIP !in ("0.0.0.0") and RemoteIP !startswith_cs "10."
| where ActionType in ("ConnectionSuccess", "ConnectionRequest", "ConnectionFailed")
| project TimeGenerated, DeviceId, DeviceName, InitiatingProcessFileName, LocalIP, LocalPort, RemoteIP, RemotePort
| sort by LocalIP asc, RemoteIP asc, TimeGenerated asc
| serialize
| extend nextTimeGenerated = next(TimeGenerated, 1), nextDeviceId = next(DeviceId, 1), nextRemoteIP = next(RemoteIP, 1)
| extend TimeDeltaInSeconds = datetime_diff("second", nextTimeGenerated, TimeGenerated)
| where DeviceId == nextDeviceId and RemoteIP == nextRemoteIP
| where TimeDeltaInSeconds > TimeDeltaThreshold
| project TimeGenerated, TimeDeltaInSeconds, DeviceName, InitiatingProcessFileName, LocalIP, LocalPort, RemoteIP, RemotePort
| summarize avg(TimeDeltaInSeconds), count(), min(TimeGenerated), max(TimeGenerated), Duration=datetime_diff("second", max(TimeGenerated), min(TimeGenerated)), StandardDeviation=stdev(TimeDeltaInSeconds), TimeDeltaList=make_list(TimeDeltaInSeconds) by DeviceName, LocalIP, RemoteIP, InitiatingProcessFileName
| where count_ > TotalEventsThreshold 
// comment out the next line if you don't want to filter out short-term beacons that aren't still active
//| where count_ > datetime_diff("second", ago(endtime), min_TimeGenerated) / (avg_TimeDeltaInSeconds*2)
| where StandardDeviation < StandardDeviationThreshold
| where Duration >= DurationThreshold
| order by StandardDeviation asc
| extend HostCustomEntity = DeviceName
| extend IPCustomEntity = RemoteIP
| extend TimestampCustomEntity = max_TimeGenerated

 

 

0 Replies