Skip to content

Back to the catalog

Practical Temporal Proximity in KQL
attackthesoc.comwriteup

Practical Temporal Proximity in KQL

A technical KQL deep-dive on detecting hands-on-keyboard attacker activity by correlating multiple suspicious process executions (route.exe, quser.exe, curl.exe, setspn.exe, klist.exe) within a time window on the same device. The author explains why bin()/bin_at() bucketing and Near-Real-Time (NRT) analytic rules are unreliable for temporal-proximity detections, then walks through using the row_window_session() window function with MaxDistanceFromFirst, MaxDistanceBetweenNeighbors, and a Restart expression to build accurate session-based correlation across DeviceProcessEvents in Microsoft Defender XDR.

TL;DR Shows how to use KQL row_window_session instead of bin or NRT rules to reliably correlate multiple suspicious processes within a time window for detecting attacker hands-on-keyboard activity.
Document

What it is

A KQL detection-engineering technique for "temporal proximity" — correlating multiple related events within a bounded time window — built for Microsoft Defender XDR / Sentinel. The worked example detects APT hands-on-keyboard activity: at least 3 of route.exe, quser.exe, curl.exe, setspn.exe, klist.exe launched within 5 minutes of each other.

How it works

The core is the row_window_session window function, which groups rows into "session windows" keyed off a datetime column:

row_window_session(Expr, MaxDistanceFromFirst, MaxDistanceBetweenNeighbors [, Restart])
  • MaxDistanceFromFirst (timespan): the session starts at the first row's timestamp; rows within this span share that SessionID. A row past the span opens a new session.
  • MaxDistanceBetweenNeighbors (timespan): also pulls in rows that are close to the previous row even if beyond the first-row distance — e.g. row_window_session(Timestamp, 5m, 1m) captures a row 6m from row 1 but 1m from row 10.
  • Restart (optional boolean): forces a new SessionID when true, e.g. DeviceName != prev(DeviceName) to keep sessions per-device.

Why the obvious approaches fail:

  • bin() / bin_at(): they round timestamps into fixed buckets with a predefined start point. Events genuinely close in time can straddle a bucket boundary and be counted separately. bin_at() lets you move the start but the boundary problem remains.
  • NRT analytic rules: useful for minute-0 high-severity alerts and may handle ~75% of cases via a 30-minute lookback summarize, but carry documented restrictions (see Sandor Tokesi / Senturean on NRT rule restrictions) that make them unfit as the general solution.

Preconditions / Affected

  • Microsoft Defender XDR / MDE telemetry. Note the author's warning that some tables are unreliable for timing-based detections: DeviceImageLoadEvents was found unusable for detecting libraries loaded in a specific order/timespan.
  • Data must be serialized before any window function is called — use the serialize operator (true for all window functions). Without this, row_window_session won't behave.

Caveats

  • The fetched page cuts off before the final assembled query ("No More Time for Talk: The Solution" begins but the KQL isn't shown here), so the complete detection logic — including how the 3-of-5 process threshold is applied to the resulting sessions — must be read on the source page.
  • The 5-minute window and 3-command threshold are the author's tuning choices for a low false-positive rate against post-exploitation command patterns, not fixed values.

Technologies named here

Techniques named here

  • Temporal Proximity detection
  • Hands-on-keyboard / post-exploitation command detection
  • bin / bin_at time bucketing
  • row_window_session
  • Serialization operator / window functions requirement
  • Potential C2 beacon traffic correlation