commit 52e684d55b3de5a8e0fdf34445c52b9ebf521078 Author: sbousseaden Date: Mon Jun 8 11:50:58 2026 +0100 EDRUnChoker: fileless WMI defense against EDRChoker QoS throttling diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d5dd25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.token +.env +.env.* diff --git a/Get-EdrChokerDefenseStatus.ps1 b/Get-EdrChokerDefenseStatus.ps1 new file mode 100644 index 0000000..01e6245 --- /dev/null +++ b/Get-EdrChokerDefenseStatus.ps1 @@ -0,0 +1,103 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Reports fileless EDRChoker WMI defense subscription status. +#> +[CmdletBinding()] +param( + [string]$FilterName = 'EDRChokerDefense_QoSFilter', + [string]$ConsumerName = 'EDRChokerDefense_QoSConsumer', + [string]$TimerId = 'EDRChokerDefense_Timer' +) + +$WmiNs = 'root\subscription' +$MaxThrottleBps = 1048576 + +$ExactTargets = @( + 'amsvc', 'cb', 'cbdefense', 'cetasevc', 'cntaosmgr', 'cramtray', 'crsvc', + 'cylancesvc', 'cybereasonav', 'cyserver', 'cyveraservice', 'cyvrfsflt', + 'eiconnector', 'ekrn', 'elastic-agent', 'elastic-endpoint', 'endpointbasecamp', + 'executionpreventionsvc', 'filebeat', 'fortiedr', 'hurukai', 'logprocessorservice', + 'mpdefendercoreservice', 'msmpeng', 'mssense', 'ntrtscan', 'pccntmon', 'qualysagent', + 'sensecncproxy', 'senseir', 'sensendr', 'sensesampleuploader', 'sentinelagent', + 'sentinelagentworker', 'sentinelbrowsernativehost', 'sentinelhelperservice', + 'sentinelservicehost', 'sentinelstaticengine', 'sentinelstaticenginescanner', + 'sfc', 'taniumclient', 'taniumcx', 'taniumdetectengine', 'tmccsf', 'tmlisten', + 'tmbmsrv', 'tmwscsvc', 'traps', 'winlogbeat', 'wscommunicator', 'xagt' +) + +$VendorPatterns = @( + 'elastic', 'sentinel', 'cybereason', 'carbonblack', 'carbon\', 'crowdstrike', + 'cylance', 'defender', 'msmpeng', 'mssense', 'windows defender', 'senseir', + 'tanium', 'qualys', 'fortiedr', 'mcafee', 'symantec', 'broadcom', 'trend micro', + 'eset', 'kaspersky', 'bitdefender', 'sophos', 'fireeye', 'mandiant', 'traps', + 'cbdefense', 'cyvera', 'hurukai', 'xagt', 'filebeat', 'winlogbeat', 'elastic-endpoint' +) + +function Get-AppBaseName { + param([string]$Path) + $p = $Path.Trim().ToLowerInvariant() + if ($p.EndsWith('.exe')) { $p = $p.Substring(0, $p.Length - 4) } + $segments = $p -split '[\\/]' + return $segments[-1] +} + +function Test-KnownSecurityTarget { + param([string]$AppPath) + if ([string]::IsNullOrWhiteSpace($AppPath)) { return $false } + $lower = $AppPath.ToLowerInvariant() + $base = Get-AppBaseName -Path $lower + foreach ($t in $ExactTargets) { + if ($base -eq $t -or $base.EndsWith($t)) { return $true } + } + foreach ($p in $VendorPatterns) { + if ($lower.Contains($p)) { return $true } + } + return $false +} + +function Test-MaliciousQosPolicy { + param($Policy) + $appPath = [string]$Policy.AppPathNameMatchCondition + if ([string]::IsNullOrWhiteSpace($appPath)) { return $false } + $throttle = [uint64]$Policy.ThrottleRateAction + if ($throttle -eq 0) { return $false } + if (Test-KnownSecurityTarget -AppPath $appPath) { return $true } + if ($throttle -le $MaxThrottleBps) { return $true } + return $false +} + +Write-Host '=== WMI subscription (root\subscription) ===' -ForegroundColor Cyan +Get-WmiObject -Namespace $WmiNs -Class __IntervalTimerInstruction -ErrorAction SilentlyContinue | + Where-Object { $_.TimerId -eq $TimerId } | + Format-List TimerId, IntervalBetweenEvents + +Get-WmiObject -Namespace $WmiNs -Class __EventFilter -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq $filterName } | + Format-List Name, Query, EventNamespace + +Get-WmiObject -Namespace $WmiNs -Class ActiveScriptEventConsumer -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq $consumerName } | + Format-List Name, ScriptingEngine, KillTimeout + +Get-WmiObject -Namespace $WmiNs -Class __FilterToConsumerBinding -ErrorAction SilentlyContinue | + Where-Object { $_.Filter -match [regex]::Escape($filterName) } | + Format-List Filter, Consumer + +Write-Host '=== Policies matching hardened detection rules ===' -ForegroundColor Cyan +$all = @(Get-CimInstance -Namespace root/standardcimv2 -ClassName MSFT_NetQosPolicySettingData -ErrorAction SilentlyContinue) +$malicious = @($all | Where-Object { Test-MaliciousQosPolicy -Policy $_ }) + +Write-Host "Count: $($malicious.Count) / $($all.Count) total QoS policies" +if ($malicious.Count -gt 0) { + $malicious | Select-Object Name, AppPathNameMatchCondition, ThrottleRateAction, Owner | + Format-Table -AutoSize +} + +Write-Host '=== Recent remediation events (Application / EDRChokerDefense) ===' -ForegroundColor Cyan +Get-WinEvent -FilterHashtable @{ + LogName = 'Application' + ProviderName = 'EDRChokerDefense' +} -MaxEvents 15 -ErrorAction SilentlyContinue | + Select-Object TimeCreated, Id, LevelDisplayName, Message | + Format-Table -Wrap -AutoSize diff --git a/Install-EdrChokerWmiDefense.ps1 b/Install-EdrChokerWmiDefense.ps1 new file mode 100644 index 0000000..1631983 --- /dev/null +++ b/Install-EdrChokerWmiDefense.ps1 @@ -0,0 +1,303 @@ +#Requires -RunAsAdministrator +#Requires -Version 5.1 +<# +.SYNOPSIS + Registers a fileless permanent WMI subscription against EDRChoker QoS policies. +.DESCRIPTION + Timer-based subscription in root\subscription (no files on disk). Embedded VBScript + removes QoS policies that throttle known EDR/security agents (any rate > 0) or apply + aggressive throttling (<= 1 Mbps) to app-path policies — including paths without .exe. +#> +[CmdletBinding()] +param( + [string]$FilterName = 'EDRChokerDefense_QoSFilter', + [string]$ConsumerName = 'EDRChokerDefense_QoSConsumer', + [string]$TimerId = 'EDRChokerDefense_Timer', + [int]$PollIntervalSeconds = 5 +) + +$ErrorActionPreference = 'Stop' +$WmiNs = 'root\subscription' +$pollIntervalMs = $PollIntervalSeconds * 1000 +$LogSource = 'EDRChokerDefense' +$LogName = 'Application' + +function Register-DefenseEventSource { + if (-not [System.Diagnostics.EventLog]::SourceExists($LogSource)) { + New-EventLog -LogName $LogName -Source $LogSource -ErrorAction Stop + } +} + +function Write-DefenseInstallLog { + param([string]$Message) + Write-EventLog -LogName $LogName -Source $LogSource -EventId 1000 -EntryType Information -Message $Message +} + +function Get-LocalAdministratorsCreatorSid { + $sid = New-Object System.Security.Principal.SecurityIdentifier 'S-1-5-32-544' + $bytes = New-Object byte[] ($sid.BinaryLength) + $sid.GetBinaryForm($bytes, 0) + return $bytes +} + +function New-WmiSubscriptionObject { + param( + [Parameter(Mandatory)] + [string]$Component, + [Parameter(Mandatory)] + [string]$ClassName, + [Parameter(Mandatory)] + [hashtable]$Properties + ) + + $scope = New-Object System.Management.ManagementScope("\\.\$WmiNs") + $scope.Options.EnablePrivileges = $true + $scope.Connect() + + $mgmtClass = New-Object System.Management.ManagementClass( + $scope, + (New-Object System.Management.ManagementPath($ClassName)), + $null + ) + $instance = $mgmtClass.CreateInstance() + + foreach ($key in $Properties.Keys) { + $instance.SetPropertyValue($key, $Properties[$key]) + } + + try { + $instance.Put() | Out-Null + return $instance + } + catch { + $message = $_.Exception.Message + if ($_.Exception.InnerException) { + $message = $_.Exception.InnerException.Message + } + $hresult = '0x{0:X8}' -f $_.Exception.HResult + throw "Failed to create WMI $Component ($ClassName). $message (HRESULT $hresult)" + } +} + +if (Get-WmiObject -Namespace $WmiNs -Class __EventFilter -Filter "Name='$FilterName'" -ErrorAction SilentlyContinue) { + throw "Subscription '$FilterName' already exists. Run Uninstall-EdrChokerWmiDefense.ps1 first." +} + +$creatorSid = Get-LocalAdministratorsCreatorSid +$wql = "SELECT * FROM __TimerEvent WHERE TimerID = '$TimerId'" + +# Detection tiers (embedded — reinstall to update): +# 1) Known EDR/AV/SIEM process basename or vendor substring + any ThrottleRateAction > 0 +# 2) Any app-path policy with ThrottleRateAction > 0 and <= 1 Mbps (EDRChoker evasion with non-8 values) +# Path normalization strips .exe and uses the final path segment, so "C:\...\MsMpEng" still matches. +$scriptText = @' +On Error Resume Next + +Const MAX_THROTTLE_BPS = 1048576 + +Dim qosSvc, policies, pol + +Set qosSvc = GetObject("winmgmts:\\.\root\standardcimv2") +Set policies = qosSvc.ExecQuery("SELECT * FROM MSFT_NetQosPolicySettingData") + +For Each pol In policies + If IsMaliciousPolicy(pol) Then RemoveMaliciousPolicy pol +Next + +Sub WriteDefenseLog(eventId, entryType, message) + Dim proc, pid, cmd, safe + safe = message + safe = Replace(safe, "'", "''") + safe = Replace(safe, """", "'") + cmd = "powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ""Write-EventLog -LogName Application -Source EDRChokerDefense -EventId " & eventId & " -EntryType " & entryType & " -Message '" & safe & "' -ErrorAction SilentlyContinue""" + Set proc = GetObject("winmgmts:\\.\root\cimv2:Win32_Process") + proc.Create cmd, Null, Null, pid +End Sub + +Sub RemoveMaliciousPolicy(pol) + Dim appPath, throttle, policyName, instanceId, tier, msg + appPath = pol.AppPathNameMatchCondition + throttle = GetNumericThrottle(pol.ThrottleRateAction) + policyName = pol.Name + instanceId = pol.InstanceID + If MatchesKnownSecurityTarget(appPath) Then + tier = "tier1-known-edr" + Else + tier = "tier2-aggressive-throttle" + End If + On Error Resume Next + pol.Delete_ + If Err.Number = 0 Then + msg = "action=remediate qos_policy=" & policyName & " target=" & appPath & " throttle_bps=" & throttle & " tier=" & tier & " instance_id=" & instanceId + WriteDefenseLog 1002, "Warning", msg + Else + msg = "action=remediate_failed qos_policy=" & policyName & " target=" & appPath & " error=" & Err.Description + WriteDefenseLog 1003, "Error", msg + Err.Clear + End If + On Error GoTo 0 +End Sub + +Function GetNumericThrottle(val) + If IsNull(val) Or IsEmpty(val) Then + GetNumericThrottle = 0 + Exit Function + End If + If Len(CStr(val)) = 0 Then + GetNumericThrottle = 0 + Exit Function + End If + On Error Resume Next + GetNumericThrottle = CDbl(val) + If Err.Number <> 0 Then GetNumericThrottle = 0 + On Error GoTo 0 +End Function + +Function GetBaseName(path) + Dim p, i, c + p = LCase(Trim(path)) + If Len(p) = 0 Then + GetBaseName = "" + Exit Function + End If + If Right(p, 4) = ".exe" Then p = Left(p, Len(p) - 4) + i = Len(p) + Do While i > 0 + c = Mid(p, i, 1) + If c = "\" Or c = "/" Then + GetBaseName = Mid(p, i + 1) + Exit Function + End If + i = i - 1 + Loop + GetBaseName = p +End Function + +Function MatchesExactTarget(base) + Dim t, i + Dim targets + targets = Array( _ + "amsvc", "cb", "cbdefense", "cetasevc", "cntaosmgr", "cramtray", "crsvc", _ + "cylancesvc", "cybereasonav", "cyserver", "cyveraservice", "cyvrfsflt", _ + "eiconnector", "ekrn", "elastic-agent", "elastic-endpoint", "endpointbasecamp", _ + "executionpreventionsvc", "filebeat", "fortiedr", "hurukai", "logprocessorservice", _ + "mpdefendercoreservice", "msmpeng", "mssense", "ntrtscan", "pccntmon", "qualysagent", _ + "sensecncproxy", "senseir", "sensendr", "sensesampleuploader", "sentinelagent", _ + "sentinelagentworker", "sentinelbrowsernativehost", "sentinelhelperservice", _ + "sentinelservicehost", "sentinelstaticengine", "sentinelstaticenginescanner", _ + "sfc", "taniumclient", "taniumcx", "taniumdetectengine", "tmccsf", "tmlisten", _ + "tmbmsrv", "tmwscsvc", "traps", "winlogbeat", "wscommunicator", "xagt" _ + ) + For i = 0 To UBound(targets) + t = targets(i) + If base = t Then + MatchesExactTarget = True + Exit Function + End If + If Len(base) >= Len(t) Then + If Right(base, Len(t)) = t Then + MatchesExactTarget = True + Exit Function + End If + End If + Next + MatchesExactTarget = False +End Function + +Function MatchesVendorPattern(pathLower) + Dim p, i + Dim patterns + patterns = Array( _ + "elastic", "sentinel", "cybereason", "carbonblack", "carbon\", "crowdstrike", _ + "cylance", "defender", "msmpeng", "mssense", "windows defender", "senseir", _ + "tanium", "qualys", "fortiedr", "mcafee", "symantec", "broadcom", "trend micro", _ + "eset", "kaspersky", "bitdefender", "sophos", "fireeye", "mandiant", "traps", _ + "cbdefense", "cyvera", "hurukai", "xagt", "filebeat", "winlogbeat", "elastic-endpoint" _ + ) + For i = 0 To UBound(patterns) + p = patterns(i) + If InStr(1, pathLower, p, 1) > 0 Then + MatchesVendorPattern = True + Exit Function + End If + Next + MatchesVendorPattern = False +End Function + +Function MatchesKnownSecurityTarget(appPath) + Dim pathLower, baseName + pathLower = LCase(Trim(appPath)) + If Len(pathLower) = 0 Then + MatchesKnownSecurityTarget = False + Exit Function + End If + baseName = GetBaseName(pathLower) + If MatchesExactTarget(baseName) Then + MatchesKnownSecurityTarget = True + Exit Function + End If + MatchesKnownSecurityTarget = MatchesVendorPattern(pathLower) +End Function + +Function IsMaliciousPolicy(pol) + Dim appPath, throttle + appPath = pol.AppPathNameMatchCondition + throttle = GetNumericThrottle(pol.ThrottleRateAction) + + If Len(Trim(appPath)) = 0 Then Exit Function + If throttle <= 0 Then Exit Function + + ' Tier 1: any throttle on a known EDR/AV/SIEM target (catches non-8 bps evasion). + If MatchesKnownSecurityTarget(appPath) Then + IsMaliciousPolicy = True + Exit Function + End If + + ' Tier 2: aggressive throttle on any app-path policy (EDRChoker-style abuse). + If throttle <= MAX_THROTTLE_BPS Then + IsMaliciousPolicy = True + End If +End Function +'@ + +$null = New-WmiSubscriptionObject -Component 'IntervalTimerInstruction' -ClassName __IntervalTimerInstruction -Properties @{ + TimerId = $TimerId + IntervalBetweenEvents = $pollIntervalMs + SkipIfPassed = $false +} + +$filter = New-WmiSubscriptionObject -Component 'EventFilter' -ClassName __EventFilter -Properties @{ + Name = $FilterName + EventNamespace = $WmiNs + QueryLanguage = 'WQL' + Query = $wql + CreatorSID = $creatorSid +} + +$consumer = New-WmiSubscriptionObject -Component 'ActiveScriptEventConsumer' -ClassName ActiveScriptEventConsumer -Properties @{ + Name = $ConsumerName + ScriptingEngine = 'VBScript' + ScriptText = $scriptText + KillTimeout = 60000 + CreatorSID = $creatorSid +} + +$null = New-WmiSubscriptionObject -Component 'FilterToConsumerBinding' -ClassName __FilterToConsumerBinding -Properties @{ + Filter = $filter + Consumer = $consumer + CreatorSID = $creatorSid +} + +Register-DefenseEventSource +Write-DefenseInstallLog "action=install subscription=EDRChokerDefense poll_interval_seconds=$PollIntervalSeconds log_source=$LogSource" + +Write-Host 'Fileless WMI subscription installed (hardened detection, polls every' $PollIntervalSeconds 's).' +Write-Host " Timer : $TimerId (${pollIntervalMs}ms)" +Write-Host " Filter : $FilterName" +Write-Host " Consumer : $ConsumerName (ActiveScriptEventConsumer / VBScript)" +Write-Host ' Detection : known EDR targets (any throttle) + app-path throttle <= 1 Mbps' +Write-Host " Event log : $LogName / Source $LogSource (ID 1002 = remediate, 1003 = failed)" +Write-Host '' +Write-Host 'Reinstall was required to update embedded VBScript if upgrading.' +Write-Host 'Verify: .\Get-EdrChokerDefenseStatus.ps1' +Write-Host "SOC query : Get-WinEvent -FilterHashtable @{ LogName='$LogName'; ProviderName='$LogSource' } -MaxEvents 20" diff --git a/README.md b/README.md new file mode 100644 index 0000000..474d857 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Unchoker + +**Fileless WMI remediation for [EDRChoker](https://github.com/TwoSevenOneT/EDRChoker)** counters QoS abuse (`pacer.sys`) that throttles EDR agents to near-zero network bandwidth. + +Registers a permanent subscription in `root\subscription` (no files on disk). A 5-second timer runs embedded VBScript that deletes malicious `MSFT_NetQosPolicySettingData` policies targeting known security products or aggressive app-path throttles. + +## Scripts + +| Script | Purpose | +|---|---| +| `Install-EdrChokerWmiDefense.ps1` | Deploy subscription (elevated) | +| `Uninstall-EdrChokerWmiDefense.ps1` | Remove subscription | +| `Get-EdrChokerDefenseStatus.ps1` | Check subscription and policy count | + +## Quick start + +```powershell +.\Install-EdrChokerWmiDefense.ps1 +.\Get-EdrChokerDefenseStatus.ps1 +``` + +## SOC / event log + +Each successful cleanup writes a **Warning** to the **Application** log under source **EDRChokerDefense**. One event is emitted per removed policy, useful as remediation evidence and to correlate with EDRChoker activity on the host. + +image + + +| Event ID | Meaning | +|---|---| +| 1000 | Subscription installed | +| 1001 | Subscription removed | +| 1002 | Malicious QoS policy removed | +| 1003 | Remediation failed | + +**1002 example:** `action=remediate qos_policy=02zxnnzr target=elastic-endpoint.exe throttle_bps=8 tier=tier1-known-edr instance_id={...}` + +```powershell +Get-WinEvent -FilterHashtable @{ LogName='Application'; ProviderName='EDRChokerDefense' } -MaxEvents 50 +``` + +Forward `ProviderName="EDRChokerDefense"` via WEF, Splunk UF, Elastic Agent, etc. + +## Protect the subscription + +Attackers may delete or modify the WMI objects in `root\subscription` to disable defense. **Baseline** the subscription after install and alert on changes. + +**Sysmon** (recommended): enable and monitor: + +| Sysmon ID | What to watch | +|---|---| +| **19** | `WmiEventFilter` created/modified/deleted | +| **20** | `WmiEventConsumer` created/modified/deleted | +| **21** | `WmiEventFilter` ↔ consumer binding changes | + +Alert on any activity involving `EDRChokerDefense_QoSFilter`, `EDRChokerDefense_QoSConsumer`, or `EDRChokerDefense_Timer`, and on **new** `ActiveScriptEventConsumer` / `CommandLineEventConsumer` instances outside your change window. + +**Periodic validation** (GPO/script): `Get-WmiObject -Namespace root\subscription -Class __EventFilter | Where-Object Name -like 'EDRChokerDefense*'` + +## References + +- [EDRChoker](https://github.com/TwoSevenOneT/EDRChoker) +- [EDRChoker research](https://www.zerosalarium.com/2026/06/edrchoker-choking-telemetry-stream-block-edr.html) diff --git a/Uninstall-EdrChokerWmiDefense.ps1 b/Uninstall-EdrChokerWmiDefense.ps1 new file mode 100644 index 0000000..7bc959e --- /dev/null +++ b/Uninstall-EdrChokerWmiDefense.ps1 @@ -0,0 +1,53 @@ +#Requires -RunAsAdministrator +#Requires -Version 5.1 +<# +.SYNOPSIS + Removes the fileless EDRChoker WMI defense subscription. +#> +[CmdletBinding()] +param( + [string]$FilterName = 'EDRChokerDefense_QoSFilter', + [string]$ConsumerName = 'EDRChokerDefense_QoSConsumer', + [string]$TimerId = 'EDRChokerDefense_Timer' +) + +$ErrorActionPreference = 'Stop' +$WmiNs = 'root\subscription' +$LogSource = 'EDRChokerDefense' +$LogName = 'Application' + +function Remove-WmiSubscriptionComponent { + param( + [string]$Class, + [string]$Filter + ) + + $instance = Get-WmiObject -Namespace $WmiNs -Class $Class -Filter $Filter -ErrorAction SilentlyContinue + if ($instance) { + $instance | Remove-WmiObject + Write-Host "Removed $Class ($Filter)" + } +} + +$bindings = @(Get-WmiObject -Namespace $WmiNs -Class __FilterToConsumerBinding -ErrorAction SilentlyContinue | + Where-Object { $_.Filter -match [regex]::Escape($FilterName) -or $_.Consumer -match [regex]::Escape($ConsumerName) }) + +foreach ($binding in $bindings) { + $binding | Remove-WmiObject + Write-Host 'Removed __FilterToConsumerBinding' +} + +Remove-WmiSubscriptionComponent -Class ActiveScriptEventConsumer -Filter "Name='$ConsumerName'" +Remove-WmiSubscriptionComponent -Class __EventFilter -Filter "Name='$FilterName'" +Remove-WmiSubscriptionComponent -Class __IntervalTimerInstruction -Filter "TimerId='$TimerId'" + +if ([System.Diagnostics.EventLog]::SourceExists($LogSource)) { + try { + Write-EventLog -LogName $LogName -Source $LogSource -EventId 1001 -EntryType Information -Message 'action=uninstall subscription=EDRChokerDefense' + } + catch { + Write-Warning "Could not write uninstall event: $($_.Exception.Message)" + } +} + +Write-Host 'Uninstall complete. WMI subscription removed; past Application log events retained for SOC.'