Windows 10 made a lot of improvements in Kernel Address Space Layout Randomization (KASLR) that increases the cost of exploitation, particularly for remote code execution exploits. Many kernel virtual address space (VAS) locations including kernel stacks, pools, system PTEs etc. are randomized. A well-known exception to this is the KUSER_SHARED_DATA structure which is a page of memory that has always been traditionally mapped at a fixed virtual address in the kernel. This post highlights the work we have done towards strengthening KASLR in Windows by mitigating the last remaining blind-write target structure, to our knowledge, in the kernel that remote code execution exploits can utilize.
None of this work would have been possible without the close partnership between teams from across the Windows organization and MSRC.
The KUSER_SHARED_DATA structure is a single page (4096 bytes) in memory that is mapped at a fixed, hardcoded address in both kernel and user side of VAS. KUSER_SHARED_DATA is mapped into every process and provides a quick mechanism to obtain frequently needed global data (interrupt time, version, debugger state, processor extensions, etc.) from the kernel without involving user-kernel mode switching using system calls or interrupts. On 64-bit Windows the kernel-mode virtual address of the page is 0xFFFFF78000000000 and it is mapped with both Read and Write permissions. The user-mode mapping is at address 0x7FFE0000, and the page permissions are read-only. NT Kernel and associated drivers dynamically write to the various fields in the KUSER_SHARED_DATA structure using the kernel-mode mapping during and after boot and user-mode code can read these values using the user-mode mapping.
Even though the KUSER_SHARED_DATA structure doesn’t contain any pointers (thanks to previous work on this), the fact that it is mapped at a hardcoded virtual address has been useful for remote, kernel-mode exploitation. When a remote attacker gains a ‘write’ primitive by triggering a vulnerability in the kernel, KASLR is a barrier, and they must figure out where exactly to write to. A blind-write primitive such as this usually forces attackers to exploit an additional vulnerability that can leak information to disclose the memory address of a kernel-mode structure, preferably the contents of which can be controlled by the attacker remotely. But as a way around having to use another bug in an exploit chain, attackers can instead use the same blind-write primitive and write some specially crafted data structures inside KUSER_SHARED_DATA. With this, they can use the address of this data (fixed across reboots) as a pointer to another code path in the kernel. This can, in certain situations, enable attackers to extend the initial blind-write primitive they obtained (by triggering the vulnerability) to also read arbitrary regions of the kernel VAS resulting in a KASLR compromise. An article from Ricerca Security details such a technique by writing a payload (forged MDL structures) inside KUSER_SHARED_DATA and providing the address of this payload to a specific code path which, expecting legitimate MDL structure pointers, reads from arbitrary memory as described by the payload. This allows extending the blind-write primitive to an arbitrary memory read primitive and was used in the process of a proof-of-concept exploit for the Remote Code Execution vulnerability that was patched as CVE-2020-07096
KUSER_SHARED_DATA is a backwards compatible structure that has existed since the very early versions of Windows. The fields within the page have remained at fixed offsets and there have been a couple of additional fields added to it over the years. As such a lot of third-party kernel-mode code running on Windows relies on reading from the fixed address of this page. How do we randomize this without breaking this assumption of the static nature of the shared page?
To ensure compatibility, we decided to keep the page at the current hardcoded virtual address (0xFFFFF78000000000), change the page protections for this mapping to READ-ONLY, and create a new virtual mapping of the same underlying physical page at a randomized address. This new randomized mapping is mapped with Read and Write protections. Essentially, any writes to the actual page would have to go through this new randomized virtual address and the old, fixed address can only be read from but not written into.
This design ensures that we don’t break the existing usage of the address but mitigates the technique noted before where remote kernel-mode exploits can utilize the writeable nature of the fixed virtual address to get around KASLR.
The following figures illustrate the change-
Figure 1: Before the Mitigation
Figure 2: After the Mitigation
Let’s take a brief look at how this randomization and page protection is implemented in Windows.
As seen from Figure 2 above, the design necessitates modifying all existing code in Windows that relies on the old design to be compatible. This involved changing all the current locations across Windows kernel-mode code that write to the KUSER_SHARED_DATA structure to use the new randomized virtual address.
There is a new global variable MmWriteableSharedUserData that stores the randomized address of the new mapping. This is not exported and only visible to code inside the NT kernel. The location of this global variable is randomized on account of it being inside the NT kernel which is itself randomized.
Early on during boot before “Phase 0” initialization, this new global variable holds the address of the fixed(old) KUSER_SHARED_DATA structure. Once the OS is in Phase 0 initialization (initializing memory management paging, system page table entries, etc.) the function that handles protecting the KUSER_SHARED_DATA mapping is MiProtectSharedUserPage. This function previously only modified the existing KUSER_SHARED_DATA page permissions to make it non-executable. MiProtectSharedUserPage now performs the following actions (not in the same order as listed, the flow has been changed for clarity)-
- Modify the page permissions of the existing, static mapping of KUSER_SHARED_DATA page to make it Read-Only (no Write or Execute permissions). When the page is first mapped early in boot, it has RWX permissions.
- Create a new randomized mapping of the underlying KUSER_SHARED_DATA physical page. Set the permissions of this mapping to READWRITE
- The virtual address of the new randomized mapping is written inside the NTOS-only global variable MmWriteableSharedUserData
This mitigation breaks the ability of remote attackers to use the KUSER_SHARED_DATA structure to exploit blind-write primitives since the writeable address is now randomized and the static address is read-only.
The KUSER_SHARED_DATA structure is meant to be updated only by the core OS and internal drivers. External, third-party drivers should not be relying on writing or updating this structure. We have been working with external vendors and driver developers who are testing their code on Windows Insiders builds to ensure their products remain compatible. An incompatible third-party driver that relies on writing to the current, static address of KUSER_SHARED_DATA will cause a system crash. If you develop drivers for Windows, please make sure to test against current Windows Insider Preview builds and update any legacy code to remain compatible with the latest OS!
This feature is enabled on 64-bit Windows only (AMD64 and ARM64) and you can test it currently with a Windows Insider Program build (kudos to the external security researchers who have already started noticing these changes!). We intend to include this mitigation in a future release of Windows.