Tech News
← Back to articles

Windows ARM64 Internals: Deconstructing Pointer Authentication

read original related products more articles

Pointer Authentication Code, or PAC, is an anti-exploit/memory-corruption feature that signs pointers so their use (as code or data) can be validated at runtime. PAC is available on Armv8.3-A and Armv9.0-A (and later) ARM architectures and leverages virtual addressing in order to store a small cryptographic signature alongside the pointer value.

On a typical 64-bit processor a pointer is considered a "user-mode" pointer if bit 47 of a 64-bit address is set to 0 (meaning, then, bits 48-63 are also 0). This is known as a canonical user-mode address. If bit 47 is set to 1, bits 48-63 are also set to 1, with this being considered a canonical kernel-mode address. Additionally, LA57, ARM 52 or 56 bit, or similar processors extend the most significant bit out even further (and PAC can also be enabled in the ARM-specific scenarios). For our purposes, however, we will be looking at a typical 64-bit processor with the most significant bit being bit 47.

It has always been an "accepted" standard that the setting of the most significant bit denotes a user-mode or kernel-mode address – with even some hardware vendors, like Intel, formalizing this architecture in actual hardware with CPU features like Linear Address Space Separation (LASS). This means that bits 48-63 are unused on a current, standard 64-bit processor, as the OS typically ignores them. Because they are unused, this allows PAC to store the aforementioned signature in these unused bits alongside the pointer itself.

As mentioned, these “unused” bits are now used to store signing information about a particular pointer in order to validate and verify execution and/or data access to the target memory address. Special CPU instructions are used to both generate and validate cryptographic signatures associated with a particular pointer value. This blog post will examine the Windows implementation of PAC on ARM64 installations of Windows, which, as we will see, supports a very specific implementation of PAC in both user-mode and kernel-mode.

PAC Enablement on Windows

PAC enablement on Windows begins at the entry point of ntoskrnl.exe, KiSystemStartup. KiSystemStartup is responsible for determining if PAC is supported on Windows and also for initializing basic PAC support. KiSystemStartup receives the loader parameter block (LOADER_PARAMETER_BLOCK) from winload.efi, the Windows boot loader. The loader block denotes if PAC is supported. Specifically, the loader parameter block extension (LOADER_PARAMETER_EXTENSION) portion of the loader block defines a bitmask of various features which are present/supported, so say the boot loader. The PointerAuthKernelIpEnabled bit of this bitmask denotes if PAC is supported. If PAC is supported, the loader parameter block extension is also responsible for providing the initial PAC signing key (PointerAuthKernelIpKey) used to sign and authenticate all kernel-mode pointers (we will see later that the "current" signing key is updated many times). When execution is occurring in kernel-mode, this is the key used to sign kernel-mode pointers. The bootloader generates the key in OslPrepareTarget by calling the function SymCryptRngAesGenerate to generate the initial kernel pointer signing key passed via the loader parameter block.

The ARM architecture supports having multiple signing keys for different scenarios, like signing instruction pointers or data pointers with different keys. Typically, "key A" and "key B" (as they are referred to), which are stored in specific system registers, are used for signing pointers used in instruction executions (like return addresses). Windows currently only uses PAC for "instruction pointers" (more on this later) and it also it only uses "key B" for cryptographic signatures and, therefore, loads the target pointer signing value into the APIBKeyLo_EL1 and APIBKeyHi_EL AArch64 system registers. These "key registers" are specific system registers, which are special registers on ARM systems which control various behaviors/controls/statuses for the system, and are responsible for maintaining the current keys used for signing and authenticating pointers. These two registers ("lo" and hi") each hold a single 64-bit value, which results in a concatenated 128-bit key. EL1, in this case, refers to exception level “1” - which denotes the ARM-equivalent of “privilege level” the CPU is running in (as ARM-based CPUs are “exception-oritented”, meaning system calls, interrupts, etc. are all treated as “exceptions”). Typically EL1 is associated with kernel-mode. User-mode and kernel-mode, for Windows, share EL1’s signing key register (although the "current" signing key in the register changes depending on if a processor is executing in kernel-mode or user-mode). It should be noted that although the signing key for user-mode is stored in an EL1 register, the register itself (e.g., reading/writing) is inaccessible from user-mode (EL 0).

It is possible to examine the current signing key values using WinDbg. Although WinDbg, on ARM systems, has no extension to read from these system registers, it was discovered through trial-and-error that it is possible to leverage the rdmsr command in WinDbg to read from ARM system registers using the encoding values provided by the ARM documentation. The two PAC key system registers used by Windows have the following encodings:

1. APIBKeyLo_EL1

... continue reading