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.
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.DATAfor the class-name hashes of__EventFilter,__FilterToConsumerBindingand 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
__FilterToConsumerBindingfollowed 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:
| Label | Meaning |
|---|---|
| Index | Reached through INDEX.BTR and the current map: live |
| Carved record | Record header found by scanning; decoded with the class layout |
| String match | Only 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\subscriptionor 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.DATAdropped), 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.