§ CM

LegacyHive Mounts Another Account’s Registry Hive Into a Session You Control, and the Write That Sets It Up Happens Where Your Auditing Isn’t

About thirty minutes after Microsoft shipped its July 14 Patch Tuesday, by secure.com’s count, someone published working exploit code for a Windows weakness that Patch Tuesday did not fix and Microsoft still has not. It is called LegacyHive, and the short version is that it lets someone who already has a foothold read another account’s registry hive by tricking Windows into mounting that hive into a session they control. The trick is to edit a different account’s profile settings while that account’s registry file is sitting closed on disk, so the edit lands somewhere Windows is not watching, and then log into that account and let Windows follow the tampered setting. There is no Microsoft patch, no advisory, and no CVE, though a third party has published unofficial micropatches. What changed for you is narrower than the headlines suggest. It bites on any box where a lot of different people log in: RDS session hosts, Citrix VDI, the jump box the whole team shares. On those, there is now a public cross-account technique whose setup write bypasses ordinary registry-value telemetry, and whose remaining traces stay transient unless you are already collecting the right authentication, process, module and file events.

The chain, as LevelBlue SpiderLabs reproduced it on fully patched hosts: build a directory hierarchy inside the NT Object Manager namespace under \BaseNamedObjects\Restricted\<UUID> via NtCreateDirectoryObjectEx resolved dynamically out of ntdll, plant Object Manager symbolic links with NtCreateSymbolicLinkObject, then open the helper account’s ntuser.dat offline through the Offline Registry Library (OROpenHive, ORSetValue, ORSaveHive) and rewrite Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders so that helper’s Local AppData resolves into that namespace, where a symbolic link points at the target account’s profile directory. A batch opportunistic lock (FSCTL_REQUEST_BATCH_OPLOCK) on a staged UsrClass.dat (the exploit works out of a GUID-named directory holding hive copies outside the normal profile paths) stalls profile initialization at a chosen moment, converting a race into a deterministic pause. CreateProcessWithLogonW with LOGON_WITH_PROFILE then forces a full profile load of the helper account through the Secondary Logon service. RegOpenUserClassesRoot establishes only that the helper’s classes root opens after that load; what proves the mounted content belongs to the target is the manual inspection in LevelBlue’s reproduction, reading adm‘s key while signed in as u1. That reproduction invokes the tool as LegacyHive.exe u1 test123 adm: helper username, helper password, target username. Hold onto the narrower claim: this is cross-account hive access from an existing foothold, not the “low-privilege account to full system control” that securityonline’s writeup advertises.

TL;DR

  • LegacyHive rewrites a User Shell Folders value inside an unmounted ntuser.dat using Microsoft’s Offline Registry Library, so Sysmon Event ID 13 and Security 4657, which only hook the live registry, never record that edit; if your profile-tampering coverage is “we audit HKU,” that write is invisible to it.
  • The account whose hive is edited is the helper account, whose credentials the attacker already holds. The payoff is that a separate target account’s UsrClass.dat gets mounted into the helper’s classes root. The released PoC needs credentials for a second account plus the username of a third, and yields read access to the target’s hive, not SYSTEM.
  • An early published rule is fitted to the sample. SOC Prime’s detection requires notepad.exe and the literal string CreateProcessWithLogonW in command-line telemetry, and SOC Prime rates its own resilience 2 out of 5 for exactly that reason. Ship it for the copy-paste crowd, tag it for deletion.
  • The more useful signals are offreg.dll loaded by a process outside the paths LevelBlue’s query carves out (System32, SysWoW64, WinSxS and the Defender platform directory, seen via Sysmon Event ID 7), plus off-path ntuser.dat/UsrClass.dat staging and a seclogon-brokered process whose user differs from the caller’s, correlated rather than alerted on individually. Every one of those belongs to the released implementation, not to the vulnerability class.
  • There is no Microsoft CVE, advisory, or KB, so automated vulnerability feeds may never create a finding, so track it as a custom exposure item instead. 0patch has shipped free unofficial micropatches, though their coverage leaves a Server 2019 question open; disable seclogon on session hosts and jump boxes to break the released trigger, and validate App Control against your own policy rather than assuming it blocks the loader.

The registry write your auditing will never see

This is the part that matters more than the object manager cleverness, and it is the part I expect most teams to miss on the first pass.

Sysmon Event ID 13 and Security 4657 hook the live registry. The Configuration Manager fires them when a value changes in a mounted hive. And 4657 fires only where you have also put a SACL on the key and enabled the Registry audit subcategory, which is its own coverage gap on a different day. The LegacyHive edit never touches a mounted hive. It uses Microsoft’s Offline Registry Library, which operates on the hive file on disk while it is unloaded, which is exactly what it exists for. Microsoft’s own description is “registry update scenarios such as servicing an operating system image,” aimed at OEMs and antimalware vendors. By the time the hive is mounted, the value is already wrong and no registry-value event was ever generated for the change.

One correction to a claim I have seen repeated, including in an earlier version of this post: offreg.dll is not documented as an inbox Windows component sitting in System32 on every host. Microsoft distributes it as a binary redistributable shipped in the Windows Driver Kit. Plenty of hosts have it because some installed product brought it along; Defender’s own directory is a common enough source that LevelBlue’s hunting query carves it out. But do not assume the path. Record where the DLL actually lives, who signed it, its hash, and which process loaded it, rather than reasoning from an assumed location.

So if your answer to “do we cover profile tampering” is “we audit HKU,” your coverage of that write is zero. Not partial. Zero. The rest of the chain is a different story, and a better one: explicit-credential logons, process creation, module loads, and file activity on the staged hives are all reachable if you collect them.

On the file side, be precise about which events you are actually getting. Sysmon Event ID 11 records file creation or overwrite and 23/26 record deletion; between them they can show the staged hive copies appearing and being cleaned up. They do not give you hive opens, reads, sharing modes, oplock requests, or every write. For that you want Security 4663 with targeted SACLs on C:\Users\*\ntuser.dat and C:\Users\*\AppData\Local\Microsoft\Windows\UsrClass.dat, which is exactly what ThreatLocker recommends, or equivalent EDR file telemetry.

Do not expect that auditing to hand you a tidy attacker-SID-touching-victim-SID record, though. 4663 reports the principal and process that performed the audited operation, and the operations here are split: the helper-hive work happens while the exploit is impersonating the helper account, and the target-hive mount is performed by ProfSvc as a privileged Windows component. What you are looking for is the shape: reads, an overwrite, and a restoration of profile hives in quick succession. So correlate subject SID, process image, access mask, path, logon ID, and timing rather than looking for a single anomalous pairing.

And if your Sysmon config drops ImageLoad because someone decided Event ID 7 was too expensive back in 2021 (a defensible call at the time, and one that has quietly cost a lot of shops a lot of coverage since), your coverage on the module load is zero too.

What an early published rule keyed on

Detections landed fast, which is good, and the most visible one is fitted to the sample rather than the technique, which is not.

SOC Prime published an early set of LegacyHive rules on July 16. One of them, “Detect LegacyHive Exploit Attempt via CreateProcessWithLogonW and Suspended Notepad,” anchors on the PoC’s observable shape: notepad.exe as the spawned target, and a command-line match on the string CreateProcessWithLogonW. Look, an API name does not appear in the command line of the process that calls it. It appears in that binary’s import table. (It turns up in a command line when somebody pastes a P/Invoke signature into a PowerShell one-liner, which is not what this loader does.)

To SOC Prime’s credit, they say so themselves: the rule carries a published resilience score of 2 out of 5, with the note that an adversary “can easily bypass this by using different API calls that achieve the same result or by using different process names instead of notepad.exe.” LevelBlue makes the same point from the other direction, showing the PoC modified to launch something other than notepad. No vendor is overselling a fragile rule here. They labeled it accurately, and the label is the part that gets lost when the rule is imported into a few hundred tenants.

Ship it if you want the cheap coverage against operators running the sample unmodified, tag it for deletion in a month, and build the durable ones underneath. That pattern, sample strings converted into rules and shipped inside 72 hours, repeats after every high-profile PoC drop, and the rules stay in the ruleset generating false confidence long after they stop catching anything.

Signals that survive a rewrite

The strongest single indicator for this implementation is offreg.dll being loaded by a process that has no business loading it. Sysmon Event ID 7: filter ImageLoaded for offreg.dll, then exclude on the loading process path: Image, not ImageLoaded. That is how LevelBlue’s published query is built, and it is worth reading carefully, because it is easy to misread as a condition on the DLL’s own location:

module.path contains "\\offreg\.dll" AND NOT (src.process.image.path matches "\\\\Windows\\\\(System32|SysWow64|WinSxs)\\\\" OR src.process.image.path contains "\\programdata\\microsoft\\windows defender\\")

Note how narrow those carve-outs actually are: System32, SysWoW64, WinSxS, and the Defender platform directory. A process living somewhere else under C:\Windows is not excluded by that query, so do not describe it to your team as “anything under Windows is fine.”

Offline-image servicing, deployment tooling, security products and backup software are the sensible places to start when you baseline this, and on a general workstation fleet an unexpected loader should be genuinely rare. But resist the urge, which I gave into in the first version of this post, to name DISM, MECM, MDT and the servicing stack as known offreg.dll loaders. Microsoft documents what the library is for; that is not evidence that any particular product loads this redistributable DLL, and LevelBlue’s path exclusions are not proof that each excluded path is a frequent legitimate loader either. Measure actual loader prevalence in your own estate before you exclude any host role, and expect golden-image build hosts and OSD distribution points to be worth investigating first rather than assuming they will account for your volume. Carve out by host role rather than by parent process path, because the servicing stack respawns from enough places that a path allowlist will rot within two patch cycles.

Be honest about what this leg is worth long-term. It is an artifact of the released PoC, which links against the Offline Registry Library. Nothing about the underlying weakness requires that specific DLL. An operator who wants to parse and write a hive can bring their own code. Treat the offreg.dll load as high-value today and implementation-specific tomorrow.

Second, hive files off-path. ntuser.dat outside \Users\, UsrClass.dat outside \Users\*\AppData\Local\Microsoft\Windows\. The false-positive source here is profile management, and it is a serious one: on an FSLogix or Citrix UPM estate this rule as written is unusable, because the entire design of those products is that the hive lives inside a VHDX attached at a non-standard mount point. A plain redirected profile root does it too: set ProfilesDirectory to D:\Profiles by policy and every hive on the box is off-path by this rule’s definition. You have to exclude the container mount path and frxsvc.exe before this rule is deployable, and once you have done that you have also punched a hole an operator could stage inside. Backup agents and DFSR-backed home directories will add their own noise on top.

Those same estates raise a question worth testing rather than assuming: a stock build of the released PoC expects a conventional profile layout, and FSLogix, UPM and redirected-ProfilesDirectory deployments do not present hives where a conventional layout would. Do not read that as safety. It is a reason to test your own configuration, in either direction, before you conclude anything about exposure.

Third, and this one is a state check rather than an event: the Volatile Environment key. LevelBlue’s hunting query looks for LOCALAPPDATA resolving into namespace paths instead of the filesystem, roughly:

registry.keyPath matches 'USER\\S-1-[0-59]-\d{2}-\d{8,10}-\d{8,10}-\d{8,10}-[1-9]\d{3}\\Volatile\ Environment\\[^\\]'
  AND (registry.value contains "\\.\globalroot\" OR registry.value contains "\BaseNamedObjects")

Near-zero false positives, because nothing legitimate points a shell folder at the object manager namespace. Two catches. The RID pattern at the end wants four digits or more, so a built-in Administrator profile (RID 500) walks straight past it; widen it if that account is live on the hosts you sweep. The bigger one is in the name: Volatile Environment exists only while the poisoned profile is loaded, and it can disappear the moment the exploit finishes and tears down its own process. This is a near-real-time hunt, not a compliance sweep. A nightly scan against workstations finds nothing regardless of what happened during the day, and even on an RDS session host with forty days of uptime you are relying on the operator’s session still being open.

And the seclogon path: svchost.exe -k netsvcs -p -s seclogon spawning a process whose user SID differs from the caller’s, with Security 4648 as the paired explicit-credential logon. Service grouping and casing on that command line drift across builds and memory tiers, so key on the service name rather than the literal string. And do not alert on 4648 by itself, ever. In a mid-sized shop that event fires constantly from credentialed vulnerability scans, backup agents, scheduled tasks with stored credentials, and every helpdesk runas. Baseline its volume in your own environment before you set any threshold; the rate depends entirely on your authentication architecture and administrative tooling, and there is no cross-organization number worth quoting.

The pairing is where the value is. Correlate an offreg.dll image load with a seclogon-brokered process creation whose user differs from the initiating user, on the same host, inside about sixty seconds:

index=sysmon host=* (EventCode=7 ImageLoaded="*\\offreg.dll") OR (EventCode=1 ParentCommandLine="*seclogon*")
| sort 0 _time
| streamstats time_window=60s dc(EventCode) as sigs values(Image) as imgs by host
| where sigs=2

That is deliberately a prevalence hunt and nothing more: do not ship it. Any production analytic must add the cross-account user test and the process-GUID or logon-ID causality that LevelBlue’s own query carries. It is weaker than the prose above it in three specific ways. It only requires that both event codes appear on one host inside the window. It does not verify that the spawned process runs as a different user, and it does not tie the two events into one execution chain. LevelBlue’s seclogon query ends with filter src.process.user != tgt.process.user, which is the condition doing the real work, and you correlate from there through process GUIDs, logon IDs, or whatever causality fields your EDR exposes. The explicit sort matters too: Splunk documents that events must be sorted in ascending or descending time order for time_window to behave, and a distributed search will not guarantee that for you. And the ParentCommandLine test is an assumption borrowed from a SentinelOne schema that models causality with separate osSrc, src, parent and tgt fields. Before you ship it, reproduce the flow on each Windows build you support and confirm what Sysmon actually records as ParentImage, ParentCommandLine and ParentProcessGuid. Use streamstats with a sliding window rather than bin span=60s, because bucketing will split the pair across a boundary and silently drop the detection.

Be honest about which half of that pairing is durable. Neither leg is guaranteed. The offreg.dll load belongs to this PoC, not to the technique. The profile-load trigger may be no more fixed: LevelBlue looked at the code and said outright that whether CreateProcessWithLogonW is fundamental or “simply one way of achieving the required profile-loading behavior remains unclear.” A scheduled task with stored credentials, a service logon, or simply waiting for a nightly batch account are the obvious candidates for replacing it, but no public analysis has demonstrated that any of them preserves the oplock and path-switching sequence the exploit depends on. Treat them as bypass hypotheses that warrant reproduction, not as substitutions an operator can make for free. Correlate for the false-positive reduction; keep the individual legs as hunts.

After the servicing carve-outs, this pairing should be rare in a normal week, but derive your alerting threshold from measured prevalence across your own host roles rather than from a number in a blog post. If the volume is higher than you expected, check your image-load exclusions before you touch the rule logic.

What actually survives, and where the sources disagree

Worth reasoning through, because it changes both the remediation and the forensics, and because the two best public analyses do not agree.

LevelBlue titles its cleanup section “Cleanup Without Removing the Impact” and states that temporary files are removed but “the modified registry configuration and Object Manager redirection persist.” ThreatLocker, working from the same public code, says the opposite: “The PoC deliberately restores the original hive and deletes the temporary artifacts during cleanup.”

The weight of the evidence favours ThreatLocker. Its account is a direct description of a cleanup path that terminates the spawned process, closes the Object Manager handles, writes the saved original contents back over the helper’s ntuser.dat, deletes the staged hives and removes the GUID directory. Readers who have gone through the repository independently report the same sequence. I have not audited the source myself, so I am reporting a preponderance rather than delivering a verdict, but LevelBlue’s persistence claim looks inconsistent with what the released code does, and you should not plan around it.

That said, do not write LevelBlue’s position off as a hot take that a slower reading would have corrected. Its analysis is dated July 27, nearly two weeks after the drop, and it still frames the section as “Cleanup Without Removing the Impact,” with a figure captioned “Cleanup logic preserving the persistent hijack.” A considered, late analysis by a team that reproduced the whole chain does not assert residual impact by accident. The possibilities I can see are that cleanup is conditional and does not always complete, that the observation was made before teardown finished, or that some residual object state outlived the handles. I cannot distinguish between those from outside, which is precisely why the operational advice below is written to hold whichever way it falls.

The Object Manager artifacts are shorter-lived than I first wrote, too. The PoC creates temporary named objects (directories and symbolic links without OBJ_PERMANENT), and Windows removes a temporary object’s name once its handle count reaches zero. Cleanup closes those handles explicitly, so the namespace should come apart at that moment. A reboot clears them as well, but it is the backstop, not the mechanism.

What that means practically is that you should not build your response on the assumption of a durable on-disk artifact. If cleanup runs as described, the poisoned User Shell Folders value is gone, the staged hives are gone, and the GUID directory is gone before you arrive. If it is interrupted, or a modified implementation deliberately skips the restore, you may find a shell-folder value pointing into \BaseNamedObjects, which is an excellent artifact and also a likely source of weird, hard-to-triage application breakage for that account.

So: sweep for the on-disk value, because when it is there it is decisive, but treat its absence as telling you nothing. You can pull it off a copy of the hive with reg load against a scratch key without disturbing the live profile. For the released PoC the useful evidence is runtime evidence: the Offreg load, explicit-credential activity, alternate-user process creation, staged hive files, Object Manager operations, oplock behaviour. None of those is individually guaranteed against a rewritten implementation, which is exactly why you want to preserve and correlate as many of them as your telemetry supports rather than picking a favourite.

Non-persistent VDI changes the calculus, though not in the direction I first suggested. Instant Clones or MCS with reset-on-logoff destroy the staged files, any deliberately modified hive, and the namespace, so they limit durable persistence and they destroy your local forensic artifacts. What they do not do is prevent the access: LegacyHive is fundamentally an access primitive, and an operator can read (or, in a write-capable variant, modify) the mounted target hive for as long as that session is up. The defensive implication on those pools is about getting telemetry off the box quickly, not about reduced exploitability while the machine is online. That is an AU-9(2) and AU-11 argument for forwarding aggressively, and it will not be won by the detection team.

How bad is it, really

The released PoC mounts the target’s UsrClass.dat and gives read access to it. ThreatLocker’s assessment is that this yields application data, Explorer history, and forensic artifacts, but “does not directly reveal password hashes or allow privileged code execution in itself.” That is a real capability against a shared session host, since knowing what an administrator has been opening is worth something, but it is not the escalation some coverage described.

The ceiling is higher than the released code, though, and that is the part worth planning around. ACROS Security, who built the micropatch, describe the underlying weakness as letting an attacker “mount any other user’s registry hive in full access mode, and then either extract that user’s stored secrets or modify any values in their registry to affect what gets executed the next time they log in.” ThreatLocker notes the researcher indicated changes to the posted PoC would allow other hives to be mounted. Write access to another user’s hive, on a box where an administrator logs in nightly, is a straightforward path to execution as that administrator. Scope your risk to that, not to the demo.

Controls that cut the class

Control What it does here 800-53
Disable Secondary Logon (seclogon) Breaks the released PoC’s CreateProcessWithLogonW trigger; does not remediate the underlying profile-load weakness CM-6, CM-7
App Control (WDAC) in enforce mode Can block the attacker’s unsigned loader, subject to what your policy already trusts; every Windows component it then abuses is signed and legitimate CM-7(5)
SACLs on profile hive paths Security 4663 on ntuser.dat / UsrClass.dat exposes the reads, overwrite and restoration the registry channel misses; attribution needs correlation, not a single cross-user pairing AU-2, AU-12
Kill dormant/shared local accounts on session hosts The PoC needs valid credentials for a helper account, and account sprawl is what supplies credentialed helpers AC-2, AC-2(3), AC-6
ImageLoad + command-line auditing on Without Event ID 7 and 4688 command line, most of the above detections do not exist AU-2, AU-12
Correlated seclogon/offreg alerting High confidence against variants that change the spawned payload but keep Offreg and Secondary Logon; not a class-wide detection SI-4(16)

A note on that fourth row: the PoC needs credentials for the helper account, but only a username for the target. Killing dormant and shared accounts shrinks the pool of usable helpers, which is the control that bites. Target usernames are generally discoverable and are not a prerequisite you can take away.

On seclogon: disable it on RDS session hosts, jump boxes, and privileged access workstations, where the population of people who need “Run as different user” from Explorer is close to zero and the blast radius is highest. Understand what you are buying, though — this blocks the trigger the PoC happens to use, not the path-resolution weakness underneath it, and other mechanisms that load a profile need evaluating separately. On the general fleet you will get pushback from helpdesk and from a couple of older management agents that shell out through it, and that is a fair fight to have on its own timeline.

App Control is the control I would reach for, with a caveat I would not have written a week ago. An enforcing policy can block the compiled PoC when the policy does not trust that binary — but whether it does depends on your signer, publisher, path, reputation, script, and managed-installer rules, and a policy permissive enough to keep a large estate running may well let an unsigned tool through. Validate it against your actual deployed policy rather than assuming. And the reason most shops cannot answer that question is not technical: the policy has been sitting in audit mode for two years and nobody wants to own the flip. Somebody has to.

There is also, unusually, a patch — just not from Microsoft. ACROS Security shipped free unofficial 0patch micropatches, reported by BleepingComputer on July 21, which redirect the exploit to a temporary user profile hive instead of the targeted account’s. Read the version boundaries carefully, because they do not line up: the micropatches cover Windows 10 2004 and later and Windows Server 2022 and later, while the same reporting says the flaw does not affect versions older than Windows 10 2004 and Windows Server 2019. That leaves Windows Server 2019 apparently in scope for the vulnerability and outside the stated micropatch set. If you run 2019, do not infer from either boundary that you are covered — confirm it with ACROS or test it. More broadly, the researcher claims the PoC works on every supported desktop and server build and LevelBlue confirmed reproduction on fully patched July-2026 systems, but no complete, independently verified affected-version matrix from a single authoritative source exists in public reporting. The best available boundaries are ACROS’s, relayed by BleepingComputer — and those are the ones that leave the Server 2019 question hanging in the first place. Test the builds that actually exist in your estate.

The disclosure shape is the last thing worth planning around. Per secure.com’s account, the same researcher has been dropping uncoordinated Windows zero-days since April 2026 — though the running total is not settled, since secure.com counts six while BleepingComputer names seven earlier disclosures without totalling them, so treat any specific figure as soft. The same caution applies to the thirty minutes in my opening: that is secure.com’s number (“Microsoft shipped 622 fixes on July 14. About thirty minutes later, a researcher published code for a hole that was not among them”), and it is the tightest figure anyone published. Other outlets only committed to “hours.” The interval is rhetorically useful and operationally irrelevant, so do not build anything on the precision. What is consistently reported is the pattern and its consequence: three Microsoft Defender flaws from that series came under active attack shortly after going public. Microsoft’s statement here is that it “is aware of the reported vulnerability and is actively investigating the validity and potential applicability of these claims.” Your vulnerability management pipeline probably assumes an advisory, a KB number, and a CVE to track against, and none of the three exist, so an automated feed may never open a finding. That is an argument for raising a custom exposure record and linking it to the detection work, the App Control validation, the affected-version testing, and the micropatch decision. It is not an argument that the work cannot be tracked.

Sources


This post was engineered and validated through a multi-agent AI workflow — drafted, adversarially reviewed by several independent models, checked against primary sources, and given a human review before publishing. See an inaccuracy, or found this useful? Leave a comment below — corrections and feedback are read and shape what comes next.

Leave a comment