
This updates the pointer authentication documentation to include a complete description of the existing functionaliy and behaviour, details of the more complex aspects of the semantics and security properties, and the Apple arm64e ABI design. Co-authored-by: Ahmed Bougacha Co-authored-by: Akira Hatanaka Co-authored-by: John Mccall --------- Co-authored-by: Ahmed Bougacha <ahmed@bougacha.org> Co-authored-by: Akira Hatanaka <ahatanak@gmail.com> Co-authored-by: John Mccall <rjmccall@apple.com>
1700 lines
77 KiB
ReStructuredText
1700 lines
77 KiB
ReStructuredText
Pointer Authentication
|
|
======================
|
|
|
|
.. contents::
|
|
:local:
|
|
|
|
Introduction
|
|
------------
|
|
|
|
Pointer authentication is a technology which offers strong probabilistic
|
|
protection against exploiting a broad class of memory bugs to take control of
|
|
program execution. When adopted consistently in a language ABI, it provides
|
|
a form of relatively fine-grained control flow integrity (CFI) check that
|
|
resists both return-oriented programming (ROP) and jump-oriented programming
|
|
(JOP) attacks.
|
|
|
|
While pointer authentication can be implemented purely in software, direct
|
|
hardware support (e.g. as provided by Armv8.3 PAuth) can dramatically improve
|
|
performance and code size. Similarly, while pointer authentication
|
|
can be implemented on any architecture, taking advantage of the (typically)
|
|
excess addressing range of a target with 64-bit pointers minimizes the impact
|
|
on memory performance and can allow interoperation with existing code (by
|
|
disabling pointer authentication dynamically). This document will generally
|
|
attempt to present the pointer authentication feature independent of any
|
|
hardware implementation or ABI. Considerations that are
|
|
implementation-specific are clearly identified throughout.
|
|
|
|
Note that there are several different terms in use:
|
|
|
|
- **Pointer authentication** is a target-independent language technology.
|
|
|
|
- **PAuth** (sometimes referred to as **PAC**, for Pointer Authentication
|
|
Codes) is an AArch64 architecture extension that provides hardware support
|
|
for pointer authentication. Additional extensions either modify some of the
|
|
PAuth instruction behavior (notably FPAC), or provide new instruction
|
|
variants (PAuth_LR).
|
|
|
|
- **Armv8.3** is an AArch64 architecture revision that makes PAuth mandatory.
|
|
|
|
- **arm64e** is a specific ABI (not yet fully stable) for implementing pointer
|
|
authentication using PAuth on certain Apple operating systems.
|
|
|
|
This document serves four purposes:
|
|
|
|
- It describes the basic ideas of pointer authentication.
|
|
|
|
- It documents several language extensions that are useful on targets using
|
|
pointer authentication.
|
|
|
|
- It presents a theory of operation for the security mitigation, describing the
|
|
basic requirements for correctness, various weaknesses in the mechanism, and
|
|
ways in which programmers can strengthen its protections (including
|
|
recommendations for language implementors).
|
|
|
|
- It documents the stable ABI of the C, C++, and Objective-C languages on arm64e
|
|
platforms.
|
|
|
|
|
|
Basic concepts
|
|
--------------
|
|
|
|
The simple address of an object or function is a **raw pointer**. A raw
|
|
pointer can be **signed** to produce a **signed pointer**. A signed pointer
|
|
can be then **authenticated** in order to verify that it was **validly signed**
|
|
and extract the original raw pointer. These terms reflect the most likely
|
|
implementation technique: computing and storing a cryptographic signature along
|
|
with the pointer.
|
|
|
|
An **abstract signing key** is a name which refers to a secret key which is
|
|
used to sign and authenticate pointers. The concrete key value for a
|
|
particular name is consistent throughout a process.
|
|
|
|
A **discriminator** is an arbitrary value used to **diversify** signed pointers
|
|
so that one validly-signed pointer cannot simply be copied over another.
|
|
A discriminator is simply opaque data of some implementation-defined size that
|
|
is included in the signature as a salt (see `Discriminators`_ for details.)
|
|
|
|
Nearly all aspects of pointer authentication use just these two primary
|
|
operations:
|
|
|
|
- ``sign(raw_pointer, key, discriminator)`` produces a signed pointer given
|
|
a raw pointer, an abstract signing key, and a discriminator.
|
|
|
|
- ``auth(signed_pointer, key, discriminator)`` produces a raw pointer given
|
|
a signed pointer, an abstract signing key, and a discriminator.
|
|
|
|
``auth(sign(raw_pointer, key, discriminator), key, discriminator)`` must
|
|
succeed and produce ``raw_pointer``. ``auth`` applied to a value that was
|
|
ultimately produced in any other way is expected to fail, which halts the
|
|
program either:
|
|
|
|
- immediately, on implementations that enforce ``auth`` success (e.g., when
|
|
using compiler-generated ``auth`` failure checks, or Armv8.3 with the FPAC
|
|
extension), or
|
|
|
|
- when the resulting pointer value is used, on implementations that don't.
|
|
|
|
However, regardless of the implementation's handling of ``auth`` failures, it
|
|
is permitted for ``auth`` to fail to detect that a signed pointer was not
|
|
produced in this way, in which case it may return anything; this is what makes
|
|
pointer authentication a probabilistic mitigation rather than a perfect one.
|
|
|
|
There are two secondary operations which are required only to implement certain
|
|
intrinsics in ``<ptrauth.h>``:
|
|
|
|
- ``strip(signed_pointer, key)`` produces a raw pointer given a signed pointer
|
|
and a key without verifying its validity, unlike ``auth``. This is useful
|
|
for certain kinds of tooling, such as crash backtraces; it should generally
|
|
not be used in the basic language ABI except in very careful ways.
|
|
|
|
- ``sign_generic(value)`` produces a cryptographic signature for arbitrary
|
|
data, not necessarily a pointer. This is useful for efficiently verifying
|
|
that non-pointer data has not been tampered with.
|
|
|
|
Whenever any of these operations is called for, the key value must be known
|
|
statically. This is because the layout of a signed pointer may vary according
|
|
to the signing key. (For example, in Armv8.3, the layout of a signed pointer
|
|
depends on whether Top Byte Ignore (TBI) is enabled, which can be set
|
|
independently for I and D keys.)
|
|
|
|
.. admonition:: Note for API designers and language implementors
|
|
|
|
These are the *primitive* operations of pointer authentication, provided for
|
|
clarity of description. They are not suitable either as high-level
|
|
interfaces or as primitives in a compiler IR because they expose raw
|
|
pointers. Raw pointers require special attention in the language
|
|
implementation to avoid the accidental creation of exploitable code
|
|
sequences; see the section on `Attackable code sequences`_.
|
|
|
|
The following details are all implementation-defined:
|
|
|
|
- the nature of a signed pointer
|
|
- the size of a discriminator
|
|
- the number and nature of the signing keys
|
|
- the implementation of the ``sign``, ``auth``, ``strip``, and ``sign_generic``
|
|
operations
|
|
|
|
While the use of the terms "sign" and "signed pointer" suggest the use of
|
|
a cryptographic signature, other implementations may be possible. See
|
|
`Alternative implementations`_ for an exploration of implementation options.
|
|
|
|
.. admonition:: Implementation example: Armv8.3
|
|
|
|
Readers may find it helpful to know how these terms map to Armv8.3 PAuth:
|
|
|
|
- A signed pointer is a pointer with a signature stored in the
|
|
otherwise-unused high bits. The kernel configures the address width based
|
|
on the system's addressing needs, and enables TBI for I or D keys as
|
|
needed. The bits above the address bits and below the TBI bits (if
|
|
enabled) are unused. The signature width then depends on this addressing
|
|
configuration.
|
|
|
|
- A discriminator is a 64-bit integer. Constant discriminators are 16-bit
|
|
integers. Blending a constant discriminator into an address consists of
|
|
replacing the top 16 bits of the pointer containing the address with the
|
|
constant. Pointers used for blending purposes should only have address
|
|
bits, since higher bits will be at least partially overwritten with the
|
|
constant discriminator.
|
|
|
|
- There are five 128-bit signing-key registers, each of which can only be
|
|
directly read or set by privileged code. Of these, four are used for
|
|
signing pointers, and the fifth is used only for ``sign_generic``. The key
|
|
data is simply a pepper added to the hash, not an encryption key, and so
|
|
can be initialized using random data.
|
|
|
|
- ``sign`` computes a cryptographic hash of the pointer, discriminator, and
|
|
signing key, and stores it in the high bits as the signature. ``auth``
|
|
removes the signature, computes the same hash, and compares the result with
|
|
the stored signature. ``strip`` removes the signature without
|
|
authenticating it. The ``aut`` instructions in the baseline Armv8.3 PAuth
|
|
feature do not guarantee to trap on authentication failure; instead, they
|
|
simply corrupt the pointer so that later uses will likely trap. Unless the
|
|
"later use" follows immediately and cannot be recovered from (e.g. with a
|
|
signal handler), this does not provide adequate protection against
|
|
`authentication oracles`_, so implementations must emit additional
|
|
instructions to force an immediate trap. This is unnecessary if the
|
|
processor provides the optional ``FPAC`` extension, which guarantees an
|
|
immediate trap.
|
|
|
|
- ``sign_generic`` corresponds to the ``pacga`` instruction, which takes two
|
|
64-bit values and produces a 64-bit cryptographic hash. Implementations of
|
|
this instruction are not required to produce meaningful data in all bits of
|
|
the result.
|
|
|
|
Discriminators
|
|
~~~~~~~~~~~~~~
|
|
|
|
A discriminator is arbitrary extra data which alters the signature calculated
|
|
for a pointer. When two pointers are signed differently --- either with
|
|
different keys or with different discriminators --- an attacker cannot simply
|
|
replace one pointer with the other.
|
|
|
|
To use standard cryptographic terminology, a discriminator acts as a
|
|
`salt <https://en.wikipedia.org/wiki/Salt_(cryptography)>`_ in the signing of a
|
|
pointer, and the key data acts as a
|
|
`pepper <https://en.wikipedia.org/wiki/Pepper_(cryptography)>`_. That is,
|
|
both the discriminator and key data are ultimately just added as inputs to the
|
|
signing algorithm along with the pointer, but they serve significantly
|
|
different roles. The key data is a common secret added to every signature,
|
|
whereas the discriminator is a value that can be derived from
|
|
the context in which a specific pointer is signed. However, unlike a password
|
|
salt, it's important that discriminators be *independently* derived from the
|
|
circumstances of the signing; they should never simply be stored alongside
|
|
a pointer. Discriminators are then re-derived in authentication operations.
|
|
|
|
The intrinsic interface in ``<ptrauth.h>`` allows an arbitrary discriminator
|
|
value to be provided, but can only be used when running normal code. The
|
|
discriminators used by language ABIs must be restricted to make it feasible for
|
|
the loader to sign pointers stored in global memory without needing excessive
|
|
amounts of metadata. Under these restrictions, a discriminator may consist of
|
|
either or both of the following:
|
|
|
|
- The address at which the pointer is stored in memory. A pointer signed with
|
|
a discriminator which incorporates its storage address is said to have
|
|
**address diversity**. In general, using address diversity means that
|
|
a pointer cannot be reliably copied by an attacker to or from a different
|
|
memory location. However, an attacker may still be able to attack a larger
|
|
call sequence if they can alter the address through which the pointer is
|
|
accessed. Furthermore, some situations cannot use address diversity because
|
|
of language or other restrictions.
|
|
|
|
- A constant integer, called a **constant discriminator**. A pointer signed
|
|
with a non-zero constant discriminator is said to have **constant
|
|
diversity**. If the discriminator is specific to a single declaration, it is
|
|
said to have **declaration diversity**; if the discriminator is specific to
|
|
a type of value, it is said to have **type diversity**. For example, C++
|
|
v-tables on arm64e sign their component functions using a hash of their
|
|
method names and signatures, which provides declaration diversity; similarly,
|
|
C++ member function pointers sign their invocation functions using a hash of
|
|
the member pointer type, which provides type diversity.
|
|
|
|
The implementation may need to restrict constant discriminators to be
|
|
significantly smaller than the full size of a discriminator. For example, on
|
|
arm64e, constant discriminators are only 16-bit values. This is believed to
|
|
not significantly weaken the mitigation, since collisions remain uncommon.
|
|
|
|
The algorithm for blending a constant discriminator with a storage address is
|
|
implementation-defined.
|
|
|
|
.. _Signing schemas:
|
|
|
|
Signing schemas
|
|
~~~~~~~~~~~~~~~
|
|
|
|
Correct use of pointer authentication requires the signing code and the
|
|
authenticating code to agree about the **signing schema** for the pointer:
|
|
|
|
- the abstract signing key with which the pointer should be signed and
|
|
- an algorithm for computing the discriminator.
|
|
|
|
As described in the section above on `Discriminators`_, in most situations, the
|
|
discriminator is produced by taking a constant discriminator and optionally
|
|
blending it with the storage address of the pointer. In these situations, the
|
|
signing schema breaks down even more simply:
|
|
|
|
- the abstract signing key,
|
|
- a constant discriminator, and
|
|
- whether to use address diversity.
|
|
|
|
It is important that the signing schema be independently derived at all signing
|
|
and authentication sites. Preferably, the schema should be hard-coded
|
|
everywhere it is needed, but at the very least, it must not be derived by
|
|
inspecting information stored along with the pointer. See the section on
|
|
`Attacks on pointer authentication`_ for more information.
|
|
|
|
|
|
Language features
|
|
-----------------
|
|
|
|
There are three levels of the pointer authentication language feature:
|
|
|
|
- The language implementation automatically signs and authenticates function
|
|
pointers (and certain data pointers) across a variety of standard situations,
|
|
including return addresses, function pointers, and C++ virtual functions. The
|
|
intent is for all pointers to code in program memory to be signed in some way
|
|
and for all branches to code in program text to authenticate those
|
|
signatures. In addition to the code pointers themselves, we also use pointer
|
|
authentication to protect data values that directly or indirectly influence
|
|
control flow or program integrity, or can provide attackers with some other
|
|
powerful program compromise.
|
|
|
|
- The language also provides extensions to override the default rules used by
|
|
the language implementation. For example, the ``__ptrauth`` type qualifier
|
|
can be used to change how pointers or pointer sized integers are signed when
|
|
they are stored in a particular variable or field; this provides much stronger
|
|
protection than is guaranteed by the default rules for C function and data
|
|
pointers.
|
|
|
|
- Finally, the language provides the ``<ptrauth.h>`` intrinsic interface for
|
|
manually signing and authenticating pointers in code. These can be used in
|
|
circumstances where very specific behavior is required.
|
|
|
|
Language implementation
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
For the most part, pointer authentication is an unobserved detail of the
|
|
implementation of the programming language. Any element of the language
|
|
implementation that would perform an indirect branch to a pointer is implicitly
|
|
altered so that the pointer is signed when first constructed and authenticated
|
|
when the branch is performed. This includes:
|
|
|
|
- indirect-call features in the programming language, such as C function
|
|
pointers, C++ virtual functions, C++ member function pointers, the "blocks"
|
|
C extension, and so on;
|
|
|
|
- returning from a function, no matter how it is called; and
|
|
|
|
- indirect calls introduced by the implementation, such as branches through the
|
|
global offset table (GOT) used to implement direct calls to functions defined
|
|
outside of the current shared object.
|
|
|
|
For more information about this, see the `Language ABI`_ section.
|
|
|
|
However, some aspects of the implementation are observable by the programmer or
|
|
otherwise require special notice.
|
|
|
|
C data pointers
|
|
^^^^^^^^^^^^^^^
|
|
|
|
The current implementation in Clang does not sign pointers to ordinary data by
|
|
default. For a partial explanation of the reasoning behind this, see the
|
|
`Theory of Operation`_ section.
|
|
|
|
A specific data pointer which is more security-sensitive than most can be
|
|
signed using the `__ptrauth qualifier`_ or using the ``<ptrauth.h>``
|
|
intrinsics.
|
|
|
|
C function pointers
|
|
^^^^^^^^^^^^^^^^^^^
|
|
|
|
The C standard imposes restrictions on the representation and semantics of
|
|
function pointer types which make it difficult to achieve satisfactory
|
|
signature diversity in the default language rules. See `Attacks on pointer
|
|
authentication`_ for more information about signature diversity. Programmers
|
|
should strongly consider using the ``__ptrauth`` qualifier to improve the
|
|
protections for important function pointers, such as the components of of
|
|
a hand-rolled "v-table"; see the section on the `__ptrauth qualifier`_ for
|
|
details.
|
|
|
|
The value of a pointer to a C function includes a signature, even when the
|
|
value is cast to a non-function-pointer type like ``void*`` or ``intptr_t``. On
|
|
implementations that use high bits to store the signature, this means that
|
|
relational comparisons and hashes will vary according to the exact signature
|
|
value, which is likely to change between executions of a program. In some
|
|
implementations, it may also vary based on the exact function pointer type.
|
|
|
|
Null pointers
|
|
^^^^^^^^^^^^^
|
|
|
|
In principle, an implementation could derive the signed null pointer value
|
|
simply by applying the standard signing algorithm to the raw null pointer
|
|
value. However, for likely signing algorithms, this would mean that the signed
|
|
null pointer value would no longer be statically known, which would have many
|
|
negative consequences. For one, it would become substantially more expensive
|
|
to emit null pointer values or to perform null-pointer checks. For another,
|
|
the pervasive (even if technically unportable) assumption that null pointers
|
|
are bitwise zero would be invalidated, making it substantially more difficult
|
|
to adopt pointer authentication, as well as weakening common optimizations for
|
|
zero-initialized memory such as the use of ``.bzz`` sections. Therefore it is
|
|
beneficial to treat null pointers specially by giving them their usual
|
|
representation. On AArch64, this requires additional code when working with
|
|
possibly-null pointers, such as when copying a pointer field that has been
|
|
signed with address diversity.
|
|
|
|
While this representation of nulls is the safest option for the general case,
|
|
there are some situations in which a null pointer may have important semantic
|
|
or security impact. For that purpose Clang has the concept of a pointer
|
|
authentication schema that signs and authenticates null values.
|
|
|
|
Return addresses
|
|
^^^^^^^^^^^^^^^^
|
|
|
|
The current implementation in Clang implicitly signs the return addresses in
|
|
function calls. While the value of the return address is technically an
|
|
implementation detail of a function, there are some important libraries and
|
|
development tools which rely on manually walking the chain of stack frames.
|
|
These tools must be updated to correctly account for pointer authentication,
|
|
either by stripping signatures (if security is not important for the tool, e.g.
|
|
if it is capturing a stack trace during a crash) or properly authenticating
|
|
them. More information about how these values are signed is available in the
|
|
`Language ABI`_ section.
|
|
|
|
C++ virtual functions
|
|
^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The current implementation in Clang signs virtual function pointers with
|
|
a discriminator derived from the full signature of the overridden method,
|
|
including the method name and parameter types. It is possible to write C++
|
|
code that relies on v-table layout remaining constant despite changes to
|
|
a method signature; for example, a parameter might be a ``typedef`` that
|
|
resolves to a different type based on a build setting. Such code violates
|
|
C++'s One Definition Rule (ODR), but that violation is not normally detected;
|
|
however, pointer authentication will detect it.
|
|
|
|
Language extensions
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
Feature testing
|
|
^^^^^^^^^^^^^^^
|
|
|
|
Whether the current target uses pointer authentication can be tested for with
|
|
a number of different tests.
|
|
|
|
- ``__PTRAUTH__`` macro is defined if ``<ptrauth.h>`` provides its normal
|
|
interface. This implies support for the pointer authentication intrinsics
|
|
and the ``__ptrauth`` qualifier.
|
|
|
|
- ``__has_feature(ptrauth_returns)`` is true if the target uses pointer
|
|
authentication to protect return addresses.
|
|
|
|
- ``__has_feature(ptrauth_calls)`` is true if the target uses pointer
|
|
authentication to protect indirect branches. On arm64e this implies
|
|
``__has_feature(ptrauth_returns)``, ``__has_feature(ptrauth_intrinsics)``,
|
|
and the ``__PTRAUTH__`` macro.
|
|
|
|
- For backwards compatibility purposes ``__has_feature(ptrauth_intrinsics)``
|
|
and ``__has_feature(ptrauth_qualifier)`` are available on arm64e targets.
|
|
These features are synonymous with each other, and are equivalent to testing
|
|
for the ``__PTRAUTH__`` macro definition. Use of these features should be
|
|
restricted to cases where backwards compatibility is required, and should be
|
|
paired with ``defined(__PTRAUTH__)``.
|
|
|
|
|
|
Clang provides several other tests only for historical purposes; for current
|
|
purposes they are all equivalent to ``ptrauth_calls``.
|
|
|
|
``__ptrauth`` qualifier
|
|
^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
``__ptrauth(key, address, discriminator)`` is an extended type
|
|
qualifier which causes so-qualified objects to hold pointers or pointer sized
|
|
integers signed using the specified schema rather than the default schema for
|
|
such types.
|
|
|
|
In the current implementation in Clang, the qualified type must be a C pointer
|
|
type, either to a function or to an object, or a pointer sized integer. It
|
|
currently cannot be an Objective-C pointer type, a C++ reference type, or a
|
|
block pointer type; these restrictions may be lifted in the future.
|
|
|
|
The current implementation in Clang is known to not provide adequate safety
|
|
guarantees against the creation of `signing oracles`_ when assigning data
|
|
pointers to ``__ptrauth``-qualified gl-values. See the section on `safe
|
|
derivation`_ for more information.
|
|
|
|
The qualifier's operands are as follows:
|
|
|
|
- ``key`` - an expression evaluating to a key value from ``<ptrauth.h>``; must
|
|
be a constant expression
|
|
|
|
- ``address`` - whether to use address diversity (1) or not (0); must be
|
|
a constant expression with one of these two values
|
|
|
|
- ``discriminator`` - a constant discriminator; must be a constant expression
|
|
|
|
See `Discriminators`_ for more information about discriminators.
|
|
|
|
Currently the operands must be constant-evaluable even within templates. In the
|
|
future this restriction may be lifted to allow value-dependent expressions as
|
|
long as they instantiate to a constant expression.
|
|
|
|
Consistent with the ordinary C/C++ rule for parameters, top-level ``__ptrauth``
|
|
qualifiers on a parameter (after parameter type adjustment) are ignored when
|
|
deriving the type of the function. The parameter will be passed using the
|
|
default ABI for the unqualified pointer type.
|
|
|
|
If ``x`` is an object of type ``__ptrauth(key, address, discriminator) T``,
|
|
then the signing schema of the value stored in ``x`` is a key of ``key`` and
|
|
a discriminator determined as follows:
|
|
|
|
- if ``address`` is 0, then the discriminator is ``discriminator``;
|
|
|
|
- if ``address`` is 1 and ``discriminator`` is 0, then the discriminator is
|
|
``&x``; otherwise
|
|
|
|
- if ``address`` is 1 and ``discriminator`` is non-zero, then the discriminator
|
|
is ``ptrauth_blend_discriminator(&x, discriminator)``; see
|
|
`ptrauth_blend_discriminator`_.
|
|
|
|
Non-triviality from address diversity
|
|
+++++++++++++++++++++++++++++++++++++
|
|
|
|
Address diversity must impose additional restrictions in order to allow the
|
|
implementation to correctly copy values. In C++, a type qualified with address
|
|
diversity is treated like a class type with non-trivial copy/move constructors
|
|
and assignment operators, with the usual effect on containing classes and
|
|
unions. C does not have a standard concept of non-triviality, and so we must
|
|
describe the basic rules here, with the intention of imitating the emergent
|
|
rules of C++:
|
|
|
|
- A type may be **non-trivial to copy**.
|
|
|
|
- A type may also be **illegal to copy**. Types that are illegal to copy are
|
|
always non-trivial to copy.
|
|
|
|
- A type may also be **address-sensitive**. This includes types that use self
|
|
referencing pointers, data protected by address diversified pointer
|
|
authentication, or other similar concepts.
|
|
|
|
- A type qualified with a ``ptrauth`` qualifier or implicit authentication
|
|
schema that requires address diversity is non-trivial to copy and
|
|
address-sensitive.
|
|
|
|
- An array type is illegal to copy, non-trivial to copy, or address-sensitive
|
|
if its element type is illegal to copy, non-trivial to copy, or
|
|
address-sensitive, respectively.
|
|
|
|
- A struct type is illegal to copy, non-trivial to copy, or address-sensitive
|
|
if it has a field whose type is illegal to copy, non-trivial to copy, or
|
|
address-sensitive, respectively.
|
|
|
|
- A union type is both illegal and non-trivial to copy if it has a field whose
|
|
type is non-trivial or illegal to copy.
|
|
|
|
- A union type is address-sensitive if it has a field whose type is
|
|
address-sensitive.
|
|
|
|
- A program is ill-formed if it uses a type that is illegal to copy as
|
|
a function parameter, argument, or return type.
|
|
|
|
- A program is ill-formed if an expression requires a type to be copied that is
|
|
illegal to copy.
|
|
|
|
- Otherwise, copying a type that is non-trivial to copy correctly copies its
|
|
subobjects.
|
|
|
|
- Types that are address-sensitive must always be passed and returned
|
|
indirectly. Thus, changing the address-sensitivity of a type may be
|
|
ABI-breaking even if its size and alignment do not change.
|
|
|
|
``<ptrauth.h>``
|
|
~~~~~~~~~~~~~~~
|
|
|
|
This header defines the following types and operations:
|
|
|
|
``ptrauth_key``
|
|
^^^^^^^^^^^^^^^
|
|
|
|
This ``enum`` is the type of abstract signing keys. In addition to defining
|
|
the set of implementation-specific signing keys (for example, Armv8.3 defines
|
|
``ptrauth_key_asia``), it also defines some portable aliases for those keys.
|
|
For example, ``ptrauth_key_function_pointer`` is the key generally used for
|
|
C function pointers, which will generally be suitable for other
|
|
function-signing schemas.
|
|
|
|
In all the operation descriptions below, key values must be constant values
|
|
corresponding to one of the implementation-specific abstract signing keys from
|
|
this ``enum``.
|
|
|
|
``ptrauth_extra_data_t``
|
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
This is a ``typedef`` of a standard integer type of the correct size to hold
|
|
a discriminator value.
|
|
|
|
In the signing and authentication operation descriptions below, discriminator
|
|
values must have either pointer type or integer type. If the discriminator is
|
|
an integer, it will be coerced to ``ptrauth_extra_data_t``.
|
|
|
|
``ptrauth_blend_discriminator``
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
.. code-block:: c
|
|
|
|
ptrauth_blend_discriminator(pointer, integer)
|
|
|
|
Produce a discriminator value which blends information from the given pointer
|
|
and the given integer.
|
|
|
|
Implementations may ignore some bits from each value, which is to say, the
|
|
blending algorithm may be chosen for speed and convenience over theoretical
|
|
strength as a hash-combining algorithm. For example, arm64e simply overwrites
|
|
the high 16 bits of the pointer with the low 16 bits of the integer, which can
|
|
be done in a single instruction with an immediate integer.
|
|
|
|
``pointer`` must have pointer type, and ``integer`` must have integer type. The
|
|
result has type ``ptrauth_extra_data_t``.
|
|
|
|
``ptrauth_string_discriminator``
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
.. code-block:: c
|
|
|
|
ptrauth_string_discriminator(string)
|
|
|
|
Compute a constant discriminator from the given string.
|
|
|
|
``string`` must be a string literal of ``char`` character type. The result has
|
|
type ``ptrauth_extra_data_t``.
|
|
|
|
The result value is never zero and always within range for both the
|
|
``__ptrauth`` qualifier and ``ptrauth_blend_discriminator``.
|
|
|
|
This can be used in constant expressions.
|
|
|
|
``ptrauth_strip``
|
|
^^^^^^^^^^^^^^^^^
|
|
|
|
.. code-block:: c
|
|
|
|
ptrauth_strip(signedPointer, key)
|
|
|
|
Given that ``signedPointer`` matches the layout for signed pointers signed with
|
|
the given key, extract the raw pointer from it. This operation does not trap
|
|
and cannot fail, even if the pointer is not validly signed.
|
|
|
|
``ptrauth_sign_constant``
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
.. code-block:: c
|
|
|
|
ptrauth_sign_constant(pointer, key, discriminator)
|
|
|
|
Return a signed pointer for a constant address in a manner which guarantees
|
|
a non-attackable sequence.
|
|
|
|
``pointer`` must be a constant expression of pointer type which evaluates to
|
|
a non-null pointer.
|
|
``key`` must be a constant expression of type ``ptrauth_key``.
|
|
``discriminator`` must be a constant expression of pointer or integer type;
|
|
if an integer, it will be coerced to ``ptrauth_extra_data_t``.
|
|
The result will have the same type as ``pointer``.
|
|
|
|
This can be used in constant expressions.
|
|
|
|
``ptrauth_sign_unauthenticated``
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
.. code-block:: c
|
|
|
|
ptrauth_sign_unauthenticated(pointer, key, discriminator)
|
|
|
|
Produce a signed pointer for the given raw pointer without applying any
|
|
authentication or extra treatment. This operation is not required to have the
|
|
same behavior on a null pointer that the language implementation would.
|
|
|
|
This is a treacherous operation that can easily result in `signing oracles`_.
|
|
Programs should use it seldom and carefully.
|
|
|
|
``ptrauth_auth_and_resign``
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
.. code-block:: c
|
|
|
|
ptrauth_auth_and_resign(pointer, oldKey, oldDiscriminator, newKey, newDiscriminator)
|
|
|
|
Authenticate that ``pointer`` is signed with ``oldKey`` and
|
|
``oldDiscriminator`` and then resign the raw-pointer result of that
|
|
authentication with ``newKey`` and ``newDiscriminator``.
|
|
|
|
``pointer`` must have pointer type. The result will have the same type as
|
|
``pointer``. This operation is not required to have the same behavior on
|
|
a null pointer that the language implementation would.
|
|
|
|
The code sequence produced for this operation must not be directly attackable.
|
|
However, if the discriminator values are not constant integers, their
|
|
computations may still be attackable. In the future, Clang should be enhanced
|
|
to guaranteed non-attackability if these expressions are
|
|
:ref:`safely-derived<Safe derivation>`.
|
|
|
|
``ptrauth_auth_function``
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
.. code-block:: c
|
|
|
|
ptrauth_auth_function(pointer, key, discriminator)
|
|
|
|
Authenticate that ``pointer`` is signed with ``key`` and ``discriminator`` and
|
|
re-sign it to the standard schema for a function pointer of its type.
|
|
|
|
``pointer`` must have function pointer type. The result will have the same
|
|
type as ``pointer``. This operation is not required to have the same behavior
|
|
on a null pointer that the language implementation would.
|
|
|
|
This operation makes the same attackability guarantees as
|
|
``ptrauth_auth_and_resign``.
|
|
|
|
If this operation appears syntactically as the function operand of a call,
|
|
Clang guarantees that the call will directly authenticate the function value
|
|
using the given schema rather than re-signing to the standard schema.
|
|
|
|
``ptrauth_auth_data``
|
|
^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
.. code-block:: c
|
|
|
|
ptrauth_auth_data(pointer, key, discriminator)
|
|
|
|
Authenticate that ``pointer`` is signed with ``key`` and ``discriminator`` and
|
|
remove the signature.
|
|
|
|
``pointer`` must have object pointer type. The result will have the same type
|
|
as ``pointer``. This operation is not required to have the same behavior on
|
|
a null pointer that the language implementation would.
|
|
|
|
In the future when Clang makes safe derivation guarantees, the result of
|
|
this operation should be considered safely-derived.
|
|
|
|
``ptrauth_sign_generic_data``
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
.. code-block:: c
|
|
|
|
ptrauth_sign_generic_data(value1, value2)
|
|
|
|
Computes a signature for the given pair of values, incorporating a secret
|
|
signing key.
|
|
|
|
This operation can be used to verify that arbitrary data has not been tampered
|
|
with by computing a signature for the data, storing that signature, and then
|
|
repeating this process and verifying that it yields the same result. This can
|
|
be reasonably done in any number of ways; for example, a library could compute
|
|
an ordinary checksum of the data and just sign the result in order to get the
|
|
tamper-resistance advantages of the secret signing key (since otherwise an
|
|
attacker could reliably overwrite both the data and the checksum).
|
|
|
|
``value1`` and ``value2`` must be either pointers or integers. If the integers
|
|
are larger than ``uintptr_t`` then data not representable in ``uintptr_t`` may
|
|
be discarded.
|
|
|
|
The result will have type ``ptrauth_generic_signature_t``, which is an integer
|
|
type. Implementations are not required to make all bits of the result equally
|
|
significant; in particular, some implementations are known to not leave
|
|
meaningful data in the low bits.
|
|
|
|
Standard ``__ptrauth`` qualifiers
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
``<ptrauth.h>`` additionally provides several macros which expand to
|
|
``__ptrauth`` qualifiers for common ABI situations.
|
|
|
|
For convenience, these macros expand to nothing when pointer authentication is
|
|
disabled.
|
|
|
|
These macros can be found in the header; some details of these macros may be
|
|
unstable or implementation-specific.
|
|
|
|
|
|
Theory of operation
|
|
-------------------
|
|
|
|
The threat model of pointer authentication is as follows:
|
|
|
|
- The attacker has the ability to read and write to a certain range of
|
|
addresses, possibly the entire address space. However, they are constrained
|
|
by the normal rules of the process: for example, they cannot write to memory
|
|
that is mapped read-only, and if they access unmapped memory it will trigger
|
|
a trap.
|
|
|
|
- The attacker has no ability to add arbitrary executable code to the program.
|
|
For example, the program does not include malicious code to begin with, and
|
|
the attacker cannot alter existing instructions, load a malicious shared
|
|
library, or remap writable pages as executable. If the attacker wants to get
|
|
the process to perform a specific sequence of actions, they must somehow
|
|
subvert the normal control flow of the process.
|
|
|
|
In both of the above paragraphs, it is merely assumed that the attacker's
|
|
*current* capabilities are restricted; that is, their current exploit does not
|
|
directly give them the power to do these things. The attacker's immediate goal
|
|
may well be to leverage their exploit to gain these capabilities, e.g. to load
|
|
a malicious dynamic library into the process, even though the process does not
|
|
directly contain code to do so.
|
|
|
|
Note that any bug that fits the above threat model can be immediately exploited
|
|
as a denial-of-service attack by simply performing an illegal access and
|
|
crashing the program. Pointer authentication cannot protect against this.
|
|
While denial-of-service attacks are unfortunate, they are also unquestionably
|
|
the best possible result of a bug this severe. Therefore, pointer authentication
|
|
enthusiastically embraces the idea of halting the program on a pointer
|
|
authentication failure rather than continuing in a possibly-compromised state.
|
|
|
|
Pointer authentication is a form of control-flow integrity (CFI) enforcement.
|
|
The basic security hypothesis behind CFI enforcement is that many bugs can only
|
|
be usefully exploited (other than as a denial-of-service) by leveraging them to
|
|
subvert the control flow of the program. If this is true, then by inhibiting or
|
|
limiting that subversion, it may be possible to largely mitigate the security
|
|
consequences of those bugs by rendering them impractical (or, ideally,
|
|
impossible) to exploit.
|
|
|
|
Every indirect branch in a program has a purpose. Using human intelligence, a
|
|
programmer can describe where a particular branch *should* go according to this
|
|
purpose: a ``return`` in ``printf`` should return to the call site, a particular
|
|
call in ``qsort`` should call the comparator that was passed in as an argument,
|
|
and so on. But for CFI to enforce that every branch in a program goes where it
|
|
*should* in this sense would require CFI to perfectly enforce every semantic
|
|
rule of the program's abstract machine; that is, it would require making the
|
|
programming environment perfectly sound. That is out of scope. Instead, the
|
|
goal of CFI is merely to catch attempts to make a branch go somewhere that its
|
|
obviously *shouldn't* for its purpose: for example, to stop a call from
|
|
branching into the middle of a function rather than its beginning. As the
|
|
information available to CFI gets better about the purpose of the branch, CFI
|
|
can enforce tighter and tighter restrictions on where the branch is permitted to
|
|
go. Still, ultimately CFI cannot make the program sound. This may help explain
|
|
why pointer authentication makes some of the choices it does: for example, to
|
|
sign and authenticate mostly code pointers rather than every pointer in the
|
|
program. Preventing attackers from redirecting branches is both particularly
|
|
important and particularly approachable as a goal. Detecting corruption more
|
|
broadly is infeasible with these techniques, and the attempt would have far
|
|
higher cost.
|
|
|
|
Attacks on pointer authentication
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Pointer authentication works as follows. Every indirect branch in a program has
|
|
a purpose. For every purpose, the implementation chooses a
|
|
:ref:`signing schema<Signing schemas>`. At some place where a pointer is known
|
|
to be correct for its purpose, it is signed according to the purpose's schema.
|
|
At every place where the pointer is needed for its purpose, it is authenticated
|
|
according to the purpose's schema. If that authentication fails, the program is
|
|
halted.
|
|
|
|
There are a variety of ways to attack this.
|
|
|
|
Attacks of interest to programmers
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
These attacks arise from weaknesses in the default protections offered by
|
|
pointer authentication. They can be addressed by using attributes or intrinsics
|
|
to opt in to stronger protection.
|
|
|
|
Substitution attacks
|
|
++++++++++++++++++++
|
|
|
|
An attacker can simply overwrite a pointer intended for one purpose with a
|
|
pointer intended for another purpose if both purposes use the same signing
|
|
schema and that schema does not use address diversity.
|
|
|
|
The most common source of this weakness is when code relies on using the default
|
|
language rules for C function pointers. The current implementation uses the
|
|
exact same signing schema for all C function pointers, even for functions of
|
|
substantially different type. While efforts are ongoing to improve constant
|
|
diversity for C function pointers of different type, there are necessary limits
|
|
to this. The C standard requires function pointers to be copyable with
|
|
``memcpy``, which means that function pointers can never use address diversity.
|
|
Furthermore, even if a function pointer can only be replaced with another
|
|
function of the exact same type, that can still be useful to an attacker, as in
|
|
the following example of a hand-rolled "v-table":
|
|
|
|
.. code-block:: c
|
|
|
|
struct ObjectOperations {
|
|
void (*retain)(Object *);
|
|
void (*release)(Object *);
|
|
void (*deallocate)(Object *);
|
|
void (*logStatus)(Object *);
|
|
};
|
|
|
|
The weakness in this design is that by lacking any context specific
|
|
discriminator, this means an attacker can substitute any of these fields with
|
|
any other function pointer signed with the default schema. Similarly the lack of
|
|
address diversity allows an attacker to replace the functions in one type's
|
|
"v-table" with those of another. This can be mitigated by overriding the default
|
|
authentication schema with a more specific signing schema for each purpose. For
|
|
instance, in this example, the ``__ptrauth`` qualifier can be used with a
|
|
different constant discriminator for each field. Since there's no particular
|
|
reason it's important for this v-table to be copyable with ``memcpy``, the
|
|
functions can also be signed with address diversity:
|
|
|
|
.. code-block:: c
|
|
|
|
#if defined(__PTRAUTH__)
|
|
#define objectOperation(discriminator) \
|
|
__ptrauth(ptrauth_key_function_pointer, 1, discriminator)
|
|
#else
|
|
#define objectOperation(discriminator)
|
|
#endif
|
|
|
|
struct ObjectOperations {
|
|
void (*objectOperation(0xf017) retain)(Object *);
|
|
void (*objectOperation(0x2639) release)(Object *);
|
|
void (*objectOperation(0x8bb0) deallocate)(Object *);
|
|
void (*objectOperation(0xc5d4) logStatus)(Object *);
|
|
};
|
|
|
|
This weakness can also sometimes be mitigated by simply keeping the signed
|
|
pointer in constant memory, but this is less effective than using better signing
|
|
diversity.
|
|
|
|
.. _Access path attacks:
|
|
|
|
Access path attacks
|
|
+++++++++++++++++++
|
|
|
|
If a signed pointer is often accessed indirectly (that is, by first loading the
|
|
address of the object where the signed pointer is stored), an attacker can
|
|
affect uses of it by overwriting the intermediate pointer in the access path.
|
|
|
|
The most common scenario exhibiting this weakness is an object with a pointer to
|
|
a "v-table" (a structure holding many function pointers). An attacker does not
|
|
need to replace a signed function pointer in the v-table if they can instead
|
|
simply replace the v-table pointer in the object with their own pointer ---
|
|
perhaps to memory where they've constructed their own v-table, or to existing
|
|
memory that coincidentally happens to contain a signed pointer at the right
|
|
offset that's been signed with the right signing schema.
|
|
|
|
This attack arises because data pointers are not signed by default. It works
|
|
even if the signed pointer uses address diversity: address diversity merely
|
|
means that each pointer is signed with its own storage address,
|
|
which (by design) is invariant to changes in the accessing pointer.
|
|
|
|
Using sufficiently diverse signing schemas within the v-table can provide
|
|
reasonably strong mitigation against this weakness. Always use address and type
|
|
diversity in v-tables to prevent attackers from assembling their own v-table.
|
|
Avoid re-using constant discriminators to prevent attackers from replacing a
|
|
v-table pointer with a pointer to totally unrelated memory that just happens to
|
|
contain an similarly-signed pointer, or reused memory containing a different
|
|
type.
|
|
|
|
Further mitigation can be attained by signing pointers to v-tables. Any
|
|
signature at all should prevent attackers from forging v-table pointers; they
|
|
will need to somehow harvest an existing signed pointer from elsewhere in
|
|
memory. Using a meaningful constant discriminator will force this to be
|
|
harvested from an object with similar structure (e.g. a different implementation
|
|
of the same interface). Using address diversity will prevent such harvesting
|
|
entirely. However, care must be taken when sourcing the v-table pointer
|
|
originally; do not blindly sign a pointer that is not
|
|
:ref:`safely derived<Safe derivation>`.
|
|
|
|
.. _Signing oracles:
|
|
|
|
Signing oracles
|
|
+++++++++++++++
|
|
|
|
A signing oracle is a bit of code which can be exploited by an attacker to sign
|
|
an arbitrary pointer in a way that can later be recovered. Such oracles can be
|
|
used by attackers to forge signatures matching the oracle's signing schema,
|
|
which is likely to cause a total compromise of pointer authentication's
|
|
effectiveness.
|
|
|
|
This attack only affects ordinary programmers if they are using certain
|
|
treacherous patterns of code. Currently this includes:
|
|
|
|
- all uses of the ``__ptrauth_sign_unauthenticated`` intrinsic and
|
|
- assigning values to ``__ptrauth``-qualified l-values.
|
|
|
|
Care must be taken in these situations to ensure that the pointer being signed
|
|
has been :ref:`safely derived<Safe derivation>` or is otherwise not possible to
|
|
attack. (In some cases, this may be challenging without compiler support.)
|
|
|
|
A diagnostic will be added in the future for implicitly dangerous patterns of
|
|
code, such as assigning a non-safely-derived values to a
|
|
``__ptrauth``-qualified l-value.
|
|
|
|
.. _Authentication oracles:
|
|
|
|
Authentication oracles
|
|
++++++++++++++++++++++
|
|
|
|
An authentication oracle is a bit of code which can be exploited by an attacker
|
|
to leak whether a signed pointer is validly signed without halting the program
|
|
if it isn't. Such oracles can be used to forge signatures matching the oracle's
|
|
signing schema if the attacker can repeatedly invoke the oracle for different
|
|
candidate signed pointers. This is likely to cause a total compromise of pointer
|
|
authentication's effectiveness.
|
|
|
|
There should be no way for an ordinary programmer to create an authentication
|
|
oracle using the current set of operations. However, implementation flaws in the
|
|
past have occasionally given rise to authentication oracles due to a failure to
|
|
immediately trap on authentication failure.
|
|
|
|
The likelihood of creating an authentication oracle is why there is currently no
|
|
intrinsic which queries whether a signed pointer is validly signed.
|
|
|
|
|
|
Attacks of interest to implementors
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
These attacks are not inherent to the model; they arise from mistakes in either
|
|
implementing or using the `sign` and `auth` operations. Avoiding these mistakes
|
|
requires careful work throughout the system.
|
|
|
|
Failure to trap on authentication failure
|
|
+++++++++++++++++++++++++++++++++++++++++
|
|
|
|
Any failure to halt the program on an authentication failure is likely to be
|
|
exploitable by attackers to create an
|
|
:ref:`authentication oracle<Authentication oracles>`.
|
|
|
|
There are several different ways to introduce this problem:
|
|
|
|
- The implementation might try to halt the program in some way that can be
|
|
intercepted.
|
|
|
|
For example, the Armv8.3 ``aut`` instructions do not directly trap on
|
|
authentication failure on processors that lack the ``FPAC`` extension.
|
|
Instead, they corrupt their results to be invalid pointers, with the idea that
|
|
subsequent uses of those pointers will trigger traps as bad memory accesses.
|
|
However, most kernels do not immediately halt programs that trap due to bad
|
|
memory accesses; instead, they notify the process to give it an opportunity to
|
|
recover. If this happens with an ``auth`` failure, the attacker may be able to
|
|
exploit the recovery path in a way that creates an oracle. Kernels must
|
|
provide a way for a process to trap unrecoverably, and this should cover all
|
|
``FPAC`` traps. Compilers must ensure that ``auth`` failures trigger an
|
|
unrecoverable trap, ideally by taking advantage of ``FPAC``, but if necessary
|
|
by emitting extra instructions.
|
|
|
|
- A compiler might use an intermediate representation (IR) for ``sign`` and
|
|
``auth`` operations that cannot make adequate correctness guarantees.
|
|
|
|
For example, suppose that an IR uses ARMv8.3-like semantics for ``auth``: the
|
|
operation merely corrupts its result on failure instead of promising to trap.
|
|
A frontend might emit patterns of IR that always follow an ``auth`` with a
|
|
memory access, thinking that this ensures correctness. But if the IR can be
|
|
transformed to insert code between the ``auth`` and the access, or if the
|
|
``auth`` can be speculated, then this potentially creates an oracle. It is
|
|
better for ``auth`` to semantically guarantee to trap, potentially requiring
|
|
an explicit check in the generated code. An ARMv8.3-like target can avoid this
|
|
explicit check in the common case by recognizing the pattern of an ``auth``
|
|
followed immediately by an access.
|
|
|
|
Attackable code sequences
|
|
+++++++++++++++++++++++++
|
|
|
|
If code that is part of a pointer authentication operation is interleaved with
|
|
code that may itself be vulnerable to attacks, an attacker may be able to use
|
|
this to create a :ref:`signing<Signing oracles>` or
|
|
:ref:`authentication<Authentication oracles>` oracle.
|
|
|
|
For example, suppose that the compiler is generating a call to a function and
|
|
passing two arguments: a signed constant pointer and a value derived from a
|
|
call. In ARMv8.3, this code might look like so:
|
|
|
|
.. code-block:: asm
|
|
|
|
adr x19, _callback. ; compute &_callback
|
|
paciza x19 ; sign it with a constant discriminator of 0
|
|
blr _argGenerator ; call _argGenerator() (returns in x0)
|
|
mov x1, x0 ; move call result to second arg register
|
|
mov x0, x19 ; move signed &_callback to first arg register
|
|
blr _function ; call _function
|
|
|
|
This code is correct, as would be a sequencing that does *both* the ``adr`` and
|
|
the ``paciza`` after the call to ``_argGenerator``. But a sequence that
|
|
computes the address of ``_callback`` but leaves it as a raw pointer in a
|
|
register during the call to ``_argGenerator`` would be vulnerable:
|
|
|
|
.. code-block:: asm
|
|
|
|
adr x19, _callback. ; compute &_callback
|
|
blr _argGenerator ; call _argGenerator() (returns in x0)
|
|
mov x1, x0 ; move call result to second arg register
|
|
paciza x19 ; sign &_callback
|
|
mov x0, x19 ; move signed &_callback to first arg register
|
|
blr _function ; call _function
|
|
|
|
If ``_argGenerator`` spills ``x19`` (a callee-save register), and if the
|
|
attacker can perform a write during this call, then the attacker can overwrite
|
|
the spill slot with an arbitrary pointer that will eventually be unconditionally
|
|
signed after the function returns. This would be a signing oracle.
|
|
|
|
The implementation can avoid this by obeying two basic rules:
|
|
|
|
- The compiler's intermediate representations (IR) should not provide operations
|
|
that expose intermediate raw pointers. This may require providing extra
|
|
operations that perform useful combinations of operations.
|
|
|
|
For example, there should be an "atomic" auth-and-resign operation that should
|
|
be used instead of emitting an ``auth`` operation whose result is fed into a
|
|
``sign``.
|
|
|
|
Similarly, if a pointer should be authenticated as part of doing a memory
|
|
access or a call, then the access or call should be decorated with enough
|
|
information to perform the authentication; there should not be a separate
|
|
``auth`` whose result is used as the pointer operand for the access or call.
|
|
(In LLVM IR, we do this for calls, but not yet for loads or stores.)
|
|
|
|
"Operations" includes things like materializing a signed value to a known
|
|
function or global variable. The compiler must be able to recognize and emit
|
|
this as a unified operation, rather than potentially splitting it up as in
|
|
the example above.
|
|
|
|
- The compiler backend should not be too aggressive about scheduling
|
|
instructions that are part of a pointer authentication operation. This may
|
|
require custom code-generation of these operations in some cases.
|
|
|
|
Register clobbering
|
|
+++++++++++++++++++
|
|
|
|
As a refinement of the section on `Attackable code sequences`_, if the attacker
|
|
has the ability to modify arbitrary *register* state at arbitrary points in the
|
|
program, then special care must be taken.
|
|
|
|
For example, ARMv8.3 might materialize a signed function pointer like so:
|
|
|
|
.. code-block:: asm
|
|
|
|
adr x0, _callback. ; compute &_callback
|
|
paciza x0 ; sign it with a constant discriminator of 0
|
|
|
|
If an attacker has the ability to overwrite ``x0`` between these two
|
|
instructions, this code sequence is vulnerable to becoming a signing oracle.
|
|
|
|
For the most part, this sort of attack is not possible: it is a basic element of
|
|
the design of modern computation that register state is private and inviolable.
|
|
However, in systems that support asynchronous interrupts, this property requires
|
|
the cooperation of the interrupt-handling code. If that code saves register
|
|
state to memory, and that memory can be overwritten by an attacker, then
|
|
essentially the attack can overwrite arbitrary register state at an arbitrary
|
|
point. This could be a concern if the threat model includes attacks on the
|
|
kernel or if the program uses user-space preemptive multitasking.
|
|
|
|
(Readers might object that an attacker cannot rely on asynchronous interrupts
|
|
triggering at an exact instruction boundary. In fact, researchers have had some
|
|
success in doing exactly that. Even ignoring that, though, we should aim to
|
|
protect against lucky attackers just as much as good ones.)
|
|
|
|
To protect against this, saved register state must be at least partially signed
|
|
(using something like `ptrauth_sign_generic_data`_). This is required for
|
|
correctness anyway because saved thread states include security-critical
|
|
registers such as SP, FP, PC, and LR (where applicable). Ideally, this
|
|
signature would cover all the registers, but since saving and restoring
|
|
registers can be very performance-sensitive, that may not be acceptable. It is
|
|
sufficient to set aside a small number of scratch registers that will be
|
|
guaranteed to be preserved correctly; the compiler can then be careful to only
|
|
store critical values like intermediate raw pointers in those registers.
|
|
|
|
``setjmp`` and ``longjmp`` should sign and authenticate the core registers (SP,
|
|
FP, PC, and LR), but they do not need to worry about intermediate values because
|
|
``setjmp`` can only be called synchronously, and the compiler should never
|
|
schedule pointer-authentication operations interleaved with arbitrary calls.
|
|
|
|
.. _Relative addresses:
|
|
|
|
Attacks on relative addressing
|
|
++++++++++++++++++++++++++++++
|
|
|
|
Relative addressing is a technique used to compress and reduce the load-time
|
|
cost of infrequently-used global data. The pointer authentication system is
|
|
unlikely to support signing or authenticating a relative address, and in most
|
|
cases it would defeat the point to do so: it would take additional storage
|
|
space, and applying the signature would take extra work at load time.
|
|
|
|
Relative addressing is not precluded by the use of pointer authentication, but
|
|
it does take extra considerations to make it secure:
|
|
|
|
- Relative addresses must only be stored in read-only memory. A writable
|
|
relative address can be overwritten to point nearly anywhere, making it
|
|
inherently insecure; this danger can only be compensated for with techniques
|
|
for protecting arbitrary data like `ptrauth_sign_generic_data`_.
|
|
|
|
- Relative addresses must only be accessed through signed pointers with adequate
|
|
diversity. If an attacker can perform an `access path attack` to replace the
|
|
pointer through which the relative address is accessed, they can easily cause
|
|
the relative address to point wherever they want.
|
|
|
|
Signature forging
|
|
+++++++++++++++++
|
|
|
|
If an attacker can exactly reproduce the behavior of the signing algorithm, and
|
|
they know all the correct inputs to it, then they can perfectly forge a
|
|
signature on an arbitrary pointer.
|
|
|
|
There are three components to avoiding this mistake:
|
|
|
|
- The abstract signing algorithm should be good: it should not have glaring
|
|
flaws which would allow attackers to predict its result with better than
|
|
random accuracy without knowing all the inputs (like the key values).
|
|
|
|
- The key values should be kept secret. If at all possible, they should never
|
|
be stored in accessible memory, or perhaps only stored encrypted.
|
|
|
|
- Contexts that are meant to be independently protected should use different
|
|
key values. For example, the kernel should not use the same keys as user
|
|
processes. Different user processes should also use different keys from each
|
|
other as much as possible, although this may pose its own technical
|
|
challenges.
|
|
|
|
Remapping
|
|
+++++++++
|
|
|
|
If an attacker can change the memory protections on certain pages of the
|
|
program's memory, that can substantially weaken the protections afforded by
|
|
pointer authentication.
|
|
|
|
- If an attacker can inject their own executable code, they can also certainly
|
|
inject code that can be used as a :ref:`signing oracle<Signing Oracles>`.
|
|
The same is true if they can write to the instruction stream.
|
|
|
|
- If an attacker can remap read-only program data sections to be writable, then
|
|
any use of :ref:`relative addresses` in global data becomes insecure.
|
|
|
|
- On platforms that use them, if an attacker can remap the memory containing
|
|
the `global offset tables`_ as writable, then any unsigned pointers in those
|
|
tables are insecure.
|
|
|
|
Remapping memory in this way often requires the attacker to have already
|
|
substantively subverted the control flow of the process. Nonetheless, if the
|
|
operating system has a mechanism for mapping pages in a way that cannot be
|
|
remapped, this should be used wherever possible.
|
|
|
|
.. _Safe Derivation:
|
|
|
|
Safe derivation
|
|
~~~~~~~~~~~~~~~
|
|
|
|
Whether a data pointer is stored, even briefly, as a raw pointer can affect the
|
|
security-correctness of a program. (Function pointers are never implicitly
|
|
stored as raw pointers; raw pointers to functions can only be produced with the
|
|
``<ptrauth.h>`` intrinsics.) Repeated re-signing can also impact performance.
|
|
Clang makes a modest set of guarantees in this area:
|
|
|
|
- An expression of pointer type is said to be **safely derived** if:
|
|
|
|
- it takes the address of a global variable or function, or
|
|
|
|
- it is a load from a gl-value of ``__ptrauth``-qualified type, or
|
|
|
|
- it is a load from read-only memory that has been initialized from a safely
|
|
derived source, such as the `data const` section of a binary or library.
|
|
|
|
- If a value that is safely derived is assigned to a ``__ptrauth``-qualified
|
|
object, including by initialization, then the value will be directly signed as
|
|
appropriate for the target qualifier and will not be stored as a raw pointer.
|
|
|
|
- If the function expression of a call is a gl-value of ``__ptrauth``-qualified
|
|
type, then the call will be authenticated directly according to the source
|
|
qualifier and will not be resigned to the default rule for a function pointer
|
|
of its type.
|
|
|
|
These guarantees are known to be inadequate for data pointer security. In
|
|
particular, Clang should be enhanced to make the following guarantees:
|
|
|
|
- A pointer should additionally be considered safely derived if it is:
|
|
|
|
- the address of a gl-value that is safely derived,
|
|
|
|
- the result of pointer arithmetic on a pointer that is safely derived (with
|
|
some restrictions on the integer operand),
|
|
|
|
- the result of a comma operator where the second operand is safely derived,
|
|
|
|
- the result of a conditional operator where the selected operand is safely
|
|
derived, or
|
|
|
|
- the result of loading from a safely derived gl-value.
|
|
|
|
- A gl-value should be considered safely derived if it is:
|
|
|
|
- a dereference of a safely derived pointer,
|
|
|
|
- a member access into a safely derived gl-value, or
|
|
|
|
- a reference to a variable.
|
|
|
|
- An access to a safely derived gl-value should be guaranteed to not allow
|
|
replacement of any of the safely-derived component values at any point in the
|
|
access. "Access" should include loading a function pointer.
|
|
|
|
- Assignments should include pointer-arithmetic operators like ``+=``.
|
|
|
|
Making these guarantees will require further work, including significant new
|
|
support in LLVM IR.
|
|
|
|
Furthermore, Clang should implement a warning when assigning a data pointer that
|
|
is not safely derived to a ``__ptrauth``-qualified gl-value.
|
|
|
|
|
|
Language ABI
|
|
------------
|
|
|
|
This section describes the pointer-authentication ABI currently implemented in
|
|
Clang for the Apple arm64e target. As other targets adopt pointer
|
|
authentication, this section should be generalized to express their ABIs as
|
|
well.
|
|
|
|
Key assignments
|
|
~~~~~~~~~~~~~~~
|
|
|
|
ARMv8.3 provides four abstract signing keys: ``IA``, ``IB``, ``DA``, and ``DB``.
|
|
The architecture designates ``IA`` and ``IB`` for signing code pointers and
|
|
``DA`` and ``DB`` for signing data pointers; this is reinforced by two
|
|
properties:
|
|
|
|
- The ISA provides instructions that perform combined auth+call and auth+load
|
|
operations; these instructions can only use the ``I`` keys and ``D`` keys,
|
|
respectively.
|
|
|
|
- AArch64's TBI feature can be separately enabled for code pointers (controlling
|
|
whether indirect-branch instructions ignore those bits) and data pointers
|
|
(controlling whether memory-access instructions) ignore those bits. If TBI is
|
|
enabled for a kind of pointer, the sign and auth operations preserve the TBI
|
|
bits when signing with an associated keys (at the cost of shrinking the number
|
|
of available signing bits by 8).
|
|
|
|
arm64e then further subdivides the keys as follows:
|
|
|
|
- The ``A`` keys are used for primarily "global" purposes like signing v-tables
|
|
and function pointers. These keys are sometimes called *process-independent*
|
|
or *cross-process* because on existing OSes they are not changed when changing
|
|
processes, although this is not a platform guarantee.
|
|
|
|
- The ``B`` keys are used for primarily "local" purposes like signing return
|
|
addresses. These keys are sometimes called *process-specific* because they
|
|
are typically different between processes. However, they are in fact shared
|
|
across processes in one situation: systems which provide ``fork`` cannot
|
|
change these keys in the child process; they can only be changed during
|
|
``exec``.
|
|
|
|
Implementation-defined algorithms and quantities
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The cryptographic hash algorithm used to compute signatures in ARMv8.3 is a
|
|
private detail of the hardware implementation.
|
|
|
|
arm64e restricts constant discriminators (used in ``__ptrauth`` and
|
|
``ptrauth_blend_discriminator``) to the range from 0 to 65535, inclusive. A 0
|
|
discriminator generally signifies that no blending is required; see the
|
|
documentation for ``ptrauth_blend_discriminator``. This range is somewhat
|
|
narrow but has two advantages:
|
|
|
|
- The AArch64 ISA allows an arbitrary 16-bit immediate to be written over the
|
|
top 16 bits of a register in a single instruction:
|
|
|
|
.. code-block:: asm
|
|
|
|
movk xN, #0x4849, LSL 48
|
|
|
|
This is ideal for the discriminator blending operation because it adds minimal
|
|
code-size overhead and avoids overwriting any interesting bits from the
|
|
pointer. Blending in a wider constant discriminator would either clobber
|
|
interesting bits (e.g. if it was loaded with ``movk xN, #0x4c4f, LSL 32``) or
|
|
require significantly more code (e.g. if the discriminator was loaded with a
|
|
``mov+bfi`` sequence).
|
|
|
|
- It is possible to pack a 16-bit discriminator into loader metadata with
|
|
minimal compromises, whereas a wider discriminator would require extra
|
|
metadata storage and therefore significantly impact load times.
|
|
|
|
The string hash used by ``ptrauth_string_discriminator`` is a 64-bit SipHash-2-4
|
|
using the constant seed ``b5d4c9eb79104a796fec8b1b428781d4`` (big-endian), with
|
|
the result reduced by modulo to the range of non-zero discriminators (i.e.
|
|
``(rawHash % 65535) + 1``).
|
|
|
|
Return addresses
|
|
~~~~~~~~~~~~~~~~
|
|
|
|
The kernel must ensure that attackers cannot replace LR due to an asynchronous
|
|
exception; see `Register clobbering`_. If this is done by generally protecting
|
|
LR, then functions which don't spill LR to the stack can avoid signing it
|
|
entirely. Otherwise, the return address must be signed; on arm64e it is signed
|
|
with the ``IB`` key using the stack pointer on entry as the discriminator.
|
|
|
|
Protecting return addresses is of such particular importance that the ``IB`` key
|
|
is almost entirely reserved for this purpose.
|
|
|
|
Global offset tables
|
|
~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The global offset table (GOT) is not part of the language ABI, but it is a
|
|
common implementation technique for dynamic linking which deserves special
|
|
discussion here.
|
|
|
|
Whenever possible, signed pointers should be materialized directly in code
|
|
rather than via the GOT, e.g. using an ``adrp+add+pac`` sequence on ARMv8.3.
|
|
This decreases the amount of work necessary at load time to initialize the GOT,
|
|
but more importantly, it defines away the potential for several attacks:
|
|
|
|
- Attackers cannot change instructions, so there is no way to cause this code
|
|
sequence to materialize a different pointer, whereas an access via the GOT
|
|
always has *at minimum* a probabilistic chance to be the target of successful
|
|
`substitution attacks`_.
|
|
|
|
- The GOT is a dense pool of fixed pointers at a fixed offset relative to code;
|
|
attackers can search this pool for useful pointers that can be used in
|
|
`substitution attacks`_, whereas pointers that are only materialized directly
|
|
are not so easily available.
|
|
|
|
- Similarly, attackers can use `access path attacks`_ to replace a pointer to a
|
|
signed pointer with a pointer to the GOT if the signing schema used within the
|
|
GOT happens to be the same as the original pointer. This kind of collision
|
|
becomes much less likely to be useful the fewer pointers are in the GOT in the
|
|
first place.
|
|
|
|
If this can be done for a symbol, then the compiler need only ensure that it
|
|
materializes the signed pointer using registers that are safe against
|
|
`register clobbering`_.
|
|
|
|
However, many symbols can only be accessed via the GOT, e.g. because they
|
|
resolve to definitions outside of the current image. In this case, care must
|
|
be taken to ensure that using the GOT does not introduce weaknesses.
|
|
|
|
- If the entire GOT can be mapped read-only after loading, then no signing is
|
|
required within the GOT. In fact, not signing pointers in the GOT is
|
|
preferable in this case because it makes the GOT useless for the harvesting
|
|
and access-path attacks above. Storing raw pointers in this way is usually
|
|
extremely unsafe, but for the special case of an immutable GOT entry it's fine
|
|
because the GOT is always accessed via an address that is directly
|
|
materialized in code and thus provably unattackable. (But see `Remapping`_.)
|
|
|
|
- Otherwise, GOT entries which are used for producing a signed pointer constant
|
|
must be signed. The signing schema used in the GOT need not match the target
|
|
signing schema for the signed constant. To counteract the threats of
|
|
substitution attacks, it's best if GOT entries can be signed with address
|
|
diversity. Using a good constant discriminator as well (perhaps derived from
|
|
the symbol name) can make it less useful to use a pointer to the GOT as the
|
|
replacement in an :ref:`access path attack<Access path attacks>`.
|
|
|
|
In either case, the compiler must ensure that materializing the address of a GOT
|
|
entry as part of producing a signed pointer constant is not vulnerable to
|
|
`register clobbering`_. If the linker also generates code for this, e.g. for
|
|
call stubs, this generated code must take the same precautions.
|
|
|
|
Dynamic symbol lookup
|
|
~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
On platforms that support dynamically loading or resolving symbols it is
|
|
necessary for them to define the pointer authentication semantics of the APIs
|
|
provided to perform such lookups. While the platform may choose to reply
|
|
unsigned pointers from such function and rely on the caller performing the
|
|
initial signing, doing so creates the opportunity for caller side errors that
|
|
create :ref:`signing oracles<Signing Oracles>`.
|
|
|
|
On arm64e the `dlsym` function is used to resolve a symbol at runtime. If the
|
|
resolved symbol is a function or other code pointer the returned pointer is
|
|
signed using the default function signing schema described in
|
|
:ref:`C function pointers<C function abi>`. If the resolved symbol is not a code pointer it is
|
|
returned as an unsigned pointer.
|
|
|
|
.. _C function abi:
|
|
|
|
C function pointers
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
On arm64e, C function pointers are currently signed with the ``IA`` key without
|
|
address diversity and with a constant discriminator of 0.
|
|
|
|
The C and C++ standards do not permit C function pointers to be signed with
|
|
address diversity by default: in C++ terms, function pointer types are required
|
|
to be trivially copyable, which means they must be copyable with ``memcpy``.
|
|
|
|
The use of a uniform constant discriminator greatly simplifies the adoption of
|
|
arm64e, but it is a significant weakness in the mitigation because it allows any
|
|
C function pointer to be replaced with another. Clang supports
|
|
`-fptrauth-function-pointer-type-discrimination`, which enables a variant ABI
|
|
that uses type discrimination for function pointers. When generating the type
|
|
based discriminator for a function type all primitive integer types are
|
|
considered equivalent due to the prevalence of mismatching integer parameter
|
|
types in real world code. Type discrimination of function pointers is
|
|
ABI-incompatible with the standard arm64e ABI, but it can be used in constrained
|
|
contexts such as embedded systems or in code that does not require function
|
|
pointer interoperation with the standard ABI (e.g. because it does not pass
|
|
function pointers back and forth, or only does so through
|
|
``__ptrauth``-qualified l-values).
|
|
|
|
C++ virtual tables
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
By default the pointer to a C++ virtual table is currently signed with the
|
|
``DA`` key, address diversity, and a constant discriminator equal to the string
|
|
hash (see `ptrauth_string_discriminator`_) of the mangled v-table identifier
|
|
of the primary base class for the v-table. To support existing code or ABI
|
|
constraints it is possible to use the `ptrauth_vtable_pointer` attribute to
|
|
override the schema used for the v-table pointer of the base type of
|
|
polymorphic class hierarchy. This attribute permits the configuration of the
|
|
key, address diversity mode, and any extra constant discriminator to be used.
|
|
|
|
Virtual functions in a C++ virtual table are signed with the ``IA`` key, address
|
|
diversity, and a constant discriminator equal to the string hash (see
|
|
`ptrauth_string_discriminator`_) of the mangled name of the function which
|
|
originally gave rise to the v-table slot.
|
|
|
|
C++ dynamic_cast
|
|
~~~~~~~~~~~~~~~~
|
|
|
|
C++'s ``dynamic_cast`` presents a difficulty relative to other polymorphic
|
|
languages that have a
|
|
`top type <https://en.wikipedia.org/wiki/Any_type>` as the use of declaration
|
|
diversity for v-table pointers results in distinct signing schemas for each
|
|
isolated type hierarchy. As a result it is not possible for the Itanium ABI
|
|
defined ``__dynamic_cast`` entry point to directly authenticate the v-table
|
|
pointer of the provided object.
|
|
|
|
The current implementation uses a forced authentication of the subject object's
|
|
v-table prior to invoking ``__dynamic_cast`` to partially verify that the
|
|
object's vtable is valid. The ``__dynamic_cast`` implementation currently relies
|
|
on this caller side check to limit the substitutability of the v-table pointer
|
|
with an incorrect or invalid v-table. The subsequent implementation of the
|
|
dynamic cast algorithm is built on pointer auth protected ``type_info`` objects.
|
|
|
|
In future a richer solution may be developed to support vending the correct
|
|
authentication schema directly to the ``dynamic_cast`` implementation.
|
|
|
|
C++ std::type_info v-table pointers
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The v-table pointer of the ``std::type_info`` type is signed with the ``DA`` key
|
|
and no additional diversity.
|
|
|
|
C++ member function pointers
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
A member function pointer is signed with the ``IA`` key, no address diversity,
|
|
and a constant discriminator equal to the string hash
|
|
(see `ptrauth_string_discriminator`_) of the member pointer type. Address
|
|
diversity is not permitted by C++ for member function pointers because they must
|
|
be trivially-copyable types.
|
|
|
|
The Itanium C++ ABI specifies that member function pointers to virtual functions
|
|
simply store an offset to the correct v-table slot. This ABI cannot be used
|
|
securely with pointer authentication because there is no safe place to store the
|
|
constant discriminator for the target v-table slot: if it's stored with the
|
|
offset, an attacker can simply overwrite it with the right discriminator for the
|
|
offset. Even if the programmer never uses pointers to virtual functions, the
|
|
existence of this code path makes all member function pointer dereferences
|
|
insecure.
|
|
|
|
arm64e changes this ABI so that virtual function pointers are stored using
|
|
dispatch thunks with vague linkage. Because arm64e supports interoperation with
|
|
``arm64`` code when pointer authentication is disabled, an arm64e member
|
|
function pointer dereference still recognizes the virtual-function
|
|
representation but uses an bogus discriminator on that path that should always
|
|
trap if pointer authentication is enabled dynamically.
|
|
|
|
The use of dispatch thunks means that ``==`` on member function pointers is no
|
|
longer reliable for virtual functions, but this is acceptable because the
|
|
standard makes no guarantees about it in the first place.
|
|
|
|
The use of dispatch thunks also is required to support declaration specific
|
|
authentication schemas for v-table pointers.
|
|
|
|
C++ mangling
|
|
~~~~~~~~~~~~
|
|
|
|
When the ``__ptrauth`` qualifier appears in a C++ mangled name,
|
|
it is mangled as a vendor qualifier with the signature
|
|
``U9__ptrauthILj<key>ELb<addressDiscriminated>ELj<extraDiscriminator>EE``.
|
|
|
|
e.g. ``int * __ptrauth(1, 0, 1234)`` will be mangled as
|
|
``U9__ptrauthILj1ELb0ELj1234EE``.
|
|
|
|
If the vtable pointer authentication scheme of a polymorphic class is overridden
|
|
we mangle the override information with the vendor qualifier
|
|
``__vtptrauth(int key, bool addressDiscriminated, unsigned extraDiscriminator)``,
|
|
where the extra discriminator is the explicit value the specified discrimination
|
|
mode evalutes to.
|
|
|
|
Blocks
|
|
~~~~~~
|
|
|
|
Block pointers are data pointers which must interoperate with the ObjC `id` type
|
|
and therefore cannot be signed themselves. As blocks conform to the ObjC `id`
|
|
type, they contain an ``isa`` pointer signed as described
|
|
:ref:`below<Objc isa and super>`.
|
|
|
|
The invocation pointer in a block is signed with the ``IA`` key using address
|
|
diversity and a constant dicriminator of 0. Using a uniform discriminator is
|
|
seen as a weakness to be potentially improved, but this is tricky due to the
|
|
subtype polymorphism directly permitted for blocks.
|
|
|
|
Block descriptors and ``__block`` variables can contain pointers to functions
|
|
that can be used to copy or destroy the object. These functions are signed with
|
|
the ``IA`` key, address diversity, and a constant discriminator of 0. The
|
|
structure of block descriptors is under consideration for improvement.
|
|
|
|
Objective-C runtime
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
In addition to the compile time ABI design, the Objective-C runtime provides
|
|
additional protection to methods and other metadata that have been loaded into
|
|
the Objective-C method cache; this protection is private to the runtime.
|
|
|
|
Objective-C methods
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
Objective-C method lists sign methods with the ``IA`` key using address
|
|
diversity and a constant discriminator of 0. Using a uniform constant
|
|
discriminator is believed to be acceptable because these tables are only
|
|
accessed internally to the Objective-C runtime.
|
|
|
|
Objective-C class method list pointer
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The method list pointer in Objective-C classes are signed with the ``DA`` key
|
|
using address diversity, and a constant discriminator of 0xC310.
|
|
|
|
Objective-C class read-only data pointer
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The read-only data pointer in Objective-C classes are signed with the ``DA`` key
|
|
using address diversity, and a constant discriminator of 0x61F8.
|
|
|
|
.. _Objc isa and super:
|
|
|
|
Objective-C ``isa`` and ``super`` pointers
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
An Objective-C object's ``isa`` and ``super`` pointers are both signed with
|
|
the ``DA`` key using address diversity and constant discriminators of 0x6AE1
|
|
and 0x25DA respectively.
|
|
|
|
Objective-C ``SEL`` pointers
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
By default, the type of an Objective-C instance variable of type ``SEL``, when
|
|
the qualifiers do not include an explicit ``__ptrauth`` qualifier, is adjusted
|
|
to be qualified with ``__ptrauth(ptrauth_key_asdb, 1, 0x57C2)``.
|
|
|
|
This provides a measure of implicit at-rest protection to Objective-C classes
|
|
that store selectors, as in the common target-action design pattern. This
|
|
prevents attackers from overriding the selector to invoke an arbitrary different
|
|
method, which is a major attack vector in Objective-C. Since ``SEL`` values are
|
|
not normally passed around as signed pointers, there is a
|
|
:ref:`signing oracle<Signing Oracles>` associated with the initialization of the
|
|
ivar, but the use of address and constant diversity limit the risks.
|
|
|
|
The implicit qualifier means that the type of the ivar does not match its
|
|
declaration, which can cause type errors if the address of the ivar is taken:
|
|
|
|
.. code-block:: ObjC
|
|
|
|
@interface A : NSObject {
|
|
SEL _s;
|
|
}
|
|
@end
|
|
|
|
void f(SEL *);
|
|
|
|
@implementation A
|
|
-(void)g
|
|
{
|
|
f(&_s);
|
|
}
|
|
@end
|
|
|
|
To fix such an mismatch the schema macro from `<ptrauth.h>`:
|
|
|
|
.. code-block:: ObjC
|
|
|
|
#include <ptrauth.h>
|
|
|
|
void f(SEL __ptrauth_objc_sel*);
|
|
|
|
or less safely, and introducing the possibility of an
|
|
:ref:`signing or authentication oracle<Signing oracles>`, an unauthencaticated
|
|
temporary may be used as intermediate storage.
|
|
|
|
Alternative implementations
|
|
---------------------------
|
|
|
|
Signature storage
|
|
~~~~~~~~~~~~~~~~~
|
|
|
|
It is not critical for the security of pointer authentication that the
|
|
signature be stored "together" with the pointer, as it is in Armv8.3. An
|
|
implementation could just as well store the signature in a separate word, so
|
|
that the ``sizeof`` a signed pointer would be larger than the ``sizeof`` a raw
|
|
pointer.
|
|
|
|
Storing the signature in the high bits, as Armv8.3 does, has several trade-offs:
|
|
|
|
- Disadvantage: there are substantially fewer bits available for the signature,
|
|
weakening the mitigation by making it much easier for an attacker to simply
|
|
guess the correct signature.
|
|
|
|
- Disadvantage: future growth of the address space will necessarily further
|
|
weaken the mitigation.
|
|
|
|
- Advantage: memory layouts don't change, so it's possible for
|
|
pointer-authentication-enabled code (for example, in a system library) to
|
|
efficiently interoperate with existing code, as long as pointer
|
|
authentication can be disabled dynamically.
|
|
|
|
- Advantage: the size of a signed pointer doesn't grow, which might
|
|
significantly increase memory requirements, code size, and register pressure.
|
|
|
|
- Advantage: the size of a signed pointer is the same as a raw pointer, so
|
|
generic APIs which work in types like `void *` (such as `dlsym`) can still
|
|
return signed pointers. This means that clients of these APIs will not
|
|
require insecure code in order to correctly receive a function pointer.
|
|
|
|
Hashing vs. encrypting pointers
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Armv8.3 implements ``sign`` by computing a cryptographic hash and storing that
|
|
in the spare bits of the pointer. This means that there are relatively few
|
|
possible values for the valid signed pointer, since the bits corresponding to
|
|
the raw pointer are known. Together with an ``auth`` oracle, this can make it
|
|
computationally feasible to discover the correct signature with brute force.
|
|
(The implementation should of course endeavor not to introduce ``auth``
|
|
oracles, but this can be difficult, and attackers can be devious.)
|
|
|
|
If the implementation can instead *encrypt* the pointer during ``sign`` and
|
|
*decrypt* it during ``auth``, this brute-force attack becomes far less
|
|
feasible, even with an ``auth`` oracle. However, there are several problems
|
|
with this idea:
|
|
|
|
- It's unclear whether this kind of encryption is even possible without
|
|
increasing the storage size of a signed pointer. If the storage size can be
|
|
increased, brute-force attacks can be equally well mitigated by simply storing
|
|
a larger signature.
|
|
|
|
- It would likely be impossible to implement a ``strip`` operation, which might
|
|
make debuggers and other out-of-process tools far more difficult to write, as
|
|
well as generally making primitive debugging more challenging.
|
|
|
|
- Implementations can benefit from being able to extract the raw pointer
|
|
immediately from a signed pointer. An Armv8.3 processor executing an
|
|
``auth``-and-load instruction can perform the load and ``auth`` in parallel;
|
|
a processor which instead encrypted the pointer would be forced to perform
|
|
these operations serially.
|