latest event 59m ago · pipeline live
← Blog
Malware Analysis

Suspected BatShadow BYOVD: Abusing Safetica's Signed Driver (CVE-2025-70795) to Terminate EDR from Kernel Mode

Shakil Hossain · 15 Aug 2026

The chain installs Safetica's signed ProcessMonitor driver (CVE-2025-70795) and drives it over IOCTL to terminate PPL-protected security processes from kernel mode. The kernel call is only the fallback: a rule keyed on the driver misses the userland tier that fires first, against everything else on the host.

The previous post followed a recruitment email through a renamed Microsoft Word, a 157 MB sideloaded DLL, and an encrypted container that unpacks a complete CPython 3.14 runtime to C:\Users\Public\Project\Python.

Four of those 137 unpacked files are malicious. One of them installs a kernel driver and drives it to terminate security software.

vcruntime.dll: File Format

vcruntime.dll is 14,776 bytes of base64 text. It is not a PE and has no MZ header. It decodes to 339 lines of Python.

That has consequences for two common triage approaches. Name-based triage sees a runtime DLL sitting next to a Python installation and discounts it, since that is exactly where a real vcruntime.dll belongs, and the genuine vcruntime140.dll is right there beside it. Magic-based triage reads the first two bytes, does not find MZ, and files it as not-an-executable.

Its top-level constants establish most of its behaviour before any function is read:

DEVICE_PATH = r"\\.\STProcessMonitorDriver"
IOCTL_KILL  = 0xB822200C
DRIVER_SYS  = "STProcessMonitor.sys"
DRIVER_NAME = "STProcessMonitor"
PYTHON_DIR  = r"C:\Users\Public\Project\Python"

Driver ID: Safetica ProcessMonitor (CVE-2025-70795)

STProcessMonitor.sys is 35,400 bytes, PE32+, native subsystem: a genuine kernel-mode driver. It carries a 9,800-byte Authenticode blob issued to Microsoft Windows Hardware Compatibility Publisher, under Microsoft Windows Third Party Component CA 2012, chaining to Microsoft Root Certificate Authority 2010, valid 2025-02-20 to 2026-02-18 and countersigned by Microsoft's timestamp service.

The driver's real name is ProcessMonitorDriver.sys. It belongs to Safetica Technologies, whose DLP product legitimately ships it. It is CVE-2025-70795, affecting all versions before 11.34.5.0, and it is catalogued on LOLDrivers. A second CVE, CVE-2026-0828, was later assigned against the same root cause: insufficient access control on the driver's IOCTL interface. Our SHA-256 is an exact match to that LOLDrivers entry, its recorded creation date matches the compile timestamp in the PE, and the file's own version resource reads 11.11.4.0, the legacy pre-mitigation build that a low-privileged user could drive.

This is a standard BYOVD pattern. The operator obtained no signature; they bundled a legitimate, signed, publicly documented vulnerable driver for which a public proof-of-concept already circulates.

That changes the remediation. Certificate revocation is not the appropriate response: it would break Safetica's product for its paying customers, and the certificate is not the underlying issue. The alternative also requires a caveat. The build here is version 11.11.4.0, and Safetica's advisory states Microsoft adds 11.11.4.0 and older to the Windows vulnerable driver blocklist only from 15 October 2026, with 11.26.18.0 following on 15 November. Until then HVCI, WDAC and Smart App Control do not stop it. What works today is explicit hash-blocking of both known builds, alerting on the service name, and the behavioural rule below. Where Safetica is legitimately deployed, 11.34.5.0 or newer is the first build in which the attack surface is removed; 11.26.18.0 only raises the bar to requiring LocalSystem, which this chain already has by the time it installs the driver.

IOCTL Dispatch Analysis

What the malware uses and what the driver offers are different questions, and the second determines how bad a compromise is. We read the dispatch routine end to end rather than inferring the driver's capability from the client that drives it.

Every IOCTL the driver accepts.

The driver is meant to be a process-creation notification broker: register, poll, unregister. 0xB822200C is the vulnerability. It performs no caller validation, no PID allow-listing, and no protected-process check, and it runs ZwTerminateProcess at PreviousMode = KernelMode.

That last detail is what makes the primitive effective. When the object manager sees a kernel-mode caller it skips both SeAccessCheck and the signature-level protection check. A PPL or ELAM-protected antivirus process that would reject a usermode TerminateProcess has no defence against this path, because the termination request originates inside the trust boundary.

The abuse potential is bounded, which matters for scoping an incident: the dispatcher has four branches and a default of STATUS_INVALID_DEVICE_REQUEST. There is no arbitrary file write, no kernel memory read or write, no handle duplication, and no callback-removal primitive reachable through the interface. The file-access imports belong to the driver's own tracing subsystem, whose paths the driver chooses, not the caller. Abuse potential here is arbitrary process termination only. That is significant, but an actor seeking kernel memory patching would require a second driver.

There is also an implementation defect that provides a telemetry opportunity. The OBJECT_ATTRIBUTES passed to ZwOpenProcess has Attributes = 0, so no OBJ_KERNEL_HANDLE. The handle lands in the calling process's handle table rather than System's. For the window between open and close, an unsigned usermode process holds a full-access handle to a protected process in its own table. The window is narrow, but handle-creation events attributing PROCESS_ALL_ACCESS on a PPL process to an unsigned binary are worth hunting.

Two-tier Termination Logic

The kernel path is not the first thing the killer tries, and a detection built around it therefore misses most of what the killer does.

The killer is two-tier. Before it touches the driver at all, it calls OpenProcess(PROCESS_TERMINATE) followed by TerminateProcess against every target in its list. The IOCTL is only the fallback for processes that survive that.

Tier 1 runs against everything, tier 2 is the fallback.

The consequences are direct:

  • A detection keyed on the driver load, or on IOCTL 0xB822200C, does not observe tier 1. Tier 1 is the path that executes against every unprotected AV product on the host, which in most estates is the majority of them.
  • If the driver never loads (a blocklist hit, HVCI enabled, or the user declining the UAC prompt), the killer still enumerates the whole list in userland. The chain degrades rather than failing closed.
  • The loop runs every 50 ms in a daemon thread, so terminated products cannot restart between passes.
  • It is implemented twice, independently: once in this Python stager and again natively in the Go loader at the end of the chain. Either implementation alone is sufficient.

Kill List Composition

The list is process.txt, 2,031 entries. It is not one list but three, concatenated.

Three authorship layers in one file.

40 of the 164 names in the last block already exist in the first. Nobody deduplicated; the block was concatenated rather than merged. Every entry in it also carries the literal placeholder string test where the other 1,867 carry a real vendor name.

Two things follow. First, 2,031 should not be read as a capability claim. The stager loads the file into a Python set keyed on the lowercased process name, so the 386 duplicate lines collapse and the effective list is 1,645 unique names. The base block carries vendor tags for products discontinued a decade ago, and several names in the appended block look generated from product names rather than observed from real telemetry.

Second, the appended block reflects enterprise-focused tradecraft. The targets include Splunk, QRadar, ArcSight, Graylog and LogPoint forwarders; Sysmon; and BeyondTrust, ThreatLocker, WALLIX and PAM360. Terminating SIEM forwarders and privileged-access agents is consistent with an operator expecting to land inside a monitored corporate estate. It is not required against a job-seeker's personal laptop, which is the victim profile the delivery email evidences.

Detection: Mass Termination Correlation

Because tier 1 is both the path that executes and the path most likely to be missed, the highest-value detection is behavioural and vendor-agnostic: one process attempting to terminate many distinct security products in a short window. It keys on the enumeration behaviour rather than a hash, so it survives the operator swapping drivers or rewriting the killer.

It is a Sigma correlation, so it needs a base rule feeding it. The target list here is trimmed for readability; the deployed version carries the full set.

title: Process Termination Attempt Against Security Product
id: 3b6e5c19-8f40-4a72-84d1-e97b2c5f0a63
status: experimental
description: |
  Base rule. A process requests terminate rights on a known security product.
  Low fidelity alone - intended as the input to the correlation rule below.
author: Shakil Hossain
date: 2026/08/14
tags:
  - attack.defense-evasion
  - attack.t1562.001
logsource:
  product: windows
  category: process_access
detection:
  selection_access:
    GrantedAccess|contains:
      - '0x1'
      - '0x1F0FFF'
      - '0x1FFFFF'
  selection_target:
    TargetImage|endswith:
      - '\MsMpEng.exe'
      - '\MpDefenderCoreService.exe'
      - '\SenseIR.exe'
      - '\MsSense.exe'
      - '\CSFalconService.exe'
      - '\SentinelAgent.exe'
      - '\elastic-agent.exe'
      - '\CylanceSvc.exe'
      - '\RepMgr.exe'
      - '\SAVService.exe'
      - '\McShield.exe'
      - '\avp.exe'
      - '\ekrn.exe'
      - '\bdagent.exe'
      - '\AvastSvc.exe'
      - '\360tray.exe'
      - '\TmListen.exe'
      - '\PccNTMon.exe'
      - '\fshoster32.exe'
  filter_self:
    SourceImage|endswith:
      - '\services.exe'
      - '\svchost.exe'
      - '\MsMpEng.exe'
      - '\CSFalconService.exe'
      - '\SentinelAgent.exe'
  condition: selection_access and selection_target and not filter_self
falsepositives:
  - Security products terminating their own helper processes during upgrade
  - Endpoint management tooling performing legitimate uninstalls
level: low
---
title: EDR Killer Mass Termination Burst
id: a95c2f68-d371-4e09-9b46-1c8f7d2e05ba
status: experimental
description: |
  A single process attempts to terminate ten or more DISTINCT security
  products within 30 seconds. This is the behavioural signature of an EDR
  killer sweeping a hardcoded process list, independent of which vulnerable
  driver (if any) backs it.
author: Shakil Hossain
date: 2026/08/14
tags:
  - attack.defense-evasion
  - attack.t1562.001
correlation:
  type: value_count
  rules:
    - 3b6e5c19-8f40-4a72-84d1-e97b2c5f0a63
  group-by:
    - Computer
    - SourceImage
  timespan: 30s
  condition:
    field: TargetImage
    gte: 10
falsepositives:
  - A supported AV migration that removes a competing product. Rare, and
    normally attributable to a signed vendor uninstaller - check SourceImage
    signature before dismissing.
level: critical

The gte: 10 threshold and the 30-second window are reasoned from the sample's own loop timing, not tuned against real telemetry. We had none in this engagement. Baseline the threshold against your own environment before enabling the rule.

For the driver itself, SigmaHQ already ships driver_load_win_vuln_drivers.yml and its name-based sibling, both LOLDrivers-derived, so a custom driver-load rule is mostly redundant. Keep it only for the explicit Safetica hash pair, or better, consume LOLDrivers as a feed.

What has no SigmaHQ equivalent is the correlation above, and a rule on kernel driver services created from user-writable paths. The chain stages its driver in C:\Users\Public\Project\Python\ before copying it to System32\drivers and calling CreateServiceW, retrying up to five times when the service is marked for delete. A SERVICE_KERNEL_DRIVER whose ImagePath resolves under \Users\Public\ should not occur on a normal host.

ATT&CK coverage for the BYOVD stage.

Indicators of Compromise

  • Driver, the build in this chain: 70bcec00c215fe52779700f74e9bd669ff836f594df92381cbfb7ee0568e7a8b · SHA-1 711ef221526997039e804a18db9647c91680bbe2 · MD5 485a04675df7cf40a8865706e003eb25
  • Driver, second known vulnerable build: 5b4f59236a9b950bcd5191b35d19125f60cfb9e1a1e1aa2e4f914b6745dde9df · SHA-1 68fec379f2ae76c3d2ce913f7be650cea1d06990 · MD5 5761bd63da03686fc480245da7bd1e9f. Block both.
  • Encrypted payload plugins.dll: 614188d7a86f594d61af7a31fd3ac8af22dadcabd918775230d639279dde0519 (6,894,643 bytes)
Service name        STProcessMonitor
Device object       \\.\STProcessMonitorDriver
Driver on disk      %SystemRoot%\System32\drivers\STProcessMonitor.sys
IOCTL               0xB822200C   (DeviceType 0xB822, function 0x803, METHOD_BUFFERED)
Payload wrapper     ENCAES1 | salt[16] | nonce[12] | ciphertext | tag[16]
PBKDF2 passphrase   MySecretPass123!   SHA-256, 200000 iterations, 32-byte key
Cipher              AES-256-GCM

For this specific sample: salt b69f49501c8884760f07b8ac841b24ed, nonce fb30d35f6c9284d5e7549771, tag 8681636dc5aa3cf03bde3717cefcf194, derived key cd97fc9cd4ec0fe384e355420bd81d3b977b52d4a1f31ba6f91be3fe9694929c. The GCM authentication tag verifies, which means the decryption is proven rather than assumed: a wrong key cannot produce a passing tag.

The final piece covers what comes out of that decryption: a 6.9 MB Go loader whose internal module name is batman, the C2 protocol we recovered in full, and what the panel's own error handling disclosed about the platform behind it.

Suspected BatShadow BYOVD: Abusing Safetica's Signed Driver (CVE-2025-70795) to Terminate EDR from Kernel Mode — Threat Intel BD