Skip to content

Recovering Deleted WMI Persistence From OBJECTS.DATA

Why deleted WMI filters, consumers and bindings survive in OBJECTS.DATA, how index walking and carving differ, and how python-cim and PyWMIPersistenceFinder compare.

Published on 5 min read

TL;DR. Deleting a WMI subscription does not erase it. Its bytes stay in OBJECTS.DATA until WMI reuses the space. Walking the index (python-cim's approach) gives you what is live; scanning the whole file (as David Pany's PyWMIPersistenceFinder does with text) also finds what was removed. Use both, and keep track of which method found what.

Why deleted objects survive

The repository is a page store. When an instance is deleted or rewritten, WMI updates INDEX.BTR and writes a new generation of the MAPPING file. The old record is not zeroed:

  • if its whole page is released, the page becomes unmapped — no longer referenced, bytes intact;
  • if other records remain in the page, the record's space becomes slack inside a page that is still in use.

In both cases the record keeps its header — the UTF-16 SHA-256 of its class name — and its heap of strings: the query, the command line, the script, the binding references. Until another write lands there, it is recoverable. How long that lasts depends on how busy WMI is on the host: collect early.

Two ways to read a repository

Structured: follow the index

This is how python-cim works, and what Windows does. INDEX.BTR keys such as NS_…/CI_<hash("__EVENTFILTER")>/IL_….page.record.length point through the current MAPPING file to a record. The class definitions give each property its name, type and slot, so every value decodes exactly — including class defaults and the namespace of each object.

It only sees what the index references: live objects.

Carving: scan every byte

Two variants:

  • Record carving. Search OBJECTS.DATA for the class-name hashes of __EventFilter, __FilterToConsumerBinding and the consumer classes. Each hit is the start of an instance record, live or not. Decode it with the class layout and check that it holds together: the record's heap must end exactly where its header says, and the heap must start with the class name.
  • String carving. PyWMIPersistenceFinder's idea: look for __FilterToConsumerBinding followed by …EventConsumer.Name="…" and __EventFilter.Name="…", then for the filter's query and the consumer's command next to their names. It needs no structure at all, so it still works when a record's header was overwritten — at the cost of guessing which string is which.

Carving sees everything, but alone it cannot say whether a hit is live. That verdict comes from the MAPPING file: a hit in a mapped page, inside a table-of-contents entry, is in use; a hit in an unmapped page or in slack is freed.

How WMI Parser combines them

WMI Parser runs the index walk when INDEX.BTR and a MAPPING file are present, then always carves the whole file, and merges the results. Every object shows how it was found:

LabelMeaning
IndexReached through INDEX.BTR and the current map: live
Carved recordRecord header found by scanning; decoded with the class layout
String matchOnly the binding's text survived (PyWMIPersistenceFinder-style)

A carved copy identical to a live object is merged into it as an extra location. A carved copy that differs is listed on its own as Recovered, and if a live object has the same key, it is marked as an older version. The sample repository shows all three cases: a deleted logon filter and its binding, found by record carving and again as a headerless fragment in page slack; and an older version of a consumer that ran an encoded PowerShell command before it was changed to C:\ProgramData\Intel\m64.exe -svc.

Reading recovered objects carefully

  • No namespace. Records do not store their namespace; only the index knows it. A recovered binding may have lived in root\subscription or elsewhere.
  • Split records. A record longer than its page continues on the next logical page, which may sit anywhere physically. Once freed, the pieces are hard to reassemble; WMI Parser then reports a partial read.
  • Layouts. Without the repository's class definitions (only OBJECTS.DATA dropped), decoding relies on the built-in layout of the standard Windows classes and on the self-checks above. Custom consumer classes are then only found through their binding text.
  • Time. Instance headers carry two undocumented FILETIMEs. They often follow the write time of that copy, which can help order versions, but they are not documented creation dates.

A recovered binding proves the subscription existed at some point, not that it ran. Look for execution evidence of the consumer's payload (Prefetch, Amcache, event logs) and for the WMI-Activity events in the investigation checklist.

FAQ

Can a deleted WMI subscription be recovered?

Often, yes. Deleting an object releases its page or record space in OBJECTS.DATA without wiping it, so the filter, consumer or binding can be carved until WMI reuses that space.

What is the difference between python-cim and PyWMIPersistenceFinder?

python-cim walks INDEX.BTR and the MAPPING file to decode live objects with their class layouts. PyWMIPersistenceFinder searches OBJECTS.DATA for binding text, which needs only that one file and can hit deleted records, but decodes less.

How do I know if a recovered object was deleted or just older?

If an active object with the same key exists and its content differs, the recovered copy is most likely an earlier version. If no active object has that key, it was deleted.

Related articles

Read CCM_RecentlyUsedApps from the WMI repository: which programs each user ran, when last and how often, including older copies carved from OBJECTS.DATA.
Step-by-step checklist for WMI persistence: live queries, the repository, event IDs 5860 and 5861, Sysmon 19-21, execution evidence and a safe clean-up.
The two WMI subscriptions Windows ships — SCM Event Log and BVTFilter/BVTConsumer — what they contain, why they are harmless, and how attackers can abuse the names.