Hunting for overlooked cookies in Windows 11 KTM
and baking exploits for them
OffensiveCon 2025
Why Should You Care?
My Promise
Why Should You Care?
My Promise
Kernel Transaction Manager (KTM)?
By Developers
Source: NT
Kernel Transaction Manager (KTM)?
Source: OffensiveCon2020
Source: OffensiveCon2020
By Attackers
Vista/ KTM
2007
2010
2015
2017
2018
2023
Proton Bot malware
2019
UAF x2
2024
Q4
2024
Q2
Be Curious
Be Curious
Be Curious
Do Not Find Bugs, Bugs Find You
Just Play Around
Ten Ideas
Ten Failed Ideas
1. Could there be a Double Free if I raced NtPropagationFailedExt with TmpDispatchPropagateRequest?
2. Could there be a Use after Free if I raced NtPropagationFailedExt in 2 threads?
3. Could I overflow the Propagation Request Refcount? (partially)
4. What happens when I mix COM+ Transactions with KTM APIs?
5. What happens when I race NtPropagationCompleteExt with TmpDispatchPropagateRequest?
6. What happens when I race TmpPromotionRequestTail with itself?
7. Is there uninitialized data in TmpProbeAndCaptureBuffer?
8. Could I get a Use After Free when racing NtPropagationFailedExt in 2 threads?
9. Could I get a Use After Free when racing NtPropagationFailedExt with NtPropagationCompleteExt, NtPropagationCompleteExt and NtPropagationCompleteExt?
10.Could the Transaction->PromotePropagation become a stale pointer in certain situations?
Failure Is Inevitable
But That’s Okay
Exercise Regularly
Protocol Creation
Resource Manager
Protocol
NtRegisterProtocolAddressInformationExt()
Cookie = 0xb00b0005
Protocol Creation
Resource Manager
ProtocolListHead
Cookie = 0xb00b0005
Protocol
Protocol
Protocol
ResourceManager
ProtocolGUID
Promoting Protocol
Protocol
ResourceManager
ProtocolGUID = { 0xac06cc84 , 0x1465, 0x428b, � { 0xa3, 0x98, 0x0a, 0xae, 0xef, 0xb4, 0x59, 0x9b} };
PropagateRequest Creation
Transaction
Propagate Request
TmpAllocatePropagateRequest()
Cookie = 0xb00b0006
PropagateRequest
Propagate
Request
ResourceManager = NULL
Dispatch PropagateRequest
Propagate
Request
TmpDispatchPropagateRequest()
Dispatch PropagateRequest
void TmpDispatchPropagateRequest(_KPROPAGATEREQUEST *PropagateRequest)
{
// 1. Find Promoting Protocol
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* PromotingProtocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
...
Dispatch PropagateRequest
void TmpDispatchPropagateRequest(_KPROPAGATEREQUEST *PropagateRequest)
{
// 1. Find Promoting Protocol
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* PromotingProtocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
...
// 2. Set Propagate Request ResourceManager to Promoting Protocol's ResourceManager
PropagateRequest->ResourceManager = PromotingProtocol->ResourceManager;
ObfReferenceObject(PromotingProtocol->ResourceManager);
...
Dispatch PropagateRequest - Vulnerability
void TmpDispatchPropagateRequest(_KPROPAGATEREQUEST *PropagateRequest)
{
// 1. Find Promoting Protocol
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* PromotingProtocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);� /*
*
* ... We can free the Promoting Protocol in this window!
*
*/
// 2. Set Propagate Request ResourceManager to Promoting Protocol's ResourceManager
PropagateRequest->ResourceManager = PromotingProtocol->ResourceManager;
ObfReferenceObject(PromotingProtocol->ResourceManager);
...
Protocol Deletion
void TmpCloseResourceManager(_KRESOURCEMANAGER *ResourceManager)
{
...
// Lock global mutex and free Protocol
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
TmpDeleteProtocol(get_entry_from_list(ResourceManager->ProtocolListHead));
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
...
CloseProcedure of ResourceManager object
Called when calling CloseHandle(hRM)
Reclaim freed Protocol with Named Pipe ⇒ poison ResourceManager field
Dispatch PropagateRequest - Vulnerability
void TmpDispatchPropagateRequest(_KPROPAGATEREQUEST *PropagateRequest)
{
// 1. Find Promoting Protocol
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* PromotingProtocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
// ... Free the Promoting Protocol in this window and reclaim with Named Pipe data
// 2. We control PromotingProtocol->ResourceManager
PropagateRequest->ResourceManager = PromotingProtocol->ResourceManager;
ObfReferenceObject(PromotingProtocol->ResourceManager);
...
// 3. Use Poisoned ResourceManager
KeWaitForSingleObject(PromotingProtocol->ResourceManager);
add_to_linked_list(&PromotingProtocol->ResourceManager->PendingPropReqListHead, PropagateRequest);
KeReleaseMutex(PromotingProtocol->ResourceManager);
...
TmpSetNotificationResourceManager(PropagateRequest->ResourceManager)
...
Constraints
Patch 1: Emulate Clustered Disk
void TmpIsClusteredTransactionManager(_KTM *pKTM, char *pIsDiskClustered)
{
...
// 1. Send IOCTL_DISK_IS_CLUSTERED to device driver
Irp = IoBuildDeviceIoControlRequest(IOCTL_DISK_IS_CLUSTERED,...,&bIsDiskClustered,...);
Status = IofCallDriver(AttachedDeviceReference, Irp);
...
// 2. Save result for caller
*pIsDiskClustered = bIsDiskClustered;
}
void TmpIsClusteredTransactionManager(_KTM *pKTM, char *pIsDiskClustered)
{
...
// 1. Send IOCTL_DISK_IS_CLUSTERED to device driver
Irp = IoBuildDeviceIoControlRequest(IOCTL_DISK_IS_CLUSTERED,...,&bIsDiskClustered,...);
Status = IofCallDriver(AttachedDeviceReference, Irp);
...
// 2. Save result for caller
*pIsDiskClustered = 1; // PATCH: We are a clustered disk!
}
Constraints
KtmRm for Distributed Transaction Coordinator SID:��S-1-5-80-2818357584-3387065753- 4000393942-342927828-138088443
Patch 2: Emulate KtmRm Security Descriptor
int NtRegisterProtocolAddressInformationExt(..., GUID* ProtocolId, ...)
{
...
// 1. If trying to register promoting protocol
if (RtlCompareMemory(ProtocolId, &TmpPromotingProtocolId, 0x10) == 0x10)
{
// 2. Check user has KtmRm Security Descriptor
SeCaptureSubjectContext(&SubjectContext);
bAllowed = SeAccessCheck(TmpKtmRmDescriptor,&SubjectContext,...,);
SeReleaseSubjectContext(&SubjectContext);
}
...
// 3. Exit if not the right permissions
if (!bAllowed)
return 0;
TmRegisterProtocolAddressInformation(ResourceManager, ProtocolId, ...);
...
int NtRegisterProtocolAddressInformationExt(..., GUID* ProtocolId, ...)
{
...
// 1. If trying to register promoting protocol
if (RtlCompareMemory(ProtocolId, &TmpPromotingProtocolId, 0x10) == 0x10)
{
// 2. Check user has KtmRm Security Descriptor
SeCaptureSubjectContext(&SubjectContext);
bAllowed = 1; // PATCH: We are KtmRm user
SeReleaseSubjectContext(&SubjectContext);
}
...
// 3. Exit if not the right permissions
if (!bAllowed)
return 0;
TmRegisterProtocolAddressInformation(ResourceManager, ProtocolId, ...);
...
Winning the Race?
void TmpDispatchPropagateRequest(_KPROPAGATIONREQUEST *PropagateRequest)
{
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* Protocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
PropagateRequest->ResourceManager = Protocol->ResourceManager;
ObfReferenceObject(Protocol->ResourceManager);
KeWaitForSingleObject(&Protocol->ResourceManager->Mutex);
add_to_linked_list(&Protocol->ResourceManager->PendingPropReqListHead, PropagateRequest);
KeReleaseMutex(&Protocol->ResourceManager->Mutex);
…
TmpSetNotificationResourceManager(PropagateRequest->ResourceManager)
Protocol controlled HERE
Protocol controlled HERE
Protocol controlled HERE
Protocol controlled HERE
Medium IL
Winning the Race?
void TmpDispatchPropagateRequest(_KPROPAGATIONREQUEST *PropagateRequest)
{
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* Protocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
PropagateRequest->ResourceManager = Protocol->ResourceManager;
ObfReferenceObject(Protocol->ResourceManager);
KeWaitForSingleObject(&Protocol->ResourceManager->Mutex);
add_to_linked_list(&Protocol->ResourceManager->PendingPropReqListHead, PropagateRequest);
KeReleaseMutex(&Protocol->ResourceManager->Mutex);
…
TmpSetNotificationResourceManager(PropagateRequest->ResourceManager)
Protocol controlled HERE
PropagateRequest and Protocol have same ResourceManager
Need valid ResourceManager→Tm
No Protocol abuse possible :(
BSOD
Winning the Race?
void TmpDispatchPropagateRequest(_KPROPAGATIONREQUEST *PropagateRequest)
{
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* Protocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
PropagateRequest->ResourceManager = Protocol->ResourceManager;
ObfReferenceObject(Protocol->ResourceManager);
KeWaitForSingleObject(&Protocol->ResourceManager->Mutex);
add_to_linked_list(&Protocol->ResourceManager->PendingPropReqListHead, PropagateRequest);
KeReleaseMutex(&Protocol->ResourceManager->Mutex);
TmpSetNotificationResourceManager(PropagateRequest->ResourceManager)
Need valid Mutant→CurrentThread.
Medium IL: NtQuerySystemInformation
No BSOD
Arbitrary increment or 0 overwrite.
Same as CVE-2018-8611
Protocol controlled HERE
Block indefinitely
Protocol controlled HERE
Pass fake Mutant into KeReleaseMutex
Winning the Race?
void TmpDispatchPropagateRequest(_KPROPAGATIONREQUEST *PropagateRequest)
{
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* Protocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
PropagateRequest->ResourceManager = Protocol->ResourceManager;
ObfReferenceObject(Protocol->ResourceManager);
KeWaitForSingleObject(&Protocol->ResourceManager->Mutex);
add_to_linked_list(&Protocol->ResourceManager->PendingPropReqListHead, PropagateRequest);
KeReleaseMutex(&Protocol->ResourceManager->Mutex);
TmpSetNotificationResourceManager(PropagateRequest->ResourceManager)
No BSOD
PropagateRequest & Protocol have different ResourceManager
Protocol controlled HERE
Winning the Race, in Practice.
void TmpDispatchPropagateRequest(_KPROPAGATIONREQUEST *PropagateRequest)
{
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* Protocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
PropagateRequest->ResourceManager = Protocol->ResourceManager;
ObfReferenceObject(Protocol->ResourceManager);
KeWaitForSingleObject(&Protocol->ResourceManager->Mutex);
add_to_linked_list(&Protocol->ResourceManager->PendingPropReqListHead, PropagateRequest);
KeReleaseMutex(&Protocol->ResourceManager->Mutex);
TmpSetNotificationResourceManager(PropagateRequest->ResourceManager)
BSOD avoided
ETHREAD previously leaked. �Write primitive.�Block indefinitely.
PropagateRequest and Protocol have same ResourceManager
Protocol controlled HERE
Vulnerability Exploitation (Medium IL)
Propagate
Request
Resource Manager
Exploit Thread
Freeing Thread
Protocol
1. Creation
2. Leak kernel @
Exploit Thread
3bis. Close
3. Trigger Promotion
user
kernel
Delete
ntoskrnl.exe
Vulnerability Exploitation (Medium IL)
Fake kernel objects
Exploit Thread
user
kernel
@ExploitThread
@ntoskrnl.exe
5bis. Arbitrary increment primitive
Spraying Thread
4. Spray NamedPipe
5. Arbitrary 0 overwrite primitive
NamedPipeFake Protocol
Propagate
Request
KeReleaseMutex(&Protocol->ResourceManager->Mutex)
3. Trigger Promotion
Vulnerability Exploitation (Medium IL)
7. Block indefinitely
Exploit Thread
user
kernel
ntoskrnl.exe
6. PreviousMode = 0
6bis. SeDebugPrivilege += 3
Exploit Thread
@ExploitThread
@ntoskrnl.exe
5bis. Arbitrary increment primitive
5. Arbitrary 0 overwrite primitive
Propagate
Request
3. Trigger Promotion
KeReleaseMutex(&Protocol->ResourceManager->Mutex)
Other Thread
@OtherThread
@ntoskrnl.exe
Set up 4 Windows Server VMs�Find a 0day in KtmRm Service
Use 2 Windbg Commands
Demo
Propagate Request Low IL exploit?
Propagate Request Low IL exploit?
void TmpDispatchPropagateRequest(_KPROPAGATIONREQUEST *PropagateRequest)
{
KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* Protocol = find_promoting_protocol();
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
PropagateRequest->ResourceManager = Protocol->ResourceManager;
ObfReferenceObject(Protocol->ResourceManager);
KeWaitForSingleObject(&Protocol->ResourceManager->Mutex);
add_to_linked_list(&Protocol->ResourceManager->PendingPropReqListHead, PropagateRequest);
KeReleaseMutex(&Protocol->ResourceManager->Mutex);
TmpSetNotificationResourceManager(PropagateRequest->ResourceManager)
And still need another 0day in KtmRm service …
Protocol controlled HERE
Protocol controlled HERE
Fake ResourceManager in userland
Leak ETHREAD.
And temporarily block
Primitive ⇒ arbitrary 0 overwrite.
Can’t just block here
Need valid ResourceManager→Tm
TmpDispatchPropagateRequest Fix
void TmpDispatchPropagateRequest(_KPROPAGATIONREQUEST *PropagateRequest)
{� KeWaitForSingleObject(&TmpAllProtocolsListMutex, Executive, 0, 0, 0LL);
KPROTOCOL* Protocol = find_promoting_protocol();� ...
if ( TmEnableMSRC88939Fix ) {
// only fetch Protocol->ResourceManager once
ResourceManager = Protocol->ResourceManager;
ObfReferenceObject(ResourceManager);
// then release Protocol mutex
KeReleaseMutex(&TmpAllProtocolsListMutex, 0);
}
...
PropagateRequest->ResourceManager = ResourceManager;
KeWaitForSingleObject(&ResourceManager->Mutex);
Just Got Lucky?
Don’t Let Your Biases Hinder You
No one thought to look at these objects
→
Good! What else is being overlooked?
Don’t Let Your Biases Hinder You
Not a real vulnerability
→
Real benefits:
Don’t Let Your Biases Hinder You
Smarter people have probably audited the rest of tm.sys already
→
No penalties for trying
Different researchers find different vulnerabilities�
Most Code is Rarely Audited in Real Depth
e
Jael’s TmpMigrateEnlistments
Jael’s TmpMigrateEnlistments
Jael’s TmpMigrateEnlistments
TmpMigrateEnlistments
License To UAF
Migrate Enlistment
void TmpMigrateEnlistments(_KTRANSACTION *SourceTx, _KTRANSACTION *DestinationTx)
{
// 1. For each Enlistment in Source Transaction’s Enlistment list
currEnlistment= SourceTx->EnlistmentHead.Flink;
while ( currEnlistment!= &SourceTx->EnlistmentHead )
{
Migrate Enlistment
void TmpMigrateEnlistments(_KTRANSACTION *SourceTx, _KTRANSACTION *DestinationTx)
{
// 1. For each Enlistment in Source Transaction’s Enlistment list
currEnlistment= SourceTx->EnlistmentHead.Flink;
while ( currEnlistment!= &SourceTx->EnlistmentHead )
{
// 2. Update Enlistment to point to Destination Transaction
currEnlistment->Transaction = DestinationTx;
Migrate Enlistment
void TmpMigrateEnlistments(_KTRANSACTION *SourceTx, _KTRANSACTION *DestinationTx)
{
// 1. For each Enlistment in Source Transaction’s Enlistment list
currEnlistment= SourceTx->EnlistmentHead.Flink;
while ( currEnlistment!= &SourceTx->EnlistmentHead )
{
// 2. Update Enlistment to point to Destination Transaction
currEnlistment->Transaction = DestinationTx;
// 3. Remove Enlistment from Source Transaction’s Enlistment List
list_safely_remove(&currEnlistment->NextSameTx);�
Migrate Enlistment
void TmpMigrateEnlistments(_KTRANSACTION *SourceTx, _KTRANSACTION *DestinationTx)
{
// 1. For each Enlistment in Source Transaction’s Enlistment list
currEnlistment= SourceTx->EnlistmentHead.Flink;
while ( currEnlistment!= &SourceTx->EnlistmentHead )
{
// 2. Update Enlistment to point to Destination Transaction
currEnlistment->Transaction = DestinationTx;
// 3. Remove Enlistment from Source Transaction’s Enlistment List
list_safely_remove(&currEnlistment->NextSameTx);�
// 4. Add Enlistment to Destination Transaction’s Enlistment List
list_safely_add(&DestinationTx->EnlistmentHead, currEnlistment);
currEnlistment = currEnlistment->Flink;
Migrate Enlistment - Vulnerability
void TmpMigrateEnlistments(_KTRANSACTION *SourceTx, _KTRANSACTION *DestinationTx)
{
// 1. For each Enlistment in Source Transaction’s Enlistment list
currEnlistment= SourceTx->EnlistmentHead.Flink;
while ( currEnlistment!= &SourceTx->EnlistmentHead )
{
// 2. Update Enlistment to point to Destination Transaction
currEnlistment->Transaction = DestinationTx;�
/* We can free currEnlistment in this window so a fake Enlistment gets
* added to Destination Transaction’s Enlistment list!
*/
// 3. Remove Enlistment from Source Transaction’s Enlistment List
list_safely_remove(&currEnlistment->NextSameTx);�
// 4. Add Enlistment to Destination Transaction’s Enlistment List
list_safely_add(&DestinationTx->EnlistmentHead, currEnlistment);
currEnlistment = currEnlistment->Flink;
How to Free an Enlistment?
CommitComplete()
-> NtCommitComplete()
-> TmCommitComplete()
-> TmpProcessNotificationResponse()
-> TmpFinalizeEnlistment()
Traditionally done by committing a Prepared Enlistment
And releasing the Enlistment’s user handle with CloseHandle()
Why is there a Vulnerability?
void TmpMigrateEnlistments(_KTRANSACTION *SourceTx, _KTRANSACTION *DestinationTx)
{
currEnlistment= SourceTx->EnlistmentHead.Flink;
while ( currEnlistment!= &SourceTx->EnlistmentHead ) // [1]
{� // Destination Transaction’s mutex is NOT held
currEnlistment->Transaction = DestinationTx; // [2]� ...
void TmpProcessNotificationResponse(_KENLISTMENT *Enlistment, ...)
{
// Acquire Transaction’s Mutex before finalizing the Enlistment
Transaction = Enlistment->Transaction;
KeWaitForSingleObject(&Transaction->Mutex, Executive, 0, 0, 0LL);
...
We can call CommitComplete() in another thread during TmpMigrateEnlistments!
But….
Only Committed Transactions will free Enlistments.
We have to call CommitTransaction/CommitTransactionAsync on Destination Transaction before CommitComplete() works.
But… we don’t have the handle to Destination Transaction.
Traditional way of freeing Enlistment will NOT work in this case.
In our case, CommitComplete() does not free Enlistment.
Hmm…
Is this even a vulnerability?
Hmm…
Constraints
None…but?
Debugger Assistance to Win the Race
void TmpMigrateEnlistments(_KTRANSACTION *SourceTx, _KTRANSACTION *DestinationTx)
{
currEnlistment= SourceTx->EnlistmentHead.Flink;
while ( currEnlistment!= &SourceTx->EnlistmentHead ) // [1]
{
currEnlistment->Transaction = DestinationTx; // [2]
/*� * PATCH: Insert an infinite jump here to force a race win
*/� while (1) {}
list_safely_remove(&currEnlistment->NextSameTx); // [3] �
list_safely_add(&DestinationTx->EnlistmentHead, currEnlistment); // [4]
currEnlistment = currEnlistment->Flink;�
Demo with Debugger Assistance
Exploitation for TmpMigrateEnlistments?
KTM == Sandbox Escape?
Sandbox | KTM Accessible? (Before Feb 2021) | KTM Accessible? (Present) |
Chrome | ✅ | ❌ |
Firefox | ✅ | ✅ |
Edge | ✅ | ❌ |
Windows Component Filter Mitigation
if (component_filter_.ComponentFlags) {
if (!startup_info_.UpdateProcThreadAttribute(
PROC_THREAD_ATTRIBUTE_COMPONENT_FILTER, &component_filter_,
sizeof(component_filter_)) &&
::GetLastError() != ERROR_NOT_SUPPORTED) {
return false;
}
Blocked APIs
Blocked Allowed APIs
Is KTM Safe Now?
Ideas for the reader…
TxR
TxF
No public vulnerabilities?
Source: BHEU17
MSDTC.exe Service
OleTx
MSDTC Connection Manager: �OleTx Transaction Protocol
500-page specification from Microsoft ([MS-DTCO])
PreviousMode is Dead
Windows 11 24H2
Windows 11 23H2 (and below)
You CAN find vulnerabilities.
Questions?
Ask us a question and get an EXCLUSIVE autographed MTG card after the talk!
References
References (Jael)
References (Jael)
Addendum 1:
For the TmpDispatchPropagateRequest vulnerability: After TmpIsClusteredTransactionManager(), the “KtmRm for Distributed Transaction Manager” service will be started. This service will attempt to register a promoting protocol. �If the service successfully registers a promoting protocol, our exploit process will no longer be able to register a promoting protocol.�Since the exploit involves use-after-freeing this promoting protocol, an attacker would likely need to win the race on the first try BEFORE the service registers a promoting protocol or gain code execution within the service to close this promoting protocol handle and trigger the exploit.�In my demonstration, I disabled this service so I could retry the exploit repeatedly without fail.