Skip to content

ARM64: Support for PAC-RET in .NET10 #109457

@a74nh

Description

@a74nh

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:

CoreCLR PAC feature OS system libraries Should be tested in CI
build with branch protection not in hardware or OS with PAC Yes
build with branch protection enabled in hardware + OS with PAC Yes
build with branch protection enabled in hardware + OS without PAC No
build without branch protection either either No

Assumptions

  • For now, only support Linux and Windows

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.

  • Verify on compatibility
    Verify this feature works on machines that do not have PAC like client devices, Ampere machines, etc. An option would be turn off the feature and hence do not emit PAC instructions if they are not available or emit them (assuming they turn into NOP on devices that do not support it).

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

  • Support Windows
  • Support MacOS, once Arm64e is fully supported in MacOS.
  • Add BTI support
  • Encrypt more data pointers. Possibly all pointers that point to jitted code.
  • Support Arm64 GCS. This is similar to Intel's CET (see Epic: Support Intel CET #47309). GCS is not yet available in hardware.

Metadata

Metadata

Assignees

Labels

User StoryA single user-facing feature. Can be grouped under an epic.arch-arm64area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions