← Back to SOC feed Coverage →

Potential rootkit network activity missing from MDE

kql MEDIUM Azure-Sentinel
T1562.001T1562.004T1011
ASimNetworkSessionLogsDeviceNetworkEventsimNetworkSession
backdoorhuntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-05-27T11:00:00Z · Confidence: medium

Hunt Hypothesis

Adversaries may be using a rootkit to hide outbound network connections, evading detection by Microsoft Defender for Endpoint. SOC teams should proactively hunt for this discrepancy in Azure Sentinel to identify potential persistent threats and exfiltration activities.

KQL Query

// DETECTION STRATEGY: Rootkit & BYOVD Ring-0 Network Bypass Detection via Firewall-to-EDR Telemetry Delta.
// THE MECHANIC: Advanced adversaries operating in Ring-0 via BYOVD can unlink WFP callouts, blinding MDE to outbound network telemetry while the host otherwise appears healthy.
// THE RESILIENCE: By comparing "Network Truth" (out-of-band firewall logs) against "Host Truth" (EDR telemetry), we trap the adversary. Endpoint tampering cannot hide the physical packet leaving the perimeter.

// Disclaimer: This query can be computationally expensive if run too frequently.

// NOTE ON ANALYTICS RULE CONVERSION, SCOPING & COMPUTE:
// If converting this Hunting Query into a scheduled Analytics Rule, you MUST do the following to preserve compute and prevent false positives:
// 1. Scope 'activeMdeNodes' strictly to "Crown Jewel" critical servers (e.g., Domain Controllers, PKI) to limit the left-anti join compute footprint.
// 2. Introduce an ingestion offset using "ago()" (e.g., | where TimeGenerated between (ago(24h) .. ago(1h))) to account for MDE telemetry batching delays and prevent "Ghost Deltas".

// Time framing: Hunting blade passes the selected time automatically via StartTimeISO and EndTimeISO
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');

// Thresholds: Avoid micro-session noise, DHCP churn, or transient TCP handshakes.
let byteThreshold = 50000; 
let connThreshold = 5;

// Tier 1: Firewall Perimeter Identification
// ADAPTATION: Analysts MUST adjust these arrays to match their specific perimeter security stack.
let firewallVendors = dynamic(["Palo Alto Networks", "Fortinet", "Check Point", "Cisco", "Zscaler"]);
let firewallProducts = dynamic(["Firewall", "PAN-OS", "FortiGate"]);

// STEP 1: Identify Active MDE Endpoints (Base Query)
// Why: We must ensure the source IP belongs to a machine actively running MDE.
// Note: Without this, unmanaged devices (IoT, BYOD) will trigger massive false positives.
let activeMdeNodes = DeviceNetworkEvents
  | where Timestamp between (starttime .. endtime)
  | where isnotempty(LocalIP)
  | distinct LocalIP;

// STEP 2: Establish "Network Truth" via Perimeter Appliances
let firewallTruth = ASimNetworkSessionLogs
  | where TimeGenerated between (starttime .. endtime)
  | where isnotempty(SrcIpAddr) and isnotempty(DstIpAddr)
  | where NetworkProtocol in~ ("TCP")
  // CONDITION A: Verify the log comes from a perimeter hardware/virtual appliance
  | where EventVendor has_any (firewallVendors) or EventProduct has_any (firewallProducts)
  // CONDITION B: Filter out internal-to-internal traffic to focus exclusively on external C2/Exfiltration
  | where ipv4_is_private(DstIpAddr) == false 
      and ipv4_is_private(SrcIpAddr) == true
  // CONDITION C: Ensure the source IP is an active MDE node (Filter early for compute efficiency)
  | where SrcIpAddr in (activeMdeNodes)
  // Preserving Context: Use make_set to keep analyst context without blowing up row counts
  | summarize firewallConnCount = count(), 
              totalBytes = sum(tolong(NetworkBytes)), 
              StartTime = min(TimeGenerated), 
              EndTime = max(TimeGenerated),
              destinationPorts = make_set(DstPortNumber, 100),
              appProtocols = make_set(NetworkApplicationProtocol, 100)
           by SrcIpAddr, DstIpAddr
  | where totalBytes >= byteThreshold or firewallConnCount >= connThreshold;

// STEP 3: Establish "Host Truth" via EDR Telemetry
let mdeTruth = DeviceNetworkEvents
  | where Timestamp between (starttime .. endtime)
  | where isnotempty(LocalIP) and isnotempty(RemoteIP)
  | where Protocol in~ ("TCP")
  | where ipv4_is_private(RemoteIP) == false
  // Join Optimization: Use distinct to drastically reduce memory footprint before the anti-join
  | distinct LocalIP, RemoteIP
  | project SrcIpAddr = LocalIP, DstIpAddr = RemoteIP;

// STEP 4: Capture Endpoint Context & Strict Schema Parsing
let deviceMapping = DeviceNetworkInfo
  | where Timestamp between (starttime .. endtime)
  | where isnotempty(IPAddresses) and isnotempty(DeviceName)
  // Expand the JSON array of IP addresses so each IP gets its own row
  | extend parsedIPs = todynamic(IPAddresses)
  | mv-expand parsedIPs
  | extend LocalIP = tostring(parsedIPs.IPAddress)
  | where isnotempty(LocalIP)
  // Get the most recent heartbeat for each IP address to account for DHCP churn
  | summarize arg_max(Timestamp, DeviceName, DeviceId) by LocalIP
  | project SrcIpAddr = LocalIP, RawDeviceName = DeviceName, DeviceId = tostring(DeviceId);

// STEP 5: Execute the Delta Anti-Join
firewallTruth
  // Left Anti-Join: Keep ONLY the connections seen by the Firewall that are completely missing from MDE
  | join hint.strategy=shuffle kind=leftanti (
      mdeTruth
  ) on SrcIpAddr, DstIpAddr
  // Enrich with Device Details
  | join kind=leftouter (
      deviceMapping 
  ) on SrcIpAddr
  
  // EXCLUSION: Built-in tuning guidance for org-specific baselines.
  // | where DstIpAddr !in~ ("8.8.8.8", "1.1.1.1") // Example: Exclude common DNS bypasses
  
  // Data Sanitization
  // Rename generic schema columns into distinct, narrative evidence
  | extend timestamp = StartTime, 
           InternalIP = tostring(SrcIpAddr), 
           DestinationIP = tostring(DstIpAddr),
           EvasiveConnectionCount = firewallConnCount,
           TotalBytesSent = totalBytes,
           DestinationPorts = destinationPorts,
           HostName = tostring(split(RawDeviceName, ".")[0]),
           DnsDomain = iff(RawDeviceName contains ".", substring(RawDeviceName, indexof(RawDeviceName, ".") + 1), "")


  // ANALYST ACTION: Check 'DestinationIP' against Threat Intel. 
  // If the IP is a known SaaS/Cloud provider, verify if frequency matches beaconing patterns or if the 'TotalBytesSent' aligns with 
  // anomalous Rclone/exfiltration activity. Run a live response memory scan for hidden drivers.
           
  // Ordering Visual Hierarchy
  // Drop raw artifacts and display the chronology left-to-right
  | project timestamp,
            StartTime, 
            EndTime, 
            HostName,
            DnsDomain,
            DeviceId,
            InternalIP, 
            DestinationIP, 
            EvasiveConnectionCount, 
            TotalBytesSent, 
            DestinationPorts,
            appProtocols
  | project-reorder timestamp, 
                    HostName, 
                    InternalIP, 
                    DestinationIP, 
                    EvasiveConnectionCount, 
                    TotalBytesSent, 
                    DestinationPorts, 
                    appProtocols
  | order by TotalBytesSent desc

Analytic Rule Definition

id: 564bf64a-bada-4c6b-8821-53138d660f78
name: Potential rootkit network activity missing from MDE
description: |
  Identifies outbound network connections logged by perimeter firewalls that are entirely missing from Microsoft Defender for Endpoint (MDE) telemetry. This discrepancy strongly indicates a threat actor operating in kernel space, to hide C2 traffic.
description-detailed: |
  Advanced adversaries operating in Ring-0 via BYOVD can unlink WFP (Windows Filtering Platform) callouts or inject raw frames into NDIS. This completely blinds EDR sensors like MDE to outbound network telemetry, while the host otherwise appears healthy. By comparing "Network Truth" (out-of-band firewall appliance logs) against "Host Truth" (EDR telemetry), we trap the adversary in a paradox. Kernel-level tampering on the endpoint cannot hide the physical packet leaving the network boundary.

requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
      - DeviceNetworkInfo
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
  - connectorId: Cisco
    dataTypes:
      - CommonSecurityLog
  - connectorId: CheckPoint
    dataTypes:
      - CommonSecurityLog
tactics:
  - DefenseEvasion
  - CommandAndControl
  - Exfiltration
relevantTechniques:
  - T1562.001
  - T1562.004
  - T1011


query: |
  // DETECTION STRATEGY: Rootkit & BYOVD Ring-0 Network Bypass Detection via Firewall-to-EDR Telemetry Delta.
  // THE MECHANIC: Advanced adversaries operating in Ring-0 via BYOVD can unlink WFP callouts, blinding MDE to outbound network telemetry while the host otherwise appears healthy.
  // THE RESILIENCE: By comparing "Network Truth" (out-of-band firewall logs) against "Host Truth" (EDR telemetry), we trap the adversary. Endpoint tampering cannot hide the physical packet leaving the perimeter.
  
  // Disclaimer: This query can be computationally expensive if run too frequently.
  
  // NOTE ON ANALYTICS RULE CONVERSION, SCOPING & COMPUTE:
  // If converting this Hunting Query into a scheduled Analytics Rule, you MUST do the following to preserve compute and prevent false positives:
  // 1. Scope 'activeMdeNodes' strictly to "Crown Jewel" critical servers (e.g., Domain Controllers, PKI) to limit the left-anti join compute footprint.
  // 2. Introduce an ingestion offset using "ago()" (e.g., | where TimeGenerated between (ago(24h) .. ago(1h))) to account for MDE telemetry batching delays and prevent "Ghost Deltas".
  
  // Time framing: Hunting blade passes the selected time automatically via StartTimeISO and EndTimeISO
  let starttime = todatetime('{{StartTimeISO}}');
  let endtime = todatetime('{{EndTimeISO}}');
  
  // Thresholds: Avoid micro-session noise, DHCP churn, or transient TCP handshakes.
  let byteThreshold = 50000; 
  let connThreshold = 5;
  
  // Tier 1: Firewall Perimeter Identification
  // ADAPTATION: Analysts MUST adju

Required Data Sources

Sentinel TableNotes
ASimNetworkSessionLogsEnsure this data connector is enabled
DeviceNetworkEventsEnsure this data connector is enabled
imNetworkSessionEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/Microsoft 365 Defender/Defense evasion/PotentialRootkitTrafficMissingFromMDE.yaml