ARM64: Support for PAC-RET in .NET10 #109457
Labels
arch-arm64
area-CodeGen-coreclr
CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
User Story
A single user-facing feature. Can be grouped under an epic.
Milestone
PAC-RET is a way of preventing ROP attacks on Arm64 using the PAC extension which was introduced in Arm 8.3. When enabled the stack pointer is encrypted before being stored to the stack and verified again when it is restored.
A detailed description of PAC-RET and the associated security issues can be found in Low-Level Software Security for Compiler Developers
Expand for another description....
The assumption here is that the attacker has gotten the ability the change writable memory in the process (possibly only the stack) and read executable memory. They do not have the ability to change readonly memory, change the control flow or change/access register contents. The goal of the attacker is to make the program execute arbitrary code. This can be done by editing the return addresses on the stack. When the program returns, it now jumps back to code the attacker wants to run. This in itself is not that useful as the attacker is limited to functions available and register contents. By looking through all executable memory they look for small groups of instructions directly proceeding a return instruction. These are "gadgets" which simply change a register or write a bit of memory. By chaining gadgets together using return addresses the attacker now can execute whatever they want. Tools exist to look at the executable code of a known program (or library) and build a library of gadgets (which is why I'm concerned about protection of CoreCLR code over jitted code).
PAC-RET works because the return address stays in a register LR (which the attacker cannot access) and only goes to memory when saved to the stack, which is encrypted before the store. When loading from the stack we unencrypt, and fault on an error. To modify the address, the hacker would need to know the secret per-process key. The hacker can't simply replace it with a different encrypted value as the location on the stack is used as a salt in the encryption, meaning every encrypted value is pinned to that location.
PAC-RET is self contained by function. When a function encrypts the return address, it will be the same function that decrypts it again before returning. Therefore, for standard programs, PAC can be enabled per function without interfering with other functionality. Issues arise when a program walks its own stack, rewrites it's stack, or jumps out of program order.
When run on systems without PAC, the PAC instructions are treated as NOPs. Therefore a PAC protected program can be run on a non-PAC system at a cost of a few NOPs per function.
Testing
There are a number of different scenarios that could be tested. To reduce testing size, only a few are required:
Assumptions
Work items
Add PAC supported Linux hardware in the CI
Using the scenarios above. This will likely be Cobalt 100. No other PAC work can be merged until this step is complete.
Build .NET using branch-protection flags Enable for PAC while compiling coreclr (not the jitted code) #108561
This will ensure that the entire CoreCLR VM in protected via PAC. This will always be enabled for Linux builds. The expected cost is 1-2% slowdown in the VM and jit on PAC enabled machines. This code is static and is the most vulnerable to ROP attacks as an attacker will be able to use the code to build an a library of attack gadgets ahead of time. Building with branch-protection will prevent this
Protect assembly routines:
This is only required if there are assembly routines in CoreCLR which save the return value to the stack. These should all be updated to encrypt/decrypt when saving/loading to/from the stack. Each routine could be implemented individually.
Fix up stack underwinders.
CoreCLR contains two libunwinds. It may have other stack examiners. These should be updated to strip PAC from the return address (there is no requirement here to decrypt the value). The underwinders need to be able to handle both encrypted and unencrypted values.
Add PAC-RET support to the jit
Once CoreCLR is protected, the next step is to protect code generated by CoreCLR. Enable via a config value. Ensure return values are encrypted/decrypted in prolog/epilog. Fix up any rewriting of the stack - for example return address hijacking in the GC. Suggested implementation order: 1) Encrypt the return address with a salt of 0 using the same key used by C++ code, decrypt by stripping - testing this will ensure all stack examiners/editors are found 2) decrypt fully 3) encrypt using the stack address as the salt. 4) Use a different key to C++.
Debugging and Diagnostics
Along with stack unwinding, we need to ensure that debugger can decrypt the function addresses (if needed) and display the debug info correctly.
NativeAOT / R2R
Lastly, we definitely should validate the working of PAC with NativeAOT and R2R to guarantee that it will work as expected.
Stretch items
Harden the config variable.
An attacker could potentially overwrite the config variable and disable PAC. Either always enable PAC if the hardware supports it or ensure the config variables moved to read-only memory after startup.
Harden hijacking stub addresses
The addresses used in the return address hijacking should be kept in memory as encrypted values. When rewriting the stack, the value is loaded from memory to register, decrypted, encrypted again, then stored to the stack.
Possible future work items
The text was updated successfully, but these errors were encountered: