Summary
Threre is a heap buffer overflow vulnerability in a DCOM service that can be leveraged to achieve LPE from a Medium IL standard user to System IL.
This was tested on Windows 11 25H2 Build 26200.8457 (latest patch as of May 2026).
Vendor Response
Microsoft has released a patch as part of its September 2026 release, under the CVE-2026-69459.
Credit
Two independent security researcher, won third play in the Windows LPE category during TyphoonPWN 2026.
Affected Versions
- Windows 11 25H2 Build 26200.8457
Technical Analysis
When changing Power Setting values, attacker-controlled data and length can be passed to the PSM callback. The CrmStateMonitorStateSet function copies the attacker’s data into the state buffer without validating the supplied length, resulting in a heap overflow.
Root Cause
PsmServiceExtHost.dll (PsmService) is a library used by the DcomLaunch svchost.exe process (running at System IL) to track power setting states such as power and battery status.
[CRM Object] +0x00: Type +0x08: Lock +0x18: Flink +0x20: Blink +0x28: StatePtr = CRM + 0x38 +0x30: Metadata +0x38: StateBuffer
PsmService uses CRM (Composite Resource Manager) heap objects to track states. The CRM object types include global, acdc, battery, display, powersave, and lowepoch.
[PsmServiceExtHost.dll]
__int64 __fastcall CrmStateMonitorPowerSettingsStart(unsigned int *monitor, const GUID *PowerSettingGuid){
...
notifyParams[1] = (__int64)monitor;
notifyParams[0] = (__int64)CrmStateMonitorPowerSettingsCallback
hrRegister = PowerSettingRegisterNotification(PowerSettingGuid, 2u, notifyParams, &hNotification);
...
}
To handle each object request, the CrmStateMonitorPowerSettingsStart function registers the object’s GUID value via PowerSettingRegisterNotification to subscribe to Power Setting change notifications from the kernel.
[PsmServiceExtHost.dll]
int __fastcall CrmStateMonitorStateSet(CRM_STATE_MONITOR *monitor, const void *Data, size_t DataLength, int bLockHeld){
...
memcpy_0(monitor->StatePtr, Data, DataLength);
...
}
CRM uses a StateBuffer to store the state of each object. The CrmStateMonitorStateSet function inside the callback copies user data into the StateBuffer via memcpy without validating the length. This allows an attacker to trigger an overflow.
Trigger
opnum34 CreatePossibleSetting opnum6 accessor=4 write <idx>\SettingValue = payload opnum6 accessor=7 write active-scheme ACSettingIndex = idx opnum6 accessor=8 write active-scheme DCSettingIndex = idx opnum9 Trigger ApplyPowerSetting(subgroup, setting) to opnum35 cleanup subgroup/setting
The vulnerability is triggered by using RPC opnum 34 and 6 to set up the payload followed by opnum 9 to trigger it and opnum 35 to perform cleanup.
When writing registry keys via UmpoRpcWriteToSystemPowerKey (opnum 6) the registry path varies depending on the accessor value. To trigger the vulnerability the overflow payload must be placed through accessor 4 and the index value pointing to that payload must be set as the value for the AC and DC Settings.
Once the Power Setting is applied via opnum 9, an ALPC request is sent to the kernel and the changed value is published. As a result the vulnerability is triggered in the CrmStateMonitorStateSet function of PsmService which had been subscribing to these notifications.
Role of the RPC
Since the callbacks registered by CRM cannot be invoked directly, the UMPO RPC loaded by the DcomLaunch svchost is used to write the payload and publish the Power Settings configuration. This triggers an ALPC request which in turn invokes the CRM callback. The role of each RPC is as follows.
- opnum34 – UmpoRpcCreatePossibleSetting
HKLM\SYSTEM\CurrentControlSet\Control\Power\PowerSettings<subgroup><setting><idx>registry key creation- Prepare the registry location to store the payload.
- opnum6 – UmpoRpcWriteToSystemPowerKey
- Accessor 4
HKLM\SYSTEM\CurrentControlSet\Control\Power\PowerSettings<subgroup><setting><idx>\SettingValueregistry value creation- Write the exploit payload to the SettingValue as REG_BINARY.
- The payload and its length are subsequently passed to the vulnerable function.
- Accessor 7
{setting}\DefaultPowerSchemeValues{scheme}\ACSettingIndex- Configure the payload key to be used under the AC power state (7).
- Accessor 8
{setting}\DefaultPowerSchemeValues{scheme}\DCSettingIndex- Configure the payload key to be used under the DC power state (8).
- opnum9 – UmpoRpcApplyPowerSetting
- Read
<idx>\SettingValuebased on the AC/DC SettingIndex configured in opnum 6 and publish the power setting event. - The event is sent to the kernel via ALPC and is subsequently consumed by the vulnerable function
CrmStateMonitorStateSet.
- opnum35 – UmpoRpcRemoveSetting
- Delete the
<subgroup>\<setting>subtree used during the exploit (cleanup). - Used to ensure exploit stability.
// An example of storing payload on registry using opnum6 RPC call.
...
put_u32(payload, size, off + 0x00, type);
put_u64(payload, size, off + 0x08, 0);
put_u64(payload, size, off + 0x10, 0);
put_u64(payload, size, off + 0x18, list_entry);
put_u64(payload, size, off + 0x20, list_entry);
put_u64(payload, size, off + 0x28, stateptr);
...
call_opnum6(h,
NULL, // scheme
&g_subgroup, // subgroup
NULL, // overlay
setting, // setting
4, // accessor
REG_BINARY, // type
9000, // !!!!!! PAYLOAD INDEX!!!!!
payload, // OUR PAYLOAD HERE
size); // OUR PAYLOAD LEN
Exploit
Overview
- Stage A
- The overflow overwrites the adjacent target monitor object and redirects the StatePtr to the user32_scratch region.
- Stage B
- Since target->StatePtr points to user32_scratch the Stage B payload is written into the user32.dll scratch region.
- The function then traverses the listener list following target->ListHead and invokes the callback.
- When the callback is invoked it is redirected to the address tampered by Stage B resulting in a call to
_ClientLoadLibrarylocated at user32_scratch+0x00.
Details
LFH Bucket Layout [acdc][global]...[battery][display]...[powersave]..[lowepoch] ------->overflow
In the Windows LFH there are no inline headers between chunks within the same bucket. This makes it possible to use the overflow to manipulate the data of the next chunk and consequently corrupt the adjacent CRM object.
[Overflowed CRM Object] +0x00: Type = CRM_Type // bruteforce +0x08: Lock = 0 +0x18: Flink = scrach+0x08 +0x20: Blink = scrach+0x08 +0x28: StatePtr = scratch
The CRM object was constructed as shown above when overwriting the next chunk via the overflow. Since DcomLaunch svchost.exe uses the same library addresses as Medium IL normal processes the address of user32.dll can be predicted regardless of ASLR. Leveraging this, the writable region (scratch) within user32.dll was used as a referenceable address.
[PsmServiceExtHost.dll]
int __fastcall CrmStateMonitorStateSet(
CRM_STATE_MONITOR *monitor,
const void *Data,
unsigned __int64 DataLength,
int bLockHeld)
{
...
for ( listEntry = monitor->ListenerList_Flink;
listEntry != &monitor->ListenerList_Flink;
listEntry = *&listEntry->Type )
(*((char*)listEntry - 8))((char*)listEntry - 8, monitor->Type, Data); // CALL
if ( !bLockHeld ){
monitor->OwnerThreadId = 0;
RtlReleaseSRWLockExclusive(&monitor->SRWLock);
}
return CrmLogStateMonitorStateSet(monitor, logBuffer);
}
[scratch in user32.dll] +0x00: callback = _ClientLoadLibrary // CFG Bypass +0x08: Flink = scratch+0x8 +0x10: Blink = scratch+0x8 +0x30: scratch+0x68 // For LoadLibraryExW +0x68: L"\p\s" // DLL Path(Wide String)
When the overflowed CRM object is invoked it locates the Flink and directly calls the callback function at (char*)listEntry – 8. This call site was leveraged to invoke an arbitrary function.
The callback call site is protected by CFG. To bypass this, the scratch region was constructed to utilize the _ClientLoadLibrary function already present in user32.dll which in turn calls LoadLibraryExW to load an arbitrary DLL.
[user32.dll]
NTSTATUS __fastcall _ClientLoadLibrary(char *a1)
{
...
int v10;
v10 = 0x40000;
Library = LoadLibraryExW(a+0x30, 0LL, v10 != 0x40001 ? 8 : 0);
Ultimately, it was possible to load a DLL from an attacker-specified path via _ClientLoadLibrary.
Due to randomization, the allocation order within LFH buckets is unpredictable and exact heap layout prediction is impossible. To address this, we create Source/Target CRM sets so that the exploit succeeds when adjacent CRM objects are placed next to each other.
Upon successful exploitation, the DLL is loaded within the DcomLaunch service process and a cmd.exe shell running under NT Authority\System privileges is spawned in Session 1.
Post Exploitation
Even after a successful vulnerability trigger, the svchost.exe process may crash due to heap corruption. Furthermore, the target process is classified as a Critical Process, which means a crash can lead to a BSOD. Therefore, the DLL loaded immediately upon successful exploitation repairs the corrupted heap structure and unregisters the process as a Critical Process to prevent a BSOD.
Disable Critical Process
- NtSetInformationProcess(ProcessBreakOnTermination, 0)
- RtlSetProcessIsCritical(FALSE, …)
To eliminate the BSOD risk, we disabled the process as Critical Process right after getting elevation of privilege to the SYSTEM IL
Restore Heap Memory (Self-heal)
After Stage A/B, the target monitor may remain in a corrupted state where StatePtr and ListHead point to user32_scratch. So, we also recovered the heap layout in this process.
- Traverse the PSM monitor objects based on the global monitor table in
psmserviceexthost.dll. - For each monitor, check for the pattern where
ListHead.Flink == scratch+0x08,ListHead.Blink == scratch+0x08, andStatePtr == scratch. If matched, the monitor is identified as corrupted by the exploit and itsListHeadis restored to point to its own address. - Restore the monitor’s
StatePtrto the original inline state buffer atmonitor+0x38and recover the corrupted monitor type based on the table index.