top of page

Choking Defender With Native Windows QoS Policies (EDRChoker)

  • Writer: Damien van der Linden
    Damien van der Linden
  • Jun 9
  • 8 min read
Disclosure note: The detection gap described here, the living-off-the-land PowerShell variant of a publicly known technique evading detection.. The underlying technique is already public via the EDRChoker project. Nothing here is novel attack surface. I have spent most of my weekend trying to detect this behaviour but failed to do so, this was a massive learning experience for me and the blog below can include mistakes, or missed observations on my part.

TL;DR


  • Malware can use a native Windows feature, QoS policies (New-NetQosPolicy), to throttle a security tool's network bandwidth to near zero. The process keeps running and no tamper alert fires, but it can no longer reach the cloud.

  • The compiled tool that does this, EDRChoker, is detected by Defender as Trojan:MSIL/EDRChoker.DA!MTB. The same logic written in a few lines of native PowerShell is not detected (as of yet). This means behavioral detection still lacks.

  • Against Microsoft Defender for Endpoint the technique works. Applied across the sensor binaries it fully severs the device: the timeline stops, and Live Response stops responding and good luck trying to send a quick scan to the machine.

  • I tried two detection approaches. A behavioural one on the PowerShell activity, and an effect-based one on sensor health. I even posted one on LinkedIn, but later realized neither holds up as a reliable live detection, for reasons that are interesting in their own right.

  • In the absence of a proper detection, the current advice is to hunt for QoS policy activity and treat it as suspicious by nature, backed by out-of-band logging that does not depend on the sensor being able to talk to the cloud.


The Technique


Quality of Service (QoS) policies are a legitimate Windows networking feature for managing bandwidth. New-NetQosPolicy lets an administrator cap the throughput of a specific application by its executable path. Entirely benign, until you point it at a security tool and set the cap to effectively zero (or well, 10).


I found a sample on URLHaus that kickstarted this investigation. The sample that started this looked like the following, with the policy name shortened:


powershell -NoProfile -NonInteractive -Command "if (Get-NetQosPolicy -Name 'Limit_<random>' -ErrorAction SilentlyContinue) { Remove-NetQosPolicy -Name 'Limit_<random>' -Confirm:$false }; New-NetQosPolicy -Name 'Limit_<random>' -AppPathNameMatchCondition 'C:\Program Files (x86)\360\360Safe\safemon\wdswfsafe.exe' -ThrottleRateActionBitsPerSecond 10 -NetworkProfile All"

Breaking it down:


  • Idempotency check. It looks for a pre-existing policy of the same name and removes it before recreating. Classic dropper behaviour, safe to re-run.

  • -AppPathNameMatchCondition. Targets a specific security binary by full path. In this sample it is wdswfsafe.exe, the network-facing component of 360 Total Security.

  • -ThrottleRateActionBitsPerSecond 10. Caps the target's bandwidth at ten bits per second. Not ten kilobits. Ten bits, roughly 1.25 bytes per second, which is functionally an outage.

  • -NetworkProfile All. Applies across every network profile.

The effect is that the security tool keeps running and looks healthy, but it cannot reach its cloud backend. No reputation lookups, no signature updates, no telemetry upload. Nothing was killed, no service stopped, no registry key edited, so the tamper-protection mechanisms most products rely on never fire. The randomised policy name exists for one reason, to defeat any detection that hunts on a fixed name.


I decided to give it some rest as I couldn't find a good foolproof way to detect this (I mean, your logs are basically dead and sensorstatus doesn't really seem to update). In the meantime (weekend) the PoC of EDRChoker dropped, that's when I realized I was onto something, I wasn't crazy.

Tool versus technique: EDRChoker is caught, the PowerShell is not


The compiled implementation of this idea, EDRChoker, is a .NET binary, and Defender flags it on sight:

Trojan:MSIL/EDRChoker.DA!MTB, Severe


The MSIL in the detection name is the giveaway. Defender has a static signature for the managed-code assembly. It recognises the compiled tool.


