341 lines
10 KiB
Markdown
341 lines
10 KiB
Markdown
|
# Pointer Authentication
|
||
|
|
||
|
## Introduction
|
||
|
|
||
|
Pointer Authentication is a mechanism by which certain pointers are signed.
|
||
|
When a pointer gets signed, a cryptographic hash of its value and other values
|
||
|
(pepper and salt) is stored in unused bits of that pointer.
|
||
|
|
||
|
Before the pointer is used, it needs to be authenticated, i.e., have its
|
||
|
signature checked. This prevents pointer values of unknown origin from being
|
||
|
used to replace the signed pointer value.
|
||
|
|
||
|
At the IR level, it is represented using:
|
||
|
|
||
|
* a [set of intrinsics](#intrinsics) (to sign/authenticate pointers)
|
||
|
* a [call operand bundle](#operand-bundle) (to authenticate called pointers)
|
||
|
|
||
|
The current implementation leverages the
|
||
|
[Armv8.3-A PAuth/Pointer Authentication Code](#armv8-3-a-pauth-pointer-authentication-code)
|
||
|
instructions in the [AArch64 backend](#aarch64-support).
|
||
|
This support is used to implement the Darwin arm64e ABI, as well as the
|
||
|
[PAuth ABI Extension to ELF](https://github.com/ARM-software/abi-aa/blob/main/pauthabielf64/pauthabielf64.rst).
|
||
|
|
||
|
|
||
|
## LLVM IR Representation
|
||
|
|
||
|
### Intrinsics
|
||
|
|
||
|
These intrinsics are provided by LLVM to expose pointer authentication
|
||
|
operations.
|
||
|
|
||
|
|
||
|
#### '`llvm.ptrauth.sign`'
|
||
|
|
||
|
##### Syntax:
|
||
|
|
||
|
```llvm
|
||
|
declare i64 @llvm.ptrauth.sign(i64 <value>, i32 <key>, i64 <discriminator>)
|
||
|
```
|
||
|
|
||
|
##### Overview:
|
||
|
|
||
|
The '`llvm.ptrauth.sign`' intrinsic signs a raw pointer.
|
||
|
|
||
|
|
||
|
##### Arguments:
|
||
|
|
||
|
The `value` argument is the raw pointer value to be signed.
|
||
|
The `key` argument is the identifier of the key to be used to generate the
|
||
|
signed value.
|
||
|
The `discriminator` argument is the additional diversity data to be used as a
|
||
|
discriminator (an integer, an address, or a blend of the two).
|
||
|
|
||
|
##### Semantics:
|
||
|
|
||
|
The '`llvm.ptrauth.sign`' intrinsic implements the `sign`_ operation.
|
||
|
It returns a signed value.
|
||
|
|
||
|
If `value` is already a signed value, the behavior is undefined.
|
||
|
|
||
|
If `value` is not a pointer value for which `key` is appropriate, the
|
||
|
behavior is undefined.
|
||
|
|
||
|
|
||
|
#### '`llvm.ptrauth.auth`'
|
||
|
|
||
|
##### Syntax:
|
||
|
|
||
|
```llvm
|
||
|
declare i64 @llvm.ptrauth.auth(i64 <value>, i32 <key>, i64 <discriminator>)
|
||
|
```
|
||
|
|
||
|
##### Overview:
|
||
|
|
||
|
The '`llvm.ptrauth.auth`' intrinsic authenticates a signed pointer.
|
||
|
|
||
|
##### Arguments:
|
||
|
|
||
|
The `value` argument is the signed pointer value to be authenticated.
|
||
|
The `key` argument is the identifier of the key that was used to generate
|
||
|
the signed value.
|
||
|
The `discriminator` argument is the additional diversity data to be used as a
|
||
|
discriminator.
|
||
|
|
||
|
##### Semantics:
|
||
|
|
||
|
The '`llvm.ptrauth.auth`' intrinsic implements the `auth`_ operation.
|
||
|
It returns a raw pointer value.
|
||
|
If `value` does not have a correct signature for `key` and `discriminator`,
|
||
|
the intrinsic traps in a target-specific way.
|
||
|
|
||
|
|
||
|
#### '`llvm.ptrauth.strip`'
|
||
|
|
||
|
##### Syntax:
|
||
|
|
||
|
```llvm
|
||
|
declare i64 @llvm.ptrauth.strip(i64 <value>, i32 <key>)
|
||
|
```
|
||
|
|
||
|
##### Overview:
|
||
|
|
||
|
The '`llvm.ptrauth.strip`' intrinsic strips the embedded signature out of a
|
||
|
possibly-signed pointer.
|
||
|
|
||
|
|
||
|
##### Arguments:
|
||
|
|
||
|
The `value` argument is the signed pointer value to be stripped.
|
||
|
The `key` argument is the identifier of the key that was used to generate
|
||
|
the signed value.
|
||
|
|
||
|
##### Semantics:
|
||
|
|
||
|
The '`llvm.ptrauth.strip`' intrinsic implements the `strip`_ operation.
|
||
|
It returns a raw pointer value. It does **not** check that the
|
||
|
signature is valid.
|
||
|
|
||
|
`key` should identify a key that is appropriate for `value`, as defined
|
||
|
by the target-specific [keys](#keys)).
|
||
|
|
||
|
If `value` is a raw pointer value, it is returned as-is (provided the `key`
|
||
|
is appropriate for the pointer).
|
||
|
|
||
|
If `value` is not a pointer value for which `key` is appropriate, the
|
||
|
behavior is target-specific.
|
||
|
|
||
|
If `value` is a signed pointer value, but `key` does not identify the
|
||
|
same key that was used to generate `value`, the behavior is
|
||
|
target-specific.
|
||
|
|
||
|
|
||
|
#### '`llvm.ptrauth.resign`'
|
||
|
|
||
|
##### Syntax:
|
||
|
|
||
|
```llvm
|
||
|
declare i64 @llvm.ptrauth.resign(i64 <value>,
|
||
|
i32 <old key>, i64 <old discriminator>,
|
||
|
i32 <new key>, i64 <new discriminator>)
|
||
|
```
|
||
|
|
||
|
##### Overview:
|
||
|
|
||
|
The '`llvm.ptrauth.resign`' intrinsic re-signs a signed pointer using
|
||
|
a different key and diversity data.
|
||
|
|
||
|
##### Arguments:
|
||
|
|
||
|
The `value` argument is the signed pointer value to be authenticated.
|
||
|
The `old key` argument is the identifier of the key that was used to generate
|
||
|
the signed value.
|
||
|
The `old discriminator` argument is the additional diversity data to be used
|
||
|
as a discriminator in the auth operation.
|
||
|
The `new key` argument is the identifier of the key to use to generate the
|
||
|
resigned value.
|
||
|
The `new discriminator` argument is the additional diversity data to be used
|
||
|
as a discriminator in the sign operation.
|
||
|
|
||
|
##### Semantics:
|
||
|
|
||
|
The '`llvm.ptrauth.resign`' intrinsic performs a combined `auth`_ and `sign`_
|
||
|
operation, without exposing the intermediate raw pointer.
|
||
|
It returns a signed pointer value.
|
||
|
If `value` does not have a correct signature for `old key` and
|
||
|
`old discriminator`, the intrinsic traps in a target-specific way.
|
||
|
|
||
|
#### '`llvm.ptrauth.sign_generic`'
|
||
|
|
||
|
##### Syntax:
|
||
|
|
||
|
```llvm
|
||
|
declare i64 @llvm.ptrauth.sign_generic(i64 <value>, i64 <discriminator>)
|
||
|
```
|
||
|
|
||
|
##### Overview:
|
||
|
|
||
|
The '`llvm.ptrauth.sign_generic`' intrinsic computes a generic signature of
|
||
|
arbitrary data.
|
||
|
|
||
|
##### Arguments:
|
||
|
|
||
|
The `value` argument is the arbitrary data value to be signed.
|
||
|
The `discriminator` argument is the additional diversity data to be used as a
|
||
|
discriminator.
|
||
|
|
||
|
##### Semantics:
|
||
|
|
||
|
The '`llvm.ptrauth.sign_generic`' intrinsic computes the signature of a given
|
||
|
combination of value and additional diversity data.
|
||
|
|
||
|
It returns a full signature value (as opposed to a signed pointer value, with
|
||
|
an embedded partial signature).
|
||
|
|
||
|
As opposed to [`llvm.ptrauth.sign`](#llvm-ptrauth-sign), it does not interpret
|
||
|
`value` as a pointer value. Instead, it is an arbitrary data value.
|
||
|
|
||
|
|
||
|
#### '`llvm.ptrauth.blend`'
|
||
|
|
||
|
##### Syntax:
|
||
|
|
||
|
```llvm
|
||
|
declare i64 @llvm.ptrauth.blend(i64 <address discriminator>, i64 <integer discriminator>)
|
||
|
```
|
||
|
|
||
|
##### Overview:
|
||
|
|
||
|
The '`llvm.ptrauth.blend`' intrinsic blends a pointer address discriminator
|
||
|
with a small integer discriminator to produce a new "blended" discriminator.
|
||
|
|
||
|
##### Arguments:
|
||
|
|
||
|
The `address discriminator` argument is a pointer value.
|
||
|
The `integer discriminator` argument is a small integer, as specified by the
|
||
|
target.
|
||
|
|
||
|
##### Semantics:
|
||
|
|
||
|
The '`llvm.ptrauth.blend`' intrinsic combines a small integer discriminator
|
||
|
with a pointer address discriminator, in a way that is specified by the target
|
||
|
implementation.
|
||
|
|
||
|
|
||
|
### Operand Bundle
|
||
|
|
||
|
Function pointers used as indirect call targets can be signed when materialized,
|
||
|
and authenticated before calls. This can be accomplished with the
|
||
|
[`llvm.ptrauth.auth`](#llvm-ptrauth-auth) intrinsic, feeding its result to
|
||
|
an indirect call.
|
||
|
|
||
|
However, that exposes the intermediate, unauthenticated pointer, e.g., if it
|
||
|
gets spilled to the stack. An attacker can then overwrite the pointer in
|
||
|
memory, negating the security benefit provided by pointer authentication.
|
||
|
To prevent that, the `ptrauth` operand bundle may be used: it guarantees that
|
||
|
the intermediate call target is kept in a register and never stored to memory.
|
||
|
This hardening benefit is similar to that provided by
|
||
|
[`llvm.ptrauth.resign`](#llvm-ptrauth-resign)).
|
||
|
|
||
|
Concretely:
|
||
|
|
||
|
```llvm
|
||
|
define void @f(void ()* %fp) {
|
||
|
call void %fp() [ "ptrauth"(i32 <key>, i64 <data>) ]
|
||
|
ret void
|
||
|
}
|
||
|
```
|
||
|
|
||
|
is functionally equivalent to:
|
||
|
|
||
|
```llvm
|
||
|
define void @f(void ()* %fp) {
|
||
|
%fp_i = ptrtoint void ()* %fp to i64
|
||
|
%fp_auth = call i64 @llvm.ptrauth.auth(i64 %fp_i, i32 <key>, i64 <data>)
|
||
|
%fp_auth_p = inttoptr i64 %fp_auth to void ()*
|
||
|
call void %fp_auth_p()
|
||
|
ret void
|
||
|
}
|
||
|
```
|
||
|
|
||
|
but with the added guarantee that `%fp_i`, `%fp_auth`, and `%fp_auth_p`
|
||
|
are not stored to (and reloaded from) memory.
|
||
|
|
||
|
|
||
|
## AArch64 Support
|
||
|
|
||
|
AArch64 is currently the only architecture with full support of the pointer
|
||
|
authentication primitives, based on Armv8.3-A instructions.
|
||
|
|
||
|
### Armv8.3-A PAuth Pointer Authentication Code
|
||
|
|
||
|
The Armv8.3-A architecture extension defines the PAuth feature, which provides
|
||
|
support for instructions that manipulate Pointer Authentication Codes (PAC).
|
||
|
|
||
|
#### Keys
|
||
|
|
||
|
5 keys are supported by the PAuth feature.
|
||
|
|
||
|
Of those, 4 keys are interchangeably usable to specify the key used in IR
|
||
|
constructs:
|
||
|
* `ASIA`/`ASIB` are instruction keys (encoded as respectively 0 and 1).
|
||
|
* `ASDA`/`ASDB` are data keys (encoded as respectively 2 and 3).
|
||
|
|
||
|
`ASGA` is a special key that cannot be explicitly specified, and is only ever
|
||
|
used implicitly, to implement the
|
||
|
[`llvm.ptrauth.sign_generic`](#llvm-ptrauth-sign-generic) intrinsic.
|
||
|
|
||
|
#### Instructions
|
||
|
|
||
|
The IR [Intrinsics](#intrinsics) described above map onto these
|
||
|
instructions as such:
|
||
|
* [`llvm.ptrauth.sign`](#llvm-ptrauth-sign): `PAC{I,D}{A,B}{Z,SP,}`
|
||
|
* [`llvm.ptrauth.auth`](#llvm-ptrauth-auth): `AUT{I,D}{A,B}{Z,SP,}`
|
||
|
* [`llvm.ptrauth.strip`](#llvm-ptrauth-strip): `XPAC{I,D}`
|
||
|
* [`llvm.ptrauth.blend`](#llvm-ptrauth-blend): The semantics of the blend
|
||
|
operation are specified by the ABI. In both the ELF PAuth ABI Extension and
|
||
|
arm64e, it's a `MOVK` into the high 16 bits. Consequently, this limits
|
||
|
the width of the integer discriminator used in blends to 16 bits.
|
||
|
* [`llvm.ptrauth.sign_generic`](#llvm-ptrauth-sign-generic): `PACGA`
|
||
|
* [`llvm.ptrauth.resign`](#llvm-ptrauth-resign): `AUT*+PAC*`. These are
|
||
|
represented as a single pseudo-instruction in the backend to guarantee that
|
||
|
the intermediate raw pointer value is not spilled and attackable.
|
||
|
|
||
|
#### Assembly Representation
|
||
|
|
||
|
At the assembly level, authenticated relocations are represented
|
||
|
using the `@AUTH` modifier:
|
||
|
|
||
|
```asm
|
||
|
.quad _target@AUTH(<key>,<discriminator>[,addr])
|
||
|
```
|
||
|
|
||
|
where:
|
||
|
* `key` is the Armv8.3-A key identifier (`ia`, `ib`, `da`, `db`)
|
||
|
* `discriminator` is the 16-bit unsigned discriminator value
|
||
|
* `addr` signifies that the authenticated pointer is address-discriminated
|
||
|
(that is, that the relocation's target address is to be blended into the
|
||
|
`discriminator` before it is used in the sign operation.
|
||
|
|
||
|
For example:
|
||
|
```asm
|
||
|
_authenticated_reference_to_sym:
|
||
|
.quad _sym@AUTH(db,0)
|
||
|
_authenticated_reference_to_sym_addr_disc:
|
||
|
.quad _sym@AUTH(ia,12,addr)
|
||
|
```
|
||
|
|
||
|
#### ELF Object File Representation
|
||
|
|
||
|
At the object file level, authenticated relocations are represented
|
||
|
using the `R_AARCH64_AUTH_ABS64` relocation kind (with value `0xE100`).
|
||
|
|
||
|
The signing schema is encoded in the place of relocation to be applied
|
||
|
as follows:
|
||
|
|
||
|
```
|
||
|
| 63 | 62 | 61:60 | 59:48 | 47:32 | 31:0 |
|
||
|
| ----------------- | -------- | -------- | -------- | ------------- | ------------------- |
|
||
|
| address diversity | reserved | key | reserved | discriminator | reserved for addend |
|
||
|
```
|