Return-oriented programming

Features like the execution permission that we described have made it increasingly difficult to execute arbitrary code. This means that attackers use other approaches like Return Oriented Programming (ROP). ROP takes advantage of the scale of the software stack in many modern systems. An attacker analyzes the software in a system, looking for gadgets. A gadget is a useful fragment of code, usually ending with a function return, for example:

...
ADD x0, x1, x2
RET

This code provides a gadget for adding two registers together. By scanning all the available libraries, an attacker can build a library of gadgets. These gadgets are existing legal code, within executable regions. This means that they are not affected by protections like execution permissions. The attacker strings together a chain of gadgets, forming what is effectively a new program, made up of existing code fragments. You can see an example in the following diagram:

Any library that is available in the address space for the process is a potential source of gadgets. For example, the C library contains many functions, each offering potential gadgets. With so many gadgets available, statistically enough gadgets are available to form any arbitrary new program. Some compilers are even designed to compile to gadgets, rather than assembler. An ROP attack is effective, because it is made up of existing legal code, so it is not trapped by execution permissions or checks on executing from writable memory.

It is time-consuming for an attacker to find gadgets and create the sequence that is necessary to produce a new program. However, this process can be automated and can be reused to attack multiple systems. Address Space Randomization (ASLR) can help prevent the practice of automated and multiple attacks.

Pointer authentication

Armv8.3-A introduces the option of pointer authentication. Pointer authentication can mitigate against ROP attacks.

Pointer authentication takes advantage of the fact that pointers are stored in a 64-bit format, but not all those bits are needed to represent the address. The following diagram shows the virtual address space layout:

 

You can see that there are potentially two 252 byte address ranges, one at the top of the address space, and one at the bottom of the address space:

Bottom Range: 0x0000_0000_0000_0000 - 0x000F_FFFF_FFFF_FFFF

Top Range: 0xFFF0_0000_0000_0000 - 0xFFFF_FFFF_FFFF_FFFF

Any address that falls outside of both ranges is always invalid and results in a fault if accessed.

Note: Before the release of Armv8.1-A, the maximum size of each range was 248.

You can see that any valid virtual address will have its top 12 bits as 0x000 or 0xFFF. When pointer authentication is enabled, the upper bits are used to store a signature and are not treated as part of the address. This signature is referred to as a Pointer Authentication Code (PAC).

The PAC uses the top bits of the pointer. Bit[55] is reserved to indicate whether the top or bottom region is being accessed. This is illustrated here:

The exact number of bits that are available for the PAC depends on the configured size of the virtual address space, and on whether tagged pointers are enabled. The smaller the virtual address space, the more bits that are available.

To protect against ROP attacks, at the start of a function the return address in the LR is signed. This means that a PAC is added in the upper order bits of the register. Before returning, the return address is authenticated using the PAC. If the check fails, an exception is generated when the address is used for a branch. The following diagram shows an example:

This change makes ROP attacks much harder to launch. This is because, to form the chain of gadgets, the attacker needs to know the location of those gadgets, and correctly signed pointers to those locations.  To get a signed pointer it would need access to signing gadget.

How is the PAC formed?

The architecture provides five 128-bit keys. Each key is stored in a pair of 64-bit System registers:

  • Two keys, A and B, for instruction pointers
  • Two keys, A and B, for data pointers
  • One key for general use

The registers that store these keys are only accessible at EL1 and above.

For data and instruction addresses, the instruction used to create and check the PAC specifies whether the A key or the B key is used. For a particular pointer, the instruction that generates the PAC and the instruction that authenticates the PAC must agree on which key to use.

The signature is formed from the address itself, the key, and a modifier, as you can see here:

The architecture allows different implementations, for example from different vendors, to use different encryption algorithms. The recommended algorithm is QARMA, which is required by SBSA level 5. ID_AA64ISAR1_EL1 reports which algorithm is supported on a specific processor.

The instructions that generate and authenticate the PAC specify whether the modifier is another processor register or is 0. The modifier needs to be a value which will be the same on entry and exit if the function is called correctly. For example, the Stack Pointer (SP) can have a different value every time that a function is called but will have the same value at the start and at the end of a given call. Using the SP as a modifier gives you a PAC that is only valid for that call of the function. This is because the SP will probably be in a different location on future calls.

The limited size of the PAC means that the strength of the signature is potentially low, depending on the size of the configured virtual address size. However, the keys are typically of limited life span. Each running application can use different keys, and a given application can be given different keys each time that it is launched. When forming a chain of gadgets, the attacker must get every pointer correct, otherwise an exception will be raised.

How is the PAC checked?

Before use, the pointer must be authenticated. The authentication process is shown in this diagram:

The authentication operation regenerates the PAC and compares it with the value that is stored in the pointer. If authentication succeeds, a pointer without the PAC is returned. If authentication fails, an invalid pointer is returned. This means that an exception is raised if the pointer is used.

New instructions

To support pointer authentication, new instructions are added to A64. Let’s look at some examples of the operations that are related to the instruction pointers:

PACIxSP - Sign LR using SP as the modifier.

PACIxZ - Sign LR using 0 as the modifier.

PACIx - Sign Xn using a general-purpose register as modifier.

 

AUTIxSP - Authenticate LR using SP as the modifier.

AUTIxZ -Authenticate LR using 0 as the modifier.

AUTIx - Authenticate Xn using a general-purpose register as modifier.

 

BRAx - Indirect branch with pointer authentication.

AUTIxZ - Indirect branch with link, with pointer authentication.

 

RETAx - Function return with pointer authentication.

ERETAx - Exception return with pointer authentication.

In each case, replace x with A or B to select the wanted key.

The preceding list is not complete, but it shows the type of operations that are available. You can refer to the Arm ARM for a complete list and detailed descriptions.

Use of the NOP space

Some of the new authentication instructions are in the NOP space. Applications or libraries that protect themselves with these NOP-space instructions can run on older processors without pointer authentication support. Although the older processors will not benefit from the protections, this can be very useful in heterogeneous systems, as you can see in the following diagram:

Note: To provide backwards compatibility, this program uses separate instructions to authenticate the LR and return. Ideally the combined authenticate and return instructions, RETAx, would be used. However, the RETAx instruction does not use the NOP instruction space. This means that it is not compatible with a processor that does not support authentication.

Enabling pointer authentication

Pointer authentication is controlled by Exception level using SCTLR_ELx. SCTLR_ELx uses separate controls for instruction checking and for data checking:

  • EnIx -Enables instruction pointer authentication using key x.
  • EnDx -Enables data pointer authentication using key x.
Previous Next