Run functionally identical logic as native PowerShell, New-NetQosPolicy against the same class of target, and nothing happens. No detection, no alert, no quarantine, oh yeah, and no logs because we choked it again.



This is the tool versus technique gap that every detection engineer meets eventually.


Defender clearly knows the technique exists, because the EDRChoker signature proves it. What is missing is coverage of the living-off-the-land variant that uses nothing but a built-in cmdlet. There is no malicious binary to fingerprint, so the only remaining option is to catch it behaviourally. As we will see, that is harder than it sounds.


Does it work against Defender for Endpoint? Yes.


My first instinct was that Defender's layered, protected architecture might shrug this off, and an early dead end with the antivirus engine seemed to support that. Throttling MsMpEng.exe only affected the MAPS cloud-lookup channel, not the EDR telemetry that feeds the SOC. The sensor itself, MsSense.exe, runs as a Protected Process Light and showed no user-mode network sockets when inspected, which briefly looked like it might be out of reach.


It is not. Applied across the full family of sensor binaries by path, the technique severs the device completely. The likely egress process is one of the on-demand sensor components such as the communication manager, which is not always running when you look, but a QoS policy matches by path, not by running process, so it catches that binary the moment it launches.


The device timeline stops updating. New telemetry stops arriving in Defender XDR. Live Response stops working, which is the clearest possible confirmation that the sensor has lost its cloud channel.


The technique does what it claims, and the earlier idea that Defender's architecture neutralises it was wrong.


The nuance that survives is about target architecture rather than vendor. A monolithic, user-mode security tool that handles its own networking is severed cleanly by one policy on one binary. Defender's sensor is distributed across many processes, so it needs the broader set of paths covered, but once they are, the outcome is the same.


Why I could not build a reliable detection


This is the part worth your time, because the failure modes are the actual lesson. Maybe I'm missing something, and if so, please reach out. I spent most of my weekend trying to come up with a good detection rule but all my rules eventually failed to catch the behaviour.


The self-suppression problem

The defining feature of this technique is that it blinds the same telemetry channel a detection would rely on. We confirmed that the New-NetQosPolicy activity is generated and logged locally on the device. The problem is delivery. Under a persistent throttle, the sensor stays severed and those events sit in the local buffer. They reached the cloud in testing only because the policies were removed and the buffer drained. A real attacker does not remove the policies. They leave the throttle in place, the buffer stays stuck, and the local buffer is finite, so under a long enough severance the earliest and most important events are the first to be dropped.


The consequence is blunt. Any detection that depends on the Defender sensor shipping the evidence to the cloud is, at best, retroactive. It can confirm what happened after the throttle is lifted. It cannot give you visibility while the attack is active, which is exactly when you need it.


What PowerShell logging actually captures

Even setting the delivery problem aside, the content of the logged events does not support a precise detection. PowerShell Script Block Logging records the script source text, split per physical line, with variables left unexpanded. In practice that means:


  • A cmdlet written across multiple lines with backtick continuation is captured in fragments. The cmdlet name lands in one event, its parameters in others.

  • Variables are logged literally. A script that uses -ThrottleRateActionBitsPerSecond $rate and -AppPathNameMatchCondition $path records those as the strings $rate and $path, never the actual throttle value or target path.



So the only element reliably present in the telemetry is the cmdlet name itself. The throttle value and the target are visible only in the original malware's inline one-liner, which hardcodes literals. Any attacker who moves the logic into a script with variables, which is trivial, removes the two fields a precise detection would key on. You are left detecting the existence of New-NetQosPolicy, which is too blunt to call a detection and too noisy in environments where QoS is managed by hand.


Detecting the effect instead of the action

If the action cannot be reliably detected, the natural pivot is to detect the effect: the sensor going quiet. Defender tracks a sensor health state, and in principle a device transitioning from healthy to a degraded or inactive state, inferred cloud-side from the absence of expected check-ins, would flag the severance even when the action events never arrive.


In practice this did not produce a usable live detection either. The health-state signal that would catch this technique fast enough did not transition reliably or quickly enough in testing to be operationally useful, and the broader pattern of a sensor going quiet is heavily contaminated by entirely benign causes, namely devices that shut down, sleep, change networks, or restart the agent.


Distinguishing a malicious severance from an ordinary offline event requires proving the device is still alive while its sensor is silent, and you cannot prove that from the very telemetry source that has gone silent. It needs corroboration from outside Defender, which moves the problem off the platform entirely.


Do you see my problem? Or am I completely blind and missing something here?


Where that leaves us


I currently have no reliable live detection for the PowerShell variant of this technique on Defender for Endpoint as of writing. The combination is genuinely awkward:

  • No binary signature applies, because the living-off-the-land variant has no binary.

  • A precise behavioural detection is undermined because Script Block Logging fragments the command and hides the values, and because the technique suppresses the channel that would deliver the evidence.

  • An effect-based detection on sensor health did not transition fast or cleanly enough to be operational and is hard to separate from benign offline events.

The structural answer is telemetry that does not ride the Defender sensor's cloud channel, so that severing the sensor does not also sever your visibility. PowerShell Module Logging (Event ID 4103) records bound parameters with values expanded, unlike Script Block Logging, and forwarded to a SIEM by an independent agent it would show the real throttle value and target even when the script uses variables. Sysmon paired with Windows Event Forwarding, or any collector shipping the PowerShell and Security logs straight to the SIEM, survives the sensor being choked. If the only path your evidence has to the cloud is the sensor, this technique blinds your detection and its telemetry at the same moment.

Current advice: hunt QoS policies as suspicious


Until there is a dependable detection, treat QoS policy activity as inherently suspicious and hunt it. New-NetQosPolicy invoked through PowerShell is rare in most estates, because QoS is normally configured through Group Policy or Intune rather than ad-hoc command lines.


Any occurrence is worth a look, and the idempotent Get, Remove, New sequence against a security-tool path is a strong tell when the values happen to be visible.


A starting point for manual hunting, not a detection rule, and subject to the same buffering and fragmentation limits described above, so treat an empty result as inconclusive rather than clean:


union
(DeviceProcessEvents
    | where Timestamp > ago(7d)
    | where FileName in~ ("powershell.exe","pwsh.exe")
    | where ProcessCommandLine has "NetQosPolicy"
    | extend Cmd = ProcessCommandLine),
(DeviceEvents
    | where Timestamp > ago(7d)
    | where ActionType == "PowerShellCommand"
    | where AdditionalFields has "NetQosPolicy"
    | extend Cmd = tostring(parse_json(AdditionalFields).Command))
| project Timestamp, DeviceName, InitiatingProcessAccountName, Cmd
| sort by Timestamp desc

On the response side, when you do investigate a device, enumerate applied policies in both stores, because an attacker can use the non-persistent active store, which a default enumeration misses and which clears on reboot:

Get-NetQosPolicy
Get-NetQosPolicy -PolicyStore ActiveStore

A little side-note, to exploit this technique you need admin privileges, so this is not a low-privileged attack.

Takeaways


  1. The technique works against Defender for Endpoint. Applied across the sensor binaries it fully severs the device, including Live Response.

  2. Tool signatures do not cover techniques. EDRChoker is caught, its native PowerShell equivalent is not.

  3. The hardest part is not writing detection logic, it is that the technique suppresses the channel that detection logic depends on, and PowerShell logging does not preserve the values you would key on. Effect-based detection on sensor health did not prove reliable either.

  4. The durable answer is out-of-band logging that survives the sensor being choked. Until that is in place, hunt QoS policy activity and treat it as suspicious by nature.


Thanks for reading my blog, if you do see mistakes on my part, or have detection idea's, feel free to reach out to me on my LinkedIn page. Security is community. Till next time!


Additional resources:


Comments


2025-2026 LindenSec | ©
bottom of page