Fast Flux DNS
Adversaries may use Fast Flux DNS to hide a command and control channel behind an array of rapidly changing IP addresses linked to a single domain resolution. This technique uses a fully qualified domain name with multiple IP addresses assigned to it, swapped with high frequency using a combination of round-robin IP addressing and short Time-To-Live (TTL) DNS records. The simplest 'single-flux' method involves registering and de-registering addresses as part of the DNS A record list, with an average lifespan of five minutes per IP. The 'double-flux' method additionally rotates the DNS Name Server (NS) records for the zone, providing additional resilience by allowing additional hosts to act as proxies to the true C2 host. Real-world users of this technique include Amadey malware, TA505, gh0st RAT operators, njRAT, menuPass (APT10), and Gamaredon Group.
// Fast Flux DNS Detection — Azure DNS Server Logs (DnsEvents)
// Detects domains resolving to an unusually high number of unique public IPs within a 1-hour window
let lookbackWindow = 1h;
let minUniqueIPThreshold = 5;
let minQueryCount = 3;
let KnownCDNDomains = dynamic([
".akamaiedge.net", ".akamaitechnologies.com", ".cloudfront.net",
".fastly.net", ".cloudflare.net", ".edgesuite.net",
".azureedge.net", ".trafficmanager.net", ".amazonaws.com",
".googleusercontent.com", ".gstatic.com"
]);
DnsEvents
| where TimeGenerated > ago(lookbackWindow)
| where SubType == "LookupQuery"
| where QueryType == "A" or QueryType == "AAAA"
| where ResultCode == 0
| where isnotempty(IPAddresses)
// Exclude known CDN domains by suffix
| where not(Name has_any (KnownCDNDomains))
// Exclude internal/trusted domains
| where not(Name matches regex @"\.(local|internal|corp|lan|home|arpa)$")
// Exclude Microsoft cloud services
| where not(Name endswith ".microsoft.com")
and not(Name endswith ".windows.com")
and not(Name endswith ".office.com")
and not(Name endswith ".azure.com")
and not(Name endswith ".live.com")
| mv-expand IPAddress = split(IPAddresses, ";")
| extend IPAddress = trim(" ", tostring(IPAddress))
| where isnotempty(IPAddress)
// Filter RFC1918 private and loopback addresses
| where not(IPAddress matches regex @"^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.|::1|0\.0\.0\.0)")
| summarize
UniqueIPCount = dcount(IPAddress),
IPAddressList = make_set(IPAddress, 100),
QueryCount = count(),
UniqueClients = dcount(ClientIP),
ClientList = make_set(ClientIP, 20),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by Name
| where UniqueIPCount >= minUniqueIPThreshold
| where QueryCount >= minQueryCount
| extend ObservationMinutes = max_of(datetime_diff('minute', LastSeen, FirstSeen), 1)
| extend IPChurnRate = round(todouble(UniqueIPCount) / todouble(ObservationMinutes), 4)
| extend DomainParts = split(Name, ".")
| extend TLD = tostring(DomainParts[array_length(DomainParts) - 1])
| extend SLD = tostring(DomainParts[array_length(DomainParts) - 2])
| extend SuspiciousTLD = TLD in~ ("tk", "pw", "cc", "ws", "top", "xyz", "ru", "cn", "info", "online", "click", "link", "gq", "ml", "cf")
| extend ShortSLD = strlen(SLD) <= 8
| extend SuspicionScore = toint(SuspiciousTLD) + toint(ShortSLD) + iif(UniqueIPCount >= 10, 1, 0) + iif(UniqueClients >= 5, 1, 0)
| project
DetectionTime = LastSeen,
DomainName = Name,
UniqueIPCount,
IPChurnRate,
QueryCount,
UniqueClients,
ClientList,
IPAddressList,
ObservationMinutes,
TLD,
SuspiciousTLD,
ShortSLD,
SuspicionScore,
FirstSeen
| sort by UniqueIPCount desc, IPChurnRate desc Data Sources
Required Tables
False Positives
- Content Delivery Networks (Cloudflare, Akamai, AWS CloudFront, Fastly, Azure CDN) legitimately return many different IPs via anycast geo-routing — filter by known CDN domain suffixes or IP ranges
- Global load-balanced SaaS services (Microsoft 365, Google Workspace, Zoom, Salesforce) return different IPs per geographic region — allowlist by domain suffix
- Active/passive DNS failover configurations tested during disaster recovery drills — coordinate with network operations for change window exclusions
- Internal DNS round-robin for on-premises load-balanced applications with multiple backend nodes — document and allowlist by internal domain name
- Security research sinkholes — multiple decommissioned botnet C2 domains may resolve to sinkhole IPs operated by different vendors, appearing as high IP diversity
References (12)
- https://attack.mitre.org/techniques/T1568/001/
- https://resources.infosecinstitute.com/fast-flux-networks-working-detection-part-1/#gref
- https://resources.infosecinstitute.com/fast-flux-networks-working-detection-part-2/#gref
- https://www.welivesecurity.com/2017/01/12/fast-flux-networks-work/
- https://www.cisa.gov/sites/default/files/2024-05/fast-flux-advisory-aa24-131a.pdf
- https://unit42.paloaltonetworks.com/fast-flux-networks-revisited/
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1568.001/T1568.001.md
- https://learn.microsoft.com/en-us/azure/sentinel/dns-events
- https://learn.microsoft.com/en-us/sysinternals/downloads/sysmon
- https://docs.microsoft.com/en-us/defender-endpoint/advanced-hunting-devicenetworkevents-table
- https://www.secureworks.com/blog/fast-flux-dns-detection
- https://www.caida.org/catalog/papers/2008_fast_flux_hosting/
Unlock Pro Content
Get the full detection package for T1568.001 including response playbook, investigation guide, and atomic red team tests.