Take the State of DFIR Survey

Caseloads, AI, tools and careers. 8 minutes, anonymous, early access to the report.

Take the survey →

Investigating WMI Permanent Event Subscription Persistence

by Vitalii Kukhol, Independent DFIR Researcher

In this article, we’ll work with WMI-Activity, Sysmon, PowerShell, Volatility 3 and Sigma, and set up some hunting along the way.

Permanent WMI subscriptions are a familiar sight in Windows persistence. They don’t need autorun entries, new services or scheduled tasks. Their mechanics are a little more interesting: three linked components are stored in the WMI repository. One defines the trigger conditions, another handles the events, and the third connects the two.

We’ll focus on ActiveScriptEventConsumer. It lets WMI run a script when a specified condition is met, with execution handled by the system process scrcons.exe.

Here’s our lab scenario: we create a subscription that reacts to notepad.exe starting. The subscription’s script then creates a .txt file on the desktop.


Get The Latest DFIR News

The monthly Forensic Focus newsletter, plus webinar invitations and occasional research surveys.

Unsubscribe or change what you receive at any time. We respect your privacy: read our privacy policy.


What We Want to Find Out

  • Which events appear when a WMI subscription is registered.
  • Where to look for evidence of scrcons.exe execution.
  • What we can find on disk and in RAM.
  • How to turn those findings into hunting conditions and Sigma rules.
  • How to remove the subscription afterwards and verify that the persistence is gone.

I disabled antivirus protection for this experiment: even Windows’ built-in protection was doing a good job of blocking my attempts to reproduce the subscription. At this stage, the goal was to understand what happens when the subscription is created, what it leaves behind and how to investigate this technique.

Breaking Down the WMI Objects

  • EventFilter defines the event that must occur for the subscription to trigger. It uses a WQL query. Think of it as a motion sensor.
  • Consumer defines what to do when the filter triggers.
  • ActiveScriptEventConsumer can run our script through Active Scripting. The object specifies the scripting engine and the script contents. In our scenario, the script creates or updates black_cat.txt on the desktop.
  • Binding connects the filter and the consumer. Without it, WMI doesn’t know to use them together, so this component matters just as much.

The Lab Execution Flow

Timestamps in the narrative use the lab’s local time, UTC+03:00, unless explicitly marked as UTC. The screenshots also contain UTC fields.

Here’s how the sequence works:

  1. Notepad starts.
  2. EventFilter detects the event.
  3. FilterToConsumerBinding links the condition to the action.
  4. ActiveScriptEventConsumer provides the script for execution.
  5. scrcons.exe runs it.
  6. Our .txt file appears on the desktop.

Our first key artifact is scrcons.exe, the system process WMI uses to run ActiveScriptEventConsumer scripts. After Notepad started, scrcons.exe was created with PID 7128. It ran as SYSTEM, and its parent image was svchost.exe.

Why does this matter? If scrcons.exe exits before memory acquisition, it may be missing from a later dump. That doesn’t mean it never ran. We need logs, a timeline and other artifacts to establish execution.

Getting Our Detection Sources Ready

Before running the scenario, we prepare the following evidence sources:

  • The Sysmon log.
  • Microsoft-Windows-WMI-Activity/Operational.
  • Microsoft-Windows-PowerShell/Operational.
  • The file system.
  • Memory and the WMI repository.

Sysmon will show us subscription component registration and process creation. WMI-Activity/Operational will show WMI activity during registration and execution. PowerShell/Operational records the code used during registration. The file system gives us the resulting artifact, while memory and the WMI repository provide more places to look for traces.

Checking the Tools

First, we run:

Get-WinEvent -ListLog `
  'Microsoft-Windows-Sysmon/Operational',
  'Microsoft-Windows-WMI-Activity/Operational',
  'Microsoft-Windows-PowerShell/Operational' |
Select-Object LogName, IsEnabled, RecordCount, FileSize, MaximumSizeInBytes

This checks the logs and the space available for storing events.

Next, we check whether detailed logging of executed PowerShell script blocks is enabled:

Get-ItemProperty `
  'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' `
  -ErrorAction SilentlyContinue

The setting we’re looking for is:

EnableScriptBlockLogging = 1

Then we check whether Sysmon is recording the events we need:

Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-Sysmon/Operational'
    Id      = 19,20,21
} -ErrorAction SilentlyContinue |
Select-Object -First 10 TimeCreated, Id, Message

The events we care about here are 19, 20 and 21: filter, consumer and binding creation.

One detail about my experiment: the logs still contained records from earlier attempts. I decided that was actually useful. Real investigations rarely start with a clean slate, so object names alone aren’t enough. We need precise timestamps too.

Now for the Interesting Part

Figure 1. Lab desktop after the subscription was triggered; registration commands are redacted.

The subscription triggered. I’ve blurred the console commands as a precaution. The lab subscription itself is harmless, but I’d still rather take care with what I show.

Sysmon Events

Figure 2. Sysmon registration records: consumer (20), filter (19) and binding (21), within 6 ms.

The screenshot shows all three components of our subscription being registered. From here on, we’ll correlate them using the names BlackCatFilter and BlackCatConsumer, along with their timestamps.

To find the relevant time window, we run:

$start = [datetime]'2026-07-20 22:36:20'
$end   = [datetime]'2026-07-20 22:36:35'

Get-WinEvent -FilterHashtable @{
    LogName   = 'Microsoft-Windows-Sysmon/Operational'
    Id        = 19,20,21
    StartTime = $start
    EndTime   = $end
} |
Sort-Object TimeCreated |
Select-Object TimeCreated, Id, Message

This gives us the subscription’s registration events in the time window we need. Notice that all three events are less than a second apart, which supports treating them as one registration operation.

So far, though, we’ve only established registration. Now we need to find out whether the consumer actually ran and whether scrcons.exe started.

Consumer Execution: Hunting for scrcons.exe

Figure 3. Sysmon process creation records for Notepad and scrcons.exe, including local and UTC timestamps.

Let’s start with Sysmon Event ID 1:

$start = [datetime]'2026-07-20 22:44:50'
$end   = [datetime]'2026-07-20 22:45:10'

Get-WinEvent -FilterHashtable @{
    LogName   = 'Microsoft-Windows-Sysmon/Operational'
    Id        = 1
    StartTime = $start
    EndTime   = $end
} |
Where-Object {
    $_.Message -match 'notepad\.exe|scrcons\.exe'
} |
Sort-Object TimeCreated |
Select-Object TimeCreated, Id, Message

The log contains these entries:

  • 22:44:54.575 — notepad.exe started.
  • 22:44:55.326 — scrcons.exe started, PID 7128.

Less than a second separates the two starts. That timing fits our understanding of the subscription: the filter watches for notepad.exe process creation.

A quick reminder about scrcons.exe: it’s a Windows system component used by WMI to run ActiveScriptEventConsumer scripts. Seeing it run is not, by itself, evidence of an attack. Here, we have context: a WMI subscription had already been registered, its filter was waiting for Notepad to start, and the script host appeared afterwards.

Corroborating the Findings with WMI-Activity

Almost at the same time as scrcons.exe started, WMI-Activity recorded Event ID 5857:

Get-WinEvent -FilterHashtable @{
    LogName   = 'Microsoft-Windows-WMI-Activity/Operational'
    Id        = 5857
    StartTime = $start
    EndTime   = $end
} |
Sort-Object TimeCreated |
Select-Object TimeCreated, Id, Message

Result:

Figure 19. Trigger-time WMI-Activity event 5857, Record ID 511, with provider path and host process fields.

22:44:55.370 — WMI-Activity Event ID 5857, Record ID 511.

Only 44 ms separate the local scrcons.exe creation timestamp from this WMI-Activity event. The latter lists wmiprvse.exe as HostProcess and 2628 as ProcessId; that field is not the scrcons.exe PID 7128.

Finding Another Artifact

At around 22:45:03, the following file was updated:

C:\Users\DFIR_Analyst\Desktop\black_cat.txt

Process and WMI telemetry confirm that the WMI script host started. The later file change is consistent with the consumer’s actions. However, without additional evidence, such as Sysmon Event ID 11, EDR telemetry or Procmon data, we cannot directly attribute the file write to a specific process.

WMI-Activity Events and PowerShell 4104

Let’s look at WMI-Activity events from subscription registration.

We search within a specific time window:

$start = [datetime]'2026-07-20 22:36:20'
$end   = [datetime]'2026-07-20 22:36:35'

Get-WinEvent -FilterHashtable @{
    LogName   = 'Microsoft-Windows-WMI-Activity/Operational'
    Id        = 5857,5859,5861
    StartTime = $start
    EndTime   = $end
} |
Sort-Object TimeCreated |
Select-Object TimeCreated, Id, RecordId, Message

Result:

Figure 16. Registration-time WMI-Activity events 5861, 5859 and 5857.
  • The screenshot shows three events. Event ID 5861 links BlackCatFilter and BlackCatConsumer.
  • Event ID 5859 shows the persistent WQL query watching for notepad.exe to start.
  • Event ID 5857 contains the provider path pointing to scrcons.exe and ResultCode 0x0.
  • All these records belong to the registration stage at 22:36. The separate execution sequence at 22:44 is supported by the Sysmon process creation events and the nearby WMI-Activity Event ID 5857, Record ID 511.

PowerShell Event ID 4104

Our next source is Script Block Logging. It lets us inspect the script that created the WMI components.

The main registration block appeared in the log at 2026-07-20 22:36:26.274, 122 ms before the first Sysmon event recording the consumer.

Here’s how we find it:

$start = [datetime]'2026-07-20 22:36:20'
$end   = [datetime]'2026-07-20 22:36:35'
Get-WinEvent -FilterHashtable @{
    LogName   = 'Microsoft-Windows-PowerShell/Operational'
    Id        = 4104
    StartTime = $start
    EndTime   = $end
} |
Sort-Object TimeCreated |
Select-Object TimeCreated, Id, RecordId, Message

Result:

Figure 10. PowerShell 4104 summary: Record ID 7749, one script-block part, hash and matching object names.

The block we need has Record ID 7749. It was logged in full, in a single part. The checks HasBlackCat, HasActiveScriptConsumer, HasEventFilter and HasBinding all returned true. This means the block contained our object names and references to all three subscription components.

The Record ID lets us return to the exact log entry. The SHA-256 hash lets us verify later that we’re examining the same text. Event ID 4104 shows the executed PowerShell code, while Sysmon events 19–21 confirm subscription registration.

Checking the Timestamps of black_cat.txt

$source = "$env:USERPROFILE\Desktop\black_cat.txt"
$case = "C:\DFIR\ActiveScript-Case"

New-Item -ItemType Directory -Path $case -Force |
    Out-Null

Copy-Item `
    -LiteralPath $source `
    -Destination "$case\black_cat.txt" `
    -Force

$file = Get-Item -LiteralPath $source

[pscustomobject]@{
    FullName         = $file.FullName
    Length           = $file.Length
    CreationTime     = $file.CreationTime
    LastWriteTime    = $file.LastWriteTime
    CreationTimeUtc  = $file.CreationTimeUtc
    LastWriteTimeUtc = $file.LastWriteTimeUtc
} | Format-List

Get-FileHash `
    -LiteralPath $source `
    -Algorithm SHA256 |
Select-Object Algorithm, Hash, Path |
Format-List

Get-TimeZone |
Select-Object Id, DisplayName, BaseUtcOffset |
Format-List

I saved the results of this check several times. The screenshot shows two file states: immediately after the main run and after opening Notepad again later.

Figure 4. Two preserved file states and the lab timezone, UTC+03:00. The later state has a different size and modification time.

Looking for Traces in RAM

The memory image was acquired about 40 minutes after scrcons.exe started. By then, the process could have exited, but other traces of the subscription might still be in memory.

Now we switch to Kali. First, we’ll check the image size and SHA-256 hash, then try opening it with Volatility.

Start with:

MEM="$HOME/DFIR/ActiveScript-Case/evidence/memory_working.raw"
OUT="$HOME/DFIR/ActiveScript-Case/volatility-output"
mkdir -p "$OUT"
stat --printf='Path: %n\nSize: %s bytes\n' "$MEM"
sha256sum "$MEM" | tee "$OUT/00_memory_integrity.txt"

Result:

Size: 4831838208 bytes.

SHA-256: 58c436cb4bd7c6235f543d45b443d55af0b2029c53a12638a76083d78c3513ed

Next, run windows.info:

VOL="$HOME/.venvs/vol3/bin/vol"
"$VOL" -f "$MEM" windows.info |
tee "$OUT/01_windows_info.txt"

Here’s a short summary of the output:

Figure 17. Memory image size and SHA-256, Volatility version, and windows.info output.

Volatility identified 64-bit Windows 10. That’s good news: the image is readable, and we can keep digging.

Looking for scrcons.exe

First, we save the process list with pslist, then check memory separately with psscan:

"$VOL" -f "$MEM" windows.pslist \
  > "$OUT/02_pslist.txt"

Next, scan for process structures in memory:

"$VOL" -f "$MEM" windows.psscan \
  > "$OUT/03_psscan.txt"

For a quick check of the names we’re interested in:

cd "$OUT" || exit 1

FILTER='scrcons\.exe|notepad\.exe|powershell\.exe|WmiPrvSE\.exe'

grep -EHi "$FILTER" 02_pslist.txt 03_psscan.txt | cut -f1-3,9

printf '\nscrcons.exe matches in pslist/psscan:\n'

grep -EHi 'scrcons\.exe' 02_pslist.txt 03_psscan.txt | wc -l

Our result:

Figure 5. Volatility pslist and psscan results: no matches for scrcons.exe in this memory image.

We found notepad.exe, PowerShell and WmiPrvSE.exe in memory. There were zero matches for scrcons.exe.

The image was acquired about 40 minutes later, so the process may already have exited. That is consistent with the search results, but zero matches alone do not establish why it was absent. Sysmon had recorded its start; neither of these searches recovered it from the later image.

Looking for File Traces

Even though these process searches found no scrcons.exe, references to related files could still be in memory. We’ll use windows.filescan to look for them:

"$VOL" -f "$MEM" windows.filescan \
  > "$OUT/04_filescan.txt"

Filter for the results we care about:

grep -Ei \
  'OBJECTS\.DATA|black_cat\.txt|scrcons\.exe|SCRCONS.*\.pf' \
  "$OUT/04_filescan.txt"

Here’s what we have:

Figure 6. File objects found in memory, including OBJECTS.DATA, black_cat.txt, scrcons.exe and its Prefetch path.

We found references to several files in memory:

  • OBJECTS.DATA.
  • black_cat.txt.
  • scrcons.exe.
  • SCRCONS.EXE-969EA702.pf.

OBJECTS.DATA belongs to the WMI repository, which stores permanent subscription objects. Memory also retained paths to our black_cat.txt file, the system executable scrcons.exe and its Prefetch file.

filescan shows that traces of these files remained in memory. It does not establish that scrcons.exe was running when the image was acquired, or that it wrote black_cat.txt.

So far, we’ve only found the Prefetch file as a file object. The .pf file itself needs separate analysis.

Trying to Extract OBJECTS.DATA

The screenshot gives us its memory address: 0x908395c82b20.

Now we’ll use that address with the dumpfiles plugin:

mkdir -p "$OUT/objects_data_dump"
"$VOL" \
  -f "$MEM" \
  -o "$OUT/objects_data_dump" \
  windows.dumpfiles \
  --virtaddr 0x908395c82b20

We couldn’t recover OBJECTS.DATA in full. Volatility saved only the portions of the file that were still present in memory.

I then checked the recovered fragments and searched for the main WMI strings:

Figure 7. Partial OBJECTS.DATA recovery and string searches within the recovered fragments.

The screenshot brings together the OBJECTS.DATA extraction results and the subsequent string search.

We found:

  • ActiveScriptEventConsumer.
  • root\subscription.
  • Win32_ProcessStartTrace.

We didn’t find BlackCatFilter or BlackCatConsumer in these fragments.

No disaster there: this is only part of the repository. Let’s search the whole memory image next.

Searching the Entire Image for Strings

Windows often stores text as UTF-16LE, so we’ll start there:

strings -el "$MEM" |
grep -Ei 'BlackCatFilter|BlackCatConsumer|black_cat\.txt'

Then repeat the search for ASCII strings:

strings -a "$MEM" |
grep -Ei 'BlackCatFilter|BlackCatConsumer|black_cat\.txt'

There were too many matches to show comfortably, so I counted them separately and put together a short summary:

Figure 8. Subscription names and the output filename found in ASCII and UTF-16LE strings across the memory image.

This time, we found BlackCatFilter, BlackCatConsumer and black_cat.txt. The filename appeared a lot, across different encodings.

This search only confirms that related data is present in the image. The number of matching strings does not tell us how many subscriptions existed or how many times the consumer ran.

Putting the Events on a Timeline

After checking the logs, the file and RAM, I combined the main events into one timeline:

Figure 9. Analyst-compiled timeline with UTC and UTC+03:00 columns. The Russian interpretation notes are analytical annotations; the file-write row records temporal correlation, not direct process attribution.

Subscription registration began at 22:36:26. PowerShell Event ID 4104 appeared first, followed by Sysmon and WMI-Activity records of component creation.

At 22:44:54, notepad.exe started. Just under a second later, scrcons.exe appeared. WMI-Activity recorded Event ID 5857 at almost the same time, and black_cat.txt changed at 22:45:03.

Checking the WMI Subscription Objects

We’ve already reconstructed subscription creation from the logs. Now let’s check whether its components are still present in root/subscription.

I always recommend keeping analysis output in dedicated folders. Otherwise, you can make life pretty miserable for yourself later.

$case = "C:\DFIR\ActiveScript-Case"
New-Item -ItemType Directory -Path $case -Force | Out-Null

Collect the filter’s properties:

Get-CimInstance `
  -Namespace root/subscription `
  -ClassName __EventFilter |
Where-Object {
  $_.Name -eq "BlackCatFilter"
} |
Select-Object `
  Name, EventNamespace, QueryLanguage, Query |
Format-List |
Out-File `
  -FilePath "$case\01_filter.txt" `
  -Encoding UTF8

Check the consumer:

Get-CimInstance `
  -Namespace root/subscription `
  -ClassName ActiveScriptEventConsumer |
Where-Object {
  $_.Name -eq "BlackCatConsumer"
} |
Select-Object `
  Name, ScriptingEngine, CreatorSID |
Format-List |
Out-File `
  -FilePath "$case\02_consumer.txt" `
  -Encoding UTF8

And finally, the binding:

Get-CimInstance `
  -Namespace root/subscription `
  -ClassName __FilterToConsumerBinding |
Where-Object {
  $_.Filter -match "BlackCatFilter" -and
  $_.Consumer -match "BlackCatConsumer"
} |
Select-Object Filter, Consumer |
Format-List |
Out-File `
  -FilePath "$case\03_binding.txt" `
  -Encoding UTF8

Here’s the result:

Figure 18. Live WMI object properties: filter query, VBScript consumer and binding.

The state of root/subscription: filter, consumer and binding.

The BlackCatFilter properties show a query that reacts to notepad.exe starting. BlackCatConsumer uses VBScript, and the binding links it to the filter we found.

Unlike the Sysmon records, these commands show the state of WMI at the time of the check. They don’t give us creation timestamps, and their output alone doesn’t prove that the script ran.

Turning the Findings into Sigma Rules

You can still check all of this manually on one PC. But since we’re going full hardcore and thinking in terms of 100+ devices, it makes more sense to turn the findings into Sigma rules.

We already know that Sysmon events 19, 20 and 21 show registration. Event ID 1 can show scrcons.exe starting, while PowerShell 4104 helps establish how the objects were created.

Consumer Registration

Let’s start with Event ID 20.

The rule:

title: WMI ActiveScript Event Consumer Registration
status: experimental

logsource:
  product: windows
  service: sysmon

detection:
  selection:
    EventID: 20
    Type: Script

  condition: selection

Type: Script selects consumers that use scripts. A match doesn’t mean we’ve caught an attack. It gives us a reason to check the object name, its registration time and the related events 19 and 21.

In our case, the rule should detect BlackCatConsumer being created.

Filter and Binding

Next, a rule that looks for filter creation through Event ID 19:

title: WMI Event Filter Registration
status: experimental

logsource:
  product: windows
  service: sysmon

detection:
  selection:
    EventID: 19

  condition: selection

After a match, inspect the query inside the filter. In our case, it watched for notepad.exe to start.

Now for the binding:

title: WMI Filter To Consumer Binding Registration
status: experimental

logsource:
  product: windows
  service: sysmon

detection:
  selection:
    EventID: 21

  condition: selection

This rule shows which objects were linked together.

Bringing the Events Together

A match from any one of these three rules is worth checking. Things get much more interesting when all three events appear within a short time window.

Rule: scrcons.exe Execution

We’ve covered the registration events. Now we need to track scrcons.exe starting separately:

title: WMI Script Consumer Process Execution
status: experimental
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    Image|endswith: '\wbem\scrcons.exe'
  condition: selection

This rule uses process creation events, such as Sysmon Event ID 1, to spot the script host used by ActiveScriptEventConsumer starting.

PowerShell Rule

If the subscription was created through PowerShell, the log may contain an Event ID 4104 with the script text. We’ll use the WMI class names and namespace in our rule:

title: PowerShell WMI Subscription Registration
status: experimental

logsource:
  product: windows
  category: ps_script
  definition: Script Block Logging must be enabled

detection:
  selection_objects:
    ScriptBlockText|contains|all:
      - 'ActiveScriptEventConsumer'
      - '__EventFilter'
      - '__FilterToConsumerBinding'

  selection_namespace:
    ScriptBlockText|contains:
      - 'root/subscription'
      - 'root\subscription'

  condition: selection_objects and selection_namespace

This rule requires all three class names to appear in the same PowerShell script block.

In our lab, Event ID 4104 with Record ID 7749 meets those conditions. The event helps us establish how the subscription was registered.

One catch: if another program created the objects, or they were created through separate commands, the rule may not match. That’s why I recommend using it alongside the Sysmon rules above.

Checking the Detection Conditions Locally

Figure 11. Local validation of saved case telemetry. This output is not a Sigma or SIEM backend test.

The local validator found the registration events, scrcons.exe execution and the PowerShell commands in Event ID 4104. This checks the detection conditions against saved case telemetry; it is not a test of compiled Sigma rules in a SIEM backend.

I redacted the PowerShell code from the displayed report. It isn’t needed for this part of the article, but in an actual investigation it would matter a lot. The Record ID, size, SHA-256 hash and matched indicators were also preserved.

If you move these rules into a SIEM, you still need to test them there separately.

Time to Clean Up

As any analyst knows, before deleting anything, we need to make sure we’ve saved all the evidence and haven’t missed anything.

By this point, I had saved the Sysmon, WMI-Activity and PowerShell logs, along with the collected object metadata, file hashes and memory image.

Before removal, retrieve the subscription components again and store them in variables:

$namespace = 'root/subscription'

$filter = Get-CimInstance `
  -Namespace $namespace `
  -ClassName __EventFilter |
Where-Object {
  $_.Name -eq 'BlackCatFilter'
}

$consumer = Get-CimInstance `
  -Namespace $namespace `
  -ClassName ActiveScriptEventConsumer |
Where-Object {
  $_.Name -eq 'BlackCatConsumer'
}

$binding = Get-CimInstance `
  -Namespace $namespace `
  -ClassName __FilterToConsumerBinding |
Where-Object {
  $_.Filter -match 'Name="BlackCatFilter"' -and
  $_.Consumer -match 'Name="BlackCatConsumer"'
}

[pscustomobject]@{
  FilterCount   = @($filter).Count
  ConsumerCount = @($consumer).Count
  BindingCount  = @($binding).Count
} |
Format-List
Figure 12. Pre-cleanup WMI inventory: one filter, one consumer and one binding.

As we can see, all three subscription components were still present before cleanup. I recommend saving this report separately and recording its SHA-256 hash.

We’ll remove them in this order: binding, consumer, filter.

Removing the Subscription

Binding

$binding |
Select-Object Filter, Consumer

After checking it, remove it:

$binding | Remove-CimInstance

Consumer

$consumer |
Select-Object Name, ScriptingEngine, CreatorSID

Then run:

$consumer | Remove-CimInstance

Filter

$filter |
Select-Object Name, EventNamespace, QueryLanguage, Query

After checking it:

$filter | Remove-CimInstance

Verifying Cleanup

Check root/subscription again:

$postCleanup = try {
    $filterAfter = @(
        Get-CimInstance `
            -Namespace $namespace `
            -ClassName __EventFilter `
            -ErrorAction Stop |
        Where-Object {
            $_.Name -eq 'BlackCatFilter'
        }
    )

    $consumerAfter = @(
        Get-CimInstance `
            -Namespace $namespace `
            -ClassName ActiveScriptEventConsumer `
            -ErrorAction Stop |
        Where-Object {
            $_.Name -eq 'BlackCatConsumer'
        }
    )

    $bindingAfter = @(
        Get-CimInstance `
            -Namespace $namespace `
            -ClassName __FilterToConsumerBinding `
            -ErrorAction Stop |
        Where-Object {
            $_.Filter -match 'Name="BlackCatFilter"' -or
            $_.Consumer -match 'Name="BlackCatConsumer"'
        }
    )

    [pscustomobject]@{
        Check            = 'Post-cleanup WMI verification'
        ComputerName     = $env:COMPUTERNAME
        CheckedAt        = Get-Date
        Namespace        = $namespace
        QuerySucceeded   = $true
        FilterCount      = $filterAfter.Count
        ConsumerCount    = $consumerAfter.Count
        BindingCount     = $bindingAfter.Count
        CleanupConfirmed = (
            $filterAfter.Count -eq 0 -and
            $consumerAfter.Count -eq 0 -and
            $bindingAfter.Count -eq 0
        )
        Error             = $null
    }
}
catch {
    [pscustomobject]@{
        Check            = 'Post-cleanup WMI verification'
        ComputerName     = $env:COMPUTERNAME
        CheckedAt        = Get-Date
        Namespace        = $namespace
        QuerySucceeded   = $false
        FilterCount      = $null
        ConsumerCount    = $null
        BindingCount     = $null
        CleanupConfirmed = $false
        Error             = $_.Exception.Message
    }
}

$postCleanup | Format-List

The result we’re looking for is:

  • QuerySucceeded: true.
  • Counts of 0/0/0.
  • CleanupConfirmed: true.
  • No error reported in Error.
Figure 13. Later verification on 7 August 2026: the WMI queries succeeded and all three counts are zero.

The query succeeded, and our subscription is gone.

I took this screenshot during a later repeat check, so its timestamp differs from the post-cleanup test shown next.

The Post-Cleanup Test

Now let’s see how the system behaves after the subscription has been removed.

First, preserve the old black_cat.txt under a different name. Start Notepad, wait a few seconds, then check WMI, the desktop for a new file, and the Sysmon events.

Result:

Figure 20. Post-cleanup control test on 24 July 2026: zero WMI components, no new output file and no new scrcons.exe event.

In the post-cleanup test, New black_cat.txt created: False shows that no new file appeared. New scrcons.exe Sysmon events: 0 means Sysmon recorded no new start of the consumer’s script host.

In a real case, it’s worth repeating the check after a reboot to make sure a second persistence mechanism hasn’t brought the subscription back and that cleanup has held.

Increasing Log Sizes

During the checks, I noticed that WMI-Activity had a log size limit of just 1 MB. PowerShell Operational was capped at about 15 MB, and Sysmon at 64 MB.

When logs run out of room, new records can overwrite old ones. That’s a problem for us, so let’s give them more space.

Set WMI-Activity to 32 MB

wevtutil sl `
  "Microsoft-Windows-WMI-Activity/Operational" `
  /ms:33554432

Set PowerShell Operational to 64 MB

wevtutil sl `
  "Microsoft-Windows-PowerShell/Operational" `
  /ms:67108864

Set Sysmon Operational to 128 MB

wevtutil sl `
  "Microsoft-Windows-Sysmon/Operational" `
  /ms:134217728

Result:

Figure 14. Log size limits before and after adjustment; the logs remain in circular mode.

The log sizes before and after the change.

Increasing the size doesn’t enable new event types. It simply gives the logs more room. Choose the limits based on event volume, available disk space and whether the logs are being forwarded to a SIEM.

Limitations of the Lab

This whole exercise took place in a lab where we already knew the object names. In a real case, you can’t search only for these names: a subscription can be named anything.

The memory image was acquired late. That underlines the importance of responding quickly: a later memory capture may no longer contain traces of the full sequence of events.

The detection conditions were checked locally against saved events. The Sigma rules were not tested through a Sigma or SIEM backend. If you use them in your own SIEM, you’ll need to validate and tune them for your environment, particularly for false positives.

Wrapping Up

We investigated a WMI subscription that falls under ATT&CK technique T1546.003. Sysmon showed us the filter and consumer being registered and the binding between them, while PowerShell 4104 preserved the script block.

I won’t go over the scenario again. My main takeaway is that one evidence source really isn’t enough here. Logs, detections, the file system and memory show different parts of the same story. The sooner you set up collection and give the logs enough room, the better your chances of reconstructing an attack. And remember: timestamps and fast detection can make or break the whole investigation.

My GitHub Repository

ActiveScriptEventConsumer-DFIR

Sources

Vitalii, also known as Debusterlil, is an independent DFIR researcher focused on Windows forensics, threat detection, and reverse engineering. His work centers on reproducible Windows labs, endpoint artifact analysis, timeline reconstruction, memory forensics, and turning investigative findings into practical detection logic using tools such as Sysmon, PowerShell, Volatility 3, and MITRE ATT&CK. He is also a published author at Xakep.ru and is currently expanding his skills in malware analysis, x86-64 assembly, and reverse engineering.

Latest from Forensic Focus

Leave a Comment