A key aspect of threat hunting is monitoring where identities and devices connect by analyzing RemoteIP, Domains, and URLs. Matching these indicators with external threat intelligence feeds helps assess their reputation and trigger alerts or actions.
While having strong detection and monitoring enhances defense, an often-overlooked factor is analyzing the responses from remote destinations — what they return could reveal additional threats and here it is where can talk about DNS Response.
In addition, how many times do you see private IP into RemoteIP column? It can indicate that different devices are querying an internal DNS servers or internal DNS resolvers that forward requests to external servers. Therefore, how we can identify the traffic triggered by these devices? DNS Queries.
DNS Queries
I’ll start by listing the DNS queries that our devices send to the DNS systems:
DeviceNetworkEvents
| extend query = (tostring(parse_json(AdditionalFields).query))
| distinct query
In the obtained list, you can see the different domains requested by the devices. As a first validation, you can check if any of these domains appear in an TI Feed source like URLHaus. To filter out known domains, you can use : | where query !in (“docs.google.com”,”drive.google.com”)
let URLHausOnlineRAW = externaldata (UHFeed:string) [“https://urlhaus.abuse.ch/downloads/csv_online/”] with(format=“txt”)
| where UHFeed !startswith “#”
| extend UHRAW=replace_string(UHFeed, ‘"’, ‘’)
| project splitted=split(UHRAW, ‘,’)
| mv-expand id=splitted[0], dateadded=splitted[1], UHUrl=splitted[2], UHurl_status=splitted[3], UHlast_onlin=splitted[4], UHthreat=splitted[5], UHtags=splitted[6], UHLink=splitted[7], UHReporter=splitted[8]
| extend UHUrl = tostring(UHUrl)
| extend UHUrlDomain = tostring(parse_url(UHUrl).Host)
| project-away splitted;
DeviceNetworkEvents
| extend direction = (tostring(parse_json(AdditionalFields).direction))
| extend query = todynamic(tostring(parse_json(AdditionalFields).query))
| mv-expand query
| extend tostring(query)
| where query !in (“docs.google.com”,“drive.google.com”)
| join kind=inner ( URLHausOnlineRAW) on $left.query == $right.UHUrlDomain
| distinct DeviceId, direction, RemoteIP, query
DNS Answers data
When we make DNS queries, we expect responses. Analyzing DNS response data is essential for detecting cyber threats across various aspects, such as:
- Attackers often exploit DNS for evasion and command-and-control (C2) communication.
- Unusual TTL values may indicate fast-flux botnets or rapidly changing malicious domains, while high RTT values can reveal hidden or long-distance C2 servers.
- DNS tunneling, used for data exfiltration, can be identified through abnormal TXT records and RTT fluctuations.
- Inconsistent AA (Authoritative Answer) values may suggest DNS spoofing or MITM attacks, and unexpected RD (Recursion Desired) flags can indicate malicious DNS resolver abuse.
- Additionally, fast-flux techniques leverage frequent IP changes and low TTLs to evade detection, while DNS cache poisoning manipulates records to redirect users to malicious sites.
Monitoring recursive queries helps uncover traffic rerouted through rogue DNS servers, and threat actors frequently abuse DNS to bypass firewalls for phishing and malware delivery. By proactively analyzing DNS response attributes, security teams can detect hidden adversary techniques, enhance threat hunting, and strengthen their cybersecurity posture.
In the following section, I will go through different fields related to DNS Response and some insights which can help to define your own detections based on your criteria and requirements.
Answers
DNS answers can be in different formats, like IPv4, IPv6, or URLs, so I used the case operator to distinguish between them:
| extend Type =
case(
answers matches regex @“^(\d{1,3}.){3}\d{1,3}$”, “IPv4”,
answers matches regex @“^([a-fA-F0-9:]+)$”, “IPv6”,
answers contains “.”, “URL”,
“Unknown”
)
Based on these 3 fields, you can use TI Feed sources to detect malicious ones. For example, for Malicious IP, I am using a source that continuously updates a list of malicious IP addresses. The following one is maintained by Stamparm, a well-known security researcher.
You can identify a line with : | where BL > 1 which means in how many Blacklist an IP is tagged
let IPList = externaldata (IP:string) [“https://raw.githubusercontent.com/stamparm/ipsum/refs/heads/master/ipsum.txt”] with(format=“txt”)
| where IP !startswith “#”
| extend IP_ = split(IP, " “)
| extend IP = tostring(IP_[0])
| extend BL = toint(IP_[1]);
DeviceNetworkEvents
| extend answers = todynamic(tostring(parse_json(AdditionalFields).answers))
| extend answersext = todynamic(tostring(parse_json(AdditionalFields).answers))
| extend query = (tostring(parse_json(AdditionalFields).query))
| mv-expand answers
| extend Type =
case(
answers matches regex @”^(\d{1,3}.){3}\d{1,3}$“, “IPv4”,
answers matches regex @”^([a-fA-F0-9:]+)$“, “IPv6”,
answers contains “.”, “URL”,
“Unknown”
)
| where Type has “IPv4”
| extend tostring(answers)
| join kind=inner (IPList) on $left.answers == $right.IP
| extend Geo_info_answer = tostring(geo_info_from_ip_address(answers).country)
| extend Geo_info_RemoteIP = tostring(geo_info_from_ip_address(RemoteIP).country)
| where BL > 1
| summarize dcount(answers),make_set(answers),make_set(query),make_set(Geo_info_answer),make_set(ActionType) by DeviceName, RemoteIP, Geo_info_RemoteIP
| order by dcount_answers
On the other hand, I am doing the same analysis but with URLs based on URLHaus TI Feed:
let URLHausOnlineRAW = externaldata (UHFeed:string) [“https://urlhaus.abuse.ch/downloads/csv_online/”] with(format=“txt”)
| where UHFeed !startswith “#”
| extend UHRAW=replace_string(UHFeed, '”', ‘’)
| project splitted=split(UHRAW, ‘,’)
| mv-expand id=splitted[0], dateadded=splitted[1], UHUrl=splitted[2], UHurl_status=splitted[3], UHlast_onlin=splitted[4], UHthreat=splitted[5], UHtags=splitted[6], UHLink=splitted[7], UHReporter=splitted[8]
| extend UHUrl = tostring(UHUrl)
| extend UHUrlDomain = tostring(parse_url(UHUrl).Host)
| project-away splitted;
DeviceNetworkEvents
| extend answers = todynamic(tostring(parse_json(AdditionalFields).answers))
| extend answersext = todynamic(tostring(parse_json(AdditionalFields).answers))
| mv-expand answers
//| extend geo_Remote_answers = todynamic(tostring(geo_info_from_ip_address(answers).country))
| extend Type =
case(
answers matches regex @“^(\d{1,3}.){3}\d{1,3}$”, “IPv4”, // Matches IPv4 format
answers matches regex @“^([a-fA-F0-9:]+)$”, “IPv6”, // Matches IPv6 format
answers contains “.”, “URL”, // Checks if it contains a dot (common in URLs)
“Unknown” // Default case
)
| where Type has “URL”
| extend tostring(answers)
| join kind=inner (URLHausOnlineRAW) on $left.answers == $right.UHUrl
| extend geo_Remote_ip = tostring(geo_info_from_ip_address(RemoteIP).country)
| project Timestamp,DeviceName,LocalIP,RemoteIP,geo_Remote_ip,MaliciousAnswers = UHUrl,answersext,UHUrlDomain, ActionType
Time to live (TTL)
TTL values in DNS responses provide valuable threat-hunting insights, including:
- Fast-flux botnets (rotating IPs with low TTLs).
- Malware C2 detection (extremely low TTLs). ( | where TTLs < 10 )
- DNS tunneling (high TTLs or changing TTLs). ( | where TTLs > 86400 )
- Fake domains mimicking real services (TTL anomalies).
- Evasive infrastructure constantly changing TTL values.
DeviceNetworkEvents
| extend TTLs = todynamic(tostring(parse_json(AdditionalFields).TTLs))
| mv-expand TTLs
| extend answers = todynamic(tostring(parse_json(AdditionalFields).answers))
| extend answersext = todynamic(tostring(parse_json(AdditionalFields).answers))
| extend query = (tostring(parse_json(AdditionalFields).query))
| mv-expand answers
| extend Type =
case(
answers matches regex @“^(\d{1,3}.){3}\d{1,3}$”, “IPv4”,
answers matches regex @“^([a-fA-F0-9:]+)$”, “IPv6”,
answers contains “.”, “URL”,
“Unknown”
)
| where Type has “IPv4”
| extend tostring(answers)
| extend Geo_info_answer = tostring(geo_info_from_ip_address(answers).country)
| extend Geo_info_RemoteIP = tostring(geo_info_from_ip_address(RemoteIP).country)
| where TTLs > 86400
| project DeviceName, RemoteIP,answers,Geo_info_RemoteIP, Geo_info_answer, TTLs
Example: DNS tunneling (high TTLs or changing TTLs). ( | where TTLs > 86400)
Round Trip Time (RTT)
RTT analysis is a powerful field for threat hunting, revealing:
- C2 servers hosted in unusual locations (High RTT). (| where rtt > 100)
- Malware hiding inside local networks (Low RTT). (| where rtt < 5 )
- DNS tunneling activity based on RTT anomalies.
- Tor/VPN evasion techniques (RTT fluctuation detection).
- Compromised infrastructure using offshore hosting (Known malicious IPs with high RTT).
DeviceNetworkEvents
| extend TTLs = todynamic(tostring(parse_json(AdditionalFields).TTLs))
| mv-expand TTLs
| extend answers = todynamic(tostring(parse_json(AdditionalFields).answers))
| extend answersext = todynamic(tostring(parse_json(AdditionalFields).answers))
| extend rtt = todynamic(tostring(parse_json(AdditionalFields).rtt))
| extend query = (tostring(parse_json(AdditionalFields).query))
| mv-expand answers
| extend Type =
case(
answers matches regex @“^(\d{1,3}.){3}\d{1,3}$”, “IPv4”,
answers matches regex @“^([a-fA-F0-9:]+)$”, “IPv6”,
answers contains “.”, “URL”,
“Unknown”
)
| where Type has “IPv4”
| extend tostring(answers)
| extend Geo_info_answer = tostring(geo_info_from_ip_address(answers).country)
| extend Geo_info_RemoteIP = tostring(geo_info_from_ip_address(RemoteIP).country)
| where rtt > 100
| project DeviceName, RemoteIP,answers,Geo_info_RemoteIP, Geo_info_answer,rtt, TTLs
Example: C2 servers hosted in unusual locations (High RTT)
Additional DNS response fields (TC, RD, RA, AA)
The following values extracted from the AdditionalFields column, provide valuable insights for threat hunting by revealing how a DNS query was processed and whether the response behavior indicates potential abuse or manipulation.
- TC (Truncated) — Detecting Large or Suspicious DNS Responses ( | where TC == 1)
A TC value of 1 means the DNS response was too large for UDP and had to be truncated.
- RD (Recursion Desired) — Identifying Unusual DNS Query Behavior
If RD = 1, the client requested recursion (expecting the resolver to fetch the answer). If RD = 0, the query was likely sent to an authoritative server instead of a recursive resolver.
- RA (Recursion Available) — Spotting Rogue DNS Servers
If RA = 1, the DNS server supports recursion. If RA = 0, it does not.
- AA (Authoritative Answer) — Detecting DNS Spoofing & Poisoning
If AA = 1, the response comes directly from an authoritative DNS server. If AA = 0, the response was relayed from a resolver and could be altered.
DeviceNetworkEvents
| extend direction = tostring(parse_json(AdditionalFields).direction)
| extend rtt = tostring(parse_json(AdditionalFields).rtt)
| extend answers = tostring(parse_json(AdditionalFields).answers)
| extend TTLs = tostring(parse_json(AdditionalFields).TTLs)
| extend TC = tostring(parse_json(AdditionalFields).TC)
| extend RD = tostring(parse_json(AdditionalFields).RD)
| extend RA = tostring(parse_json(AdditionalFields).RA)
| extend AA = tostring(parse_json(AdditionalFields).AA)
| extend geo_Remote_ip = tostring(geo_info_from_ip_address(RemoteIP).country)
| extend geo_Local_ip = tostring(geo_info_from_ip_address(LocalIP).country)
| where isnotempty(answers)
| where isnotempty( geo_Local_ip)
| where TC == 1
| project RemoteIP, geo_Remote_ip,geo_Local_ip,TTLs, answers, rtt, direction, AA,TC,RA,RD
Example: TC == 1 means the DNS response was too large for UDP and had to be truncated.
Conclusion
This article aims to highlight various network fields that can help identify threats, either directly through the shared cases or by enriching existing threat hunting queries.
Recognising known and trusted domains is crucial for reducing false positives, enabling more effective detections for unknown or suspicious DNS queries and responses within our infrastructure.
DNS Response analysis with KQL: queries, answers, TTL, RTT & more was originally published in Detect FYI on Medium, where people are continuing the conversation by highlighting and responding to this story.
Introduction to Malware Binary Triage (IMBT) Course
Looking to level up your skills? Get 10% off using coupon code: MWNEWS10 for any flavor.
Enroll Now and Save 10%: Coupon Code MWNEWS10
Note: Affiliate link – your enrollment helps support this platform at no extra cost to you.
Article Link: DNS Response analysis with KQL: queries, answers, TTL, RTT & more | by Sergio Albea | Apr, 2025 | Detect FYI