PACStack: an Authenticated Call Stack
Abstract
A popular run-time attack technique is to compromise the control-flow integrity of a program by modifying function return addresses on the stack. So far, shadow stacks have proven to be essential for comprehensively preventing return address manipulation. Shadow stacks record return addresses in integrity-protected memory secured with hardware-assistance or software access control. Software shadow stacks incur high overheads or trade off security for efficiency. Hardware-assisted shadow stacks are efficient and secure, but require the deployment of special-purpose hardware.
We present authenticated call stack (ACS), an approach that uses chained message authentication codes (MACs). Our prototype, PACStack, uses the ARM general purpose hardware mechanism for pointer authentication (PA) to implement ACS. Via a rigorous security analysis, we show that PACStack achieves security comparable to hardware-assisted shadow stacks without requiring dedicated hardware. We demonstrate that PACStack’s performance overhead is small (3%).
1 Introduction
Traditional code-injection attacks are ineffective in the presence of WX policies that prevent the modification of executable memory [49]. However, code-reuse attacks can alter the run-time behavior of a program without modifying any of its executable code sections. Return-oriented programming (ROP) is a prevalent attack technique that corrupts function return addresses to hijack a program’s control flow. ROP can be used to achieve Turing-complete computation by chaining together existing code sequences in the victim program. To prevent ROP, return addresses must be protected when stored in memory. At present, the most powerful protection against ROP is using an integrity-protected shadow stack that maintains a secure reference copy of each return address [1]. Integrity of the shadow stack is ensured by making it inaccessible to the adversary either by randomizing its location in memory or by using specialized hardware [29]. Recent software-based shadow stacks show reasonable performance [10], but are vulnerable to an adversary capable of exploiting memory vulnerabilities to infer the location of the shadow stack. To date, only hardware-assisted schemes, such as Intel CET [29], achieve negligible overhead without trading off security. But employing such a custom hardware mechanism incurs development and deployment costs.
Recent ARM processors include support for pointer authentication (PA); a hardware extension that uses tweakable message authentication codes (MACs) to sign and verify pointers [4]. One initial use case of PA is the authentication of return addresses [45]. However, current PA schemes are vulnerable to reuse attacks, where the adversary can reuse previously observed valid protected pointers [35]. Prior work [45, 35] and current implementations by GCC 11 1 https://gcc.gnu.org/onlinedocs/gcc/AArch64-Function-Attributes.html and LLVM 22 2 https://reviews.llvm.org/D49793 mitigate reuse attacks, but cannot completely prevent them.
In this paper, we propose a new approach, authenticated call stack (ACS), providing security comparable to hardware-assisted shadow stacks, with minimal overhead and without requiring new hardware-protected memory. ACS binds all return addresses into a chain of MACs that allow verification of return addresses before their use. We show how ACS can be efficiently realized using ARM PA while resisting reuse attacks. The resulting system, PACStack, can withstand strong adversaries with full memory access. Our contributions are:
- •
ACS, a new approach for precise verification of function return addresses by chaining MACs (Section 4).
- •
PACStack, an LLVM-based realization of ACS using ARM PA without requiring additional hardware (Section 5).
- •
A systematic evaluation of PACStack security, showing that its security is comparable to shadow stacks (Section 6).
- •
Demonstrating that the performance overhead of PACStack is small (3%) (Section 7).
PACStack and associated evaluation code is available as open source at https://pacstack.github.io.
2 Background
2.1 ROP on ARM
In ROP, the adversary exploits a memory vulnerability to manipulate return addresses stored on the stack, thereby altering the program’s backward-edge control flow. ROP allows Turing-complete attacks by chaining together multiple gadgets, i.e., adversary-chosen sequences of pre-existing program instructions that together perform the desired operations. ARM architectures use the link register () to hold the current function’s return address. is automatically set by the branch with link (bl) or branch with link to register (blr) instructions that are used to implement regular and indirect function calls. Because is overwritten on call, non-leaf functions must store the return address onto the stack. This opens up the possibility of ROP on ARM [30].
2.2 ARM Pointer Authentication
The ARMv8.3-A PA extension supports calculating and verifying pointer authentication codes (PACs) [4]. PA is at present deployed in the Apple A12, A13, S4, and S5 systems-on-chip (SoCs) and is going to be available in all upcoming ARMv8.3-A and later SoCs. A pac instruction calculates a keyed tweakable MAC, , over the address of a pointer using a 64-bit modifier as the tweak. The resulting authentication token, referred to as a PAC, is embedded into the unused high-order bits of . It can be verified using an aut instruction that recalculates , and compares the result to ’s PAC .
Since the PAC is stored in unused bits of a pointer, its size is limited by the virtual address size (VA_SIZE in Figure 1) and whether address tagging is enabled [4]. On a 64-bit ARM machine running a default Linux kernel, VA_SIZE is 39, which leaves 16 bits for the PAC when excluding the reserved and address tag bits. PA provides five different keys; two for code pointers, two for data pointers, and one for generic use. Each key has a separate set of instructions, e.g., the autia and pacia instructions always operate on the instruction key , stored in the APIAKey_EL1 register. Access to the key registers and PA configuration registers can be restricted to a higher exception level (EL) . Linux 33 3 https://kernelnewbies.org/Linux_5.0#ARM_pointer_authentication adds full support for PA, such that the kernel (at EL 1) manages user-space (EL 0) keys and prevents EL 0 from modifying them. The kernel generates new PA keys for a process on an exec system call.
As currently specified, PA does not cause a fault on verification failure; instead, it strips the PAC from the pointer and flips one of the high-order bits such that becomes invalid. If the invalid pointer is used by an instruction that causes the pointer to be translated, such as load or instruction fetch, the memory management unit issues a memory translation fault.
2.2.1 PA-based return address protection
PA-based return address protection is implemented as part of the -mbranch-protection feature of GCC and LLVM/Clang.
44
4
https://gcc.gnu.org/gcc-9/changes.html and
https://reviews.llvm.org/D51429
An authenticated return address is computed with paciasp (❶ in Listing ) and verified with retaa (❹).
These instructions use the instruction key and the value of stack pointer () as the modifier.
The PA-keys are protected by hardware; consequently an adversary has to resort to guessing the correct PAC for a modified return address.
The -mbranch-protection feature and other prior PA-based solutions are vulnerable to reuse attacks where an adversary replaces a valid authenticated return address with another authenticated return address previously read from the process’ memory. For a reused PAC to pass verification, both the original and replacement PAC must have been computed using the same PA key and modifier. This applies to any PA scheme, not only authenticated return addresses. Using the value as a modifier reduces the set of interchangeable pointers, but still allows reuse attacks when values coincide. Reuse attacks can be mitigated, but not completely prevented, by further narrowing the scope of modifier values [35].
3 Adversary model and requirements
In this work, we consider a powerful adversary, , with arbitrary control of process memory but restricted by a WX policy that prevents modification of code pages. This adversary model is consistent with prior work on run-time attacks [49]. We limit to user space; thus cannot read or modify kernel-managed registers such as the PA keys.
We make the following assumptions about the system:
- A1
A WX policy protects code memory pages from modification by non-privileged processes. All major processor architectures, including ARMv8-A, support WX.
- A2
Coarse-grained forward-edge control-flow integrity (CFI) that restricts forward control-flow transfers to a set of valid targets. Specifically, we assume that indirect function-calls always target the beginning of a function and that indirect jumps to arbitrary addresses is infeasible. This property is satisfied by several pre-existing software-only CFI solutions with reasonable overhead [1, 18, 31, 37], as well as with negligible overhead by using hardware-assisted mechanisms like ARM PA [35], branch target indicators [4], or TrustZone-M [39, 5]. In particular, a minimal PA scheme using a constant (e.g., 0x0) modifier fulfills this assumption.
This adversary model allows to modify any pointer in data memory pages. In particular, can modify function return addresses while they reside on the program call stack. A2 and A1 prevent from tampering with ACS instrumentation (Section 6.3). Our goal is to thwart who modifies function return addresses in order to hijack the program control flow. We define the following requirements:
- R1
Return address integrity: Detect if a function return address has been modified while in memory.
- R2
Memory disclosure tolerance: Remain effective even when can read the entire process address space.
- R3
Compatibility: Be applicable to typical (standard-compliant) C code without source code modifications.
- R4
As in prior work on CFI, we do not consider non-control data attacks [12], such as data-oriented programming (DOP) [27].
4 Design: authenticated call stack
In this section we present our general design for an authenticated call stack (ACS). In Section 5, we present our implementation that efficiently realizes ACS using ARM PA. Our key idea is to provide a modifier for the return address by cryptographically binding it to all previous return addresses in the call stack. This makes the modifier statistically unique to a particular control-flow path, thus preventing reuse-type attacks and allowing precise verification of return addresses. The return addresses (where is the depth of the call stack in terms of active function records) must be stored on the stack, where can modify them by exploiting memory vulnerabilities. ACS protects these values by computing a series of chained authentication tokens that cryptographically bind the last to all return addresses stored on the stack (Figure 2). Only the MAC key and the last authentication token must be stored securely to ensure that previous tokens and return addresses can be correctly verified when unwinding the call stack (R1). We use a tweakable MAC function to generate a -bit authentication token :
is maintained in a register unmodifiable by . Figure 3 shows how authentication tokens and return addresses are stored on the call stack. On function calls, is retained across the call to the callee, which calculates and stores both and the corresponding return address on its stack frame. On return, and values are loaded from the stack and are verified by comparing to . If the results differ, then one or both of the loaded values have been corrupted (R1). Otherwise, they are valid—i.e., and —in which case is replaced with the verified in the secure register before the function returns to .
For compactness, we can combine and , into an authenticated return address, :
We call and the corresponding valid if for some given .
4.1 Securing the authentication token
The current authenticated return address , is secured by keeping it exclusively in a CPU register which we call the chain register () . Note that reserving exclusive use of a register is also a requirement for current shadow stack implementation for the 64-bit ARM architecture [14] and has been proposed for shadow stacks on the x86 architecture [10].
ACS protects the integrity of backward-edge control-flow transfers. Combined with coarse-grained forward-edge CFI (Assumption A2), it ensures that: 1) immediately after function return, the in is valid, 2) at function entry the stored in is valid, and 3) is always used as or set to a valid . This ensures that token updates are done securely, and that the ACS instrumentation cannot be bypassed or used to generate arbitrary authenticated return addresses.
4.2 Mitigating hash-collisions
Though is protected by hardware, the size of the authentication token can be limited by the implementation. Using a PAC as the token would typically limit it to 16 bits. This is significant, as collisions can be found after has seen, on average, approximately tokens [47, Section 1.4.2] (e.g., 321 tokens for ). Despite this, we can still prevent from recognizing collisions (R2), thus forcing to guess—with a success probability —which authenticated return addresses yield a collision. The of any stored on the stack is masked using a pseudo-random value derived from the previous value:
The mask is exclusive-OR-ed with after it is generated and before it is authenticated, thereby preventing from identifying opportunities for pointer reuse. We discuss the security of masking in Section 6.2.1.
4.3 Mitigating brute-force guessing
A brute force attack where guesses an token succeeds with probability for a -bit after guesses, provided that a failed guess terminates the program and subsequent program runs use a new key to generate tokens. This assumption is similar to prior PA-based solutions [35] and is consistent with current PA behavior in Linux 5.0. However, if pre-forked or multithreaded programs share the key, can target a vulnerability in a sibling. Unless a failed authentication terminates the entire process tree, can then attempt a new guess against another sibling process without resetting the key. In this scenario, guesses on average are enough to obtain a modifier with respect to which some combination of pointer and authentication token is valid. Since this modifier becomes the next authenticated return address, the process can be repeated to use the injected address. Because the two guesses can be done separately using a divide-and-conquer strategy, this requires on average guesses to allow to jump to an arbitrary address, rather than that are needed when the guesses are independent.
Liljestrand et al. [35] recommend hardening pre-forking and multi-threaded applications against guessing attacks by having the application restart all of its processes if the number of PAC failures in child processes exceeds a pre-defined threshold. We recommend an alternative mitigation specific to ACS: "re-seeding" the calculation after a fork or thread creation. For example, calculating where corresponds to the process or thread ID. This solution is straightforward to apply to threads, as a return from the function starting the thread causes the thread to exit. Crucially, re-seeding prevents a divide-and-conquer guessing strategy and requires on average guesses. Therefore, the ACS for the thread stacks can be made disjoint from the main ACS chain. However, forked processes may use tokens in stack frames inherited from the parent process. If a child process never returns to inherited stack frames, re-seeding any new tokens beyond the point of the fork is sufficient. However, if the child process returns to inherited stack frames, the ACS must be re-seeded starting from by rewriting any tokens in pre-existing stack frames; similar to some stack canary re-randomization schemes [43, 25].
4.4 Irregular stack unwinding
The C standard includes the setjmp / longjmp programming interface, which can be used to add exception-like functionality to C. The longjmp C function executes a non-local jump to a prior calling environment stored using the setjmp function. At setjmp, callee-saved registers (whose values are guaranteed to persist through function invocations), as well as the stack pointer , and the return address are stored in the given jmp_buf buffer. Calling longjmp using an expired buffer, i.e., after the corresponding setjmp caller has returned, results in undefined behavior (the implications of this are discussed in Section 9.1). Because jmp_buf also stores the last authenticated token, ACS needs a mechanism to ensure its integrity when using setjmp and longjmp.
While in memory, the integrity of jmp_buf cannot be guaranteed. Nonetheless, the stored is bound to the corresponding on the setjmp caller’s stack. This ensures that longjmp always restores a valid ACS state. To limit the set of values can inject into jmp_buf, we replace the setjmp return address in jmp_buf with , defined as:
where is the value stored in jmp_buf. When executing longjmp, is recalculated based on the buffer values to verify that the stored was stored by a setjmp. cannot generate the value for an arbitrary , nor replace with a previously observed . But, since longjmp explicitly allows jumping to prior states, ACS cannot ensure that the target is the intended one, i.e., could substitute the correct jmp_buf with another. Shadow stacks share a similar limitation [17], and cannot guarantee that the intended state has been reached, only that the return address (and stack pointer) in that state is intact.
5 Implementation: PACStack
We present PACStack, an ACS realization using ARMv8.3-A PA. PACStack is based on LLVM 9.0 and integrated into the 64-bit ARM backend. PACStack modifies the AArch64FrameLowering such that the function stores and loads during FrameSetup and FrameDestroy, respectively. We also modify the AArch64RegisterInfo to ensure that the register holding , chain register (), is reserved for PACStack use. Our current implementation uses a Intermediate Representation (IR) pass to mark all functions for instrumentation, whereas the backend then performs instrumentation based on the function attribute.
The current authenticated return address is securely stored in . Because the unprotected return address is never stored on the stack, is limited to manipulating the earlier authenticated return addresses on stack, i.e., . An authenticated return address must therefore pass two authentications before use: first when being restored from the stack, and second, when being used as the target of a function return. We discuss the security implications in Section 6.
PACStack uses the and instructions to efficiently calculate and verify authenticated return addresses (Listing , ➂ and ➅). The result of is which is stored in the link register (, ➂) and moved to (➃):
The corresponding verification (➄ and ➅) are defined as:
where will automatically handle verification errors by setting to an unusable address . No additional checking is needed; executing a return to causes a address translation fault (Section 2.2). To maintain compatibility (R3), PACStack does not modify the frame record (➁) and instead stores in a separate stack slot (➀). This allows, for instance, debuggers to backtrace the call-stack without knowledge of PACStack. PACStack never loads from the frame record; it always uses which is securely stored in .
5.1 Securing the authentication token
PACStack uses the ARM general purpose register X28 as for storing the last authentication token. X28 is a callee-saved register, and so, any function that uses it must also restore the old value before return. By using X28 as , PACStack libraries or code can be transparently mixed with uninstrumented code (R3). We discuss the security implications of mixing instrumented and uninstrumented code in Section 9.2.
5.2 Mitigating hash collisions: PAC masking
To prevent from identifying PAC collisions that can be reused to violate the integrity of the call stack, PACStack masks all authentication tokens values before storing them on the stack. A pseudo-random value is obtained by generating a PAC for address 0x0, (Listing ❶, ❺). By using we efficiently obtain a pseudo-random value that can be directly applied to the authentication token part of using only an exclusive-or instruction (eor ❷, ❻).
Because this construction uses the same key to generate both authentication tokens and masks, must not obtain an for a and any existing . PACStack will never generate such values, as the return address never points to memory address zero. To prevent leaking the mask directly, it is cleared after use (❸, ❼). Consequently no value is visible to nor is it possible to pre-compute without the confidential PA key.
This approach to masking requires two additional PAC calculations for each function activation. PACStack supports instrumentation with or without masking. We discuss the security of PAC masking in Section 6.2.1.
5.3 Irregular stack unwinding
PACStack binds jmp_buf buffers to the at the time of setjmp call by replacing the setjmp return address with its authenticated counterpart before setjmp stores it to the jmp_buf (Section 4.4). The libc implementation is not modified; instead setjmp / longjmp calls are replaced with the wrapper functions in Listings and .
The setjmp_wrapper (Listing ) replaces the return address in with and then executes setjmp, which stores it in the buffer. The longjmp_wrapper (Listing ) retrieves , , and the values from jmp_buf, verifies their values and writes into jmp_buf before executing longjmp.
5.4 Multi-threading
The values of ARMv8-A general purpose registers are stored in memory when entering EL 1 (i.e. kernel-mode) from EL 0 (i.e. user-mode), for example during context switches and system calls. This must not allow to modify the values or read the mask, which are both exclusively in either or during execution (Listings and ), but must be stored in memory during the context switch. On ARMv8-A, system calls are implemented using the supervisor call instruction (svc) that switches the CPU to EL 1 and triggers a configured handler. On 64-bit ARM, Linux uses the kernel_entry55 5 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/arm64/kernel/entry.S?h=v5.0 macro to store all register values on the EL 1 stack, where they cannot be accessed by user-space processes. During context switches, callee-saved registers (including ) and are stored in struct cpu_context66 6 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/arm64/include/asm/processor.h?h=v5.0 which belongs to the in-kernel task structure and cannot be accessed by user space. The and values of a non-executing task are thus securely stored within the kernel, beyond the reach of other processes or other threads within the same process. Thus, no kernel modifications are needed to securely apply PACStack to multi-threaded applications.
6 Security evaluation
We address three questions in this section:
3) Do ACS’s guarantees hold when instantiated as PACStack?
6.1 Reuse attacks on PA
Reuse attack on PA-based schemes are possible when the modifier is calculated with known or predictably repeating values. Using the can mitigate reuse attacks (Section 2.2.1). However, -mbranch-protection generates the PAC immediately on function entry, before modifying the value to allocate stack space. All functions called from within a code segment use the same modifier unless there are dynamic stack allocations. Moreover, because the stack is typically aligned to 8 bytes, the value will often repeat. For example, a less than test execution of a SPEC CPU 2017 benchmark (538.imagick_r) already shows multiple collisions, with distinct (,) pairs, but only unique values. shows a minimal example where all called functions will end up using the same modifier and thus have interchangeable signed return addresses.
6.2 ACS security
A generic representation of an attack against ACS is shown in Figure 4. Under normal operation, function returns to if called from (Figure 4(a)); i.e., when called from , the return address of is an address in . The goal of (Figure 4(b)) is to cause to return to some other address .
Since the authenticated return address containing is protected from ,
in order to perform a backward-edge control-flow attack, must achieve two
goals successfully:
AG-Jump: Obtain an authenticated return address , valid with
respect to some known modifier, which will validate successfully when
returns.
AG-Load: Violate the integrity of the call stack such that
the register is loaded with from AG-Jump rather than the
correct authenticated return address
.
This requires two returns: one from a ‘loader’ function to load ’s into , and another from to the return address contained in .

In the analyses below, we treat the token P as a random oracle with respect to both the pointer and modifier . This means that if has never been computed by a function call, will match any value with probability , independently of any other value . In the analysis below we assume that programs that share the same PA keys between multiple processes or threads employ the mitigation strategy against brute-force attacks described in Section 4.3. This assumption and the design of ACS ensure that there is no authentication oracle available: the only way to test whether an token is valid with respect to some address and modifier is to attempt to return using the address and token, triggering a crash if the token is incorrect. The difficulty of achieving these goals therefore depends on whether ’s desired control-flow violation follows the call graph of the program and whether tokens are masked. Violating control-flow integrity while still traversing the call graph is easier because this allows to harvest tokens and search for collisions; violations that do not follow the call graph are more difficult because they require that make one or more guesses, risking a crash.
6.2.1 Violations that follow the call graph
As can harvest authenticated return pointers when they are written to the stack, the short tokens mean that in the absence of masking an attacker can violate the integrity of the call stack by finding collisions in .
In order to achieve goal AG-Load, must find two authenticated return addresses and , such that
i) they are both returned to by a function , ii) that contains a call-site to the loader function with a corresponding return address , and iii) such that| (1) |
Note that the collisions must be for different values in the second argument only, since that is the value in ’s control. Collisions that require different values for cannot be exploited because is in and cannot be modified by .
The tokens contained in and depend on the path that has taken through the call graph. can obtain as many tokens with as a pointer as there are distinct execution paths leading to . The number of such paths will explode combinatorially as the complexity of the program increases, and cycles in the call graph—as occur in Figure 4—make the number of paths essentially infinite, limited only by available stack space.
Having found such a collision, then arranges for function to be called, traversing the call graph in such a way that it is set up to return to using . Then, when the function calls into the loader function, it will set to . When the loader function returns to , it will attempt to load from the stack. Instead, substitutes , which because of (1) will validate correctly when returning to . Since is a valid authenticated return address, will successfully return to , thereby violating the integrity of the call stack.
More concretely, after collecting tokens, according to the birthday paradox [47, Section 1.4.2], the probability that some pair collides is:
This quickly approaches as collects more tokens, on average occurring after obtaining
tokens. With a -bit PAC, will therefore obtain a collision after harvesting pointers on average.
In order to successfully mount the above attack, must find two colliding tokens and perform the substitution. Without masking, can read the token from the stack. can then keep collecting tokens until they find two that collide; since these are both valid pointers, will always succeed once this occurs, thus
With masking cannot identify token collisions: and have different mask values and . Therefore it is impossible to identify a collision with a probability greater than by random selection. This means that will succeed in the attack above with a probability of . We give a detailed proof in Appendix A.
In practice, this means that can use this attack to traverse the program’s call graph, but cannot jump to an address that is not a valid return address for function .
6.2.2 Violations that leave the call graph
We now consider ’s probability of success when attempting to return to an address in a way that that does not follow the program’s call graph. (Summary in Table 1.)
In this case, the path from to has not been traversed, and the instrumentation has never before computed the token . Therefore, succeeds at AG-Load—i.e., —with probability , irrespective of whether the substituted is a valid authenticated return address. On failure, which has probability , the process will crash.
’s probability of then achieving goal AG-Jump depends on whether is the return address of a valid call-site. If it is, then can obtain a valid authenticated return pointer for that location in the same way as in Section 6.2.1. If has never been used as a return address, then no token has ever been generated for that pointer and AG-Jump is achieved with probability at most , independent of AG-Load.
can therefore succeed with probability when the return address is a valid call-site return address, or with probability of when the return address is not.
| Violation type | No masking | Masking |
|---|---|---|
| On-graph | ||
| Off-graph to call-site | ||
| Off-graph to arbitrary address |
6.3 Run-time attack resistance of PACStack
PACStack must ensure the integrity of and the confidentiality of the masks. The former is achieved by storing in , which is reserved for this purpose, not used by regular code, and hence, inaccessible to (Section 5.1). The latter is maintained as the mask is re-generated each time it is needed and cleared after use (Section 5.2). This holds true also in multi-threaded environments (Section 5.4). Traditional CFI solutions are unable to withstand control-flow bending [11]: attacks where each control-flow transfer follows the program’s CFG, but the program execution trace conforms to no feasible benign execution trace. Schemes like PACStack and shadow call stacks are not susceptible to backward-edge control-flow bending because they precisely protect the integrity of the return addresses. cannot trick PACStack to deviate from an expected return flow by replacing with a valid, but outdated value, because PACStack never writes onto the stack. also cannot reliably exploit PAC collisions to replace part of the chain, as each is masked. cannot tamper with the instrumentation itself by modifying the instructions in memory (Assumption A1). By requiring coarse-grained forward-edge CFI (Assumption A2), PACStack ensures that token calculations and masking are executed atomically and cannot be used to manipulate , or the mask during the function prologue and epilogue. This holds when the forward-edge CFI is susceptible to control-flow bending (Section 3).
6.3.1 Tail calls and signing gadgets
A recent discovery by Google Project Zero [8] shows that PA schemes can be vulnerable to an attack whereby specific code sequences can be used as gadgets to generate PACs for arbitrary pointers. Recall that on PAC verification failure an aut instruction removes the PAC, but corrupts a well-known high-order bit such that the pointer becomes invalid. If a pac instruction adds a PAC to a pointer with corrupt high-order bits, it treats the high-order bits as though they were correct when calculating the new PAC, and flips a well-known bit of the PAC if any high-order bit was corrupt. This means that instruction sequences such as the one shown in Listing , consisting of an aut instruction followed by a pac instruction, can be used generate a valid PAC for a pointer even if the original pointer is not valid to begin with. writes an arbitrary pointer to memory (❶) and allows it to be verified. When verification fails, autia removes the PAC, and corrupts the high-order bit in , writing the resulting to the destination register (❷). The subsequent pacia will add the correct PAC for , then flip bit of the PAC to indicate that the input pointer was invalid (❸). can now flip bit back (❺) in order to obtain the correct PAC for pointer (❻).
The PA signing gadget requires finding a matching pair operating on pointer in the code without any use of between these instructions. In PACStack each verification is immediately followed by a return, which ensures that the failure is detected. Tail calls are a notable exception. Tail calls are function calls executed before return and optimized so that the callee directly returns to the caller of the optimized function. For example, in Listing , function A ends with a tail call to B using the b instructions that does not update (➀). The tail-called function can return (➁) to the value set before the tail call (➂). PACStack limits to modifying the previous token on the stack. could attempt to exploit the signing gadget to trick PACStack to accept an invalid (➃), and subsequently load it into after return. However, cannot flip the bit of (➄) because PACStack guarantees it is immutable. The invalid is thus always passed into (➃) and so, detected at return from B (➁). Forthcoming additions in the ARMv8.6-A architecture will preclude such attacks in general [3].
6.3.2 Sigreturn-oriented programming
Sigreturn-oriented programming [9] is a exploitation technique in UNIX-like operating systems, including Linux, that abuses the signal frame to take complete control of a process’s execution state, i.e., the values of general purpose registers, , program counter (), status flags, etc. When the kernel delivers a signal, it suspends the process and changes the user-space processor context such that the appropriate signal handler is executed with the right arguments. When the signal handler returns, the original user-space processor context is restored. In a sigreturn attack sets up a fake signal frame and initiates a return from a signal that the kernel never delivered. Specifically, a program returns from the handler using a sigreturn system call that reads a signal frame (struct sigcontext in Linux) from the process stack.
Although a sigreturn attack is, in principle, problematic for PACStack (as it could allow control of any EL0 register, including ), a number of defenses against sigreturn attacks have been proposed for the Linux kernel, any of which will protect PACStack. Bosman and Bos [9] propose placing keyed signal canaries in the signal frame that are validated by the kernel before performing a sigreturn, or to keep a counter of the number of currently executing signal handlers. However, modern Linux versions rely solely on address space layout randomization (ASLR) [32] to make it difficult for the attacker to trigger an unwarranted sigreturn. Fortunately sigreturn is never called directly from program code (in fact the GNU C library sigreturn simply returns an error value). Instead the system call is triggered by signal trampoline code placed either in the kernel’s virtual dynamic shared object (vdso) or in the C library, both subject to ASLR. For our chosen adversary model (Section 3) ASLR is not sufficient as can determine the contents of any readable memory in the process memory space. However, PACStack itself, together with coarse-grained CFI (Assumption A2), ensures that cannot divert control flow from program code to the signal trampoline. Nonetheless, 64-bit ARM programs that might call system calls directly using the svc instruction (without going through C library system call wrappers), would not be protected against the presence of such gadgets. We discuss a potential general solution against sigreturn attacks that utilizes the ACS construction in Appendix B.
7 Performance Evaluation
At present, the only publicly available PA-enabled SoCs are the Apple A12, A13, S4, and S5, none of which support PA for 3rd party code at the time of writing. To verify the correctness of instrumentation we ran all benchmarks on the ARMv8-A Base Platform Fixed Virtual Platform (FVP) , based on Fast Models 11.4, which supports ARMv8.3-A [2]. Because the FVP runs the kernel, we have used PA RFC patches 77 7 https://lwn.net/Articles/752116/ modified to support all PA keys.
The FVP is not cycle-accurate and executes all instructions in one master cycle; therefore, it cannot be used for performance evaluation. Based on prior evaluations of the QARMA cipher [7], which is used as the underlying cryptographic primitive in reference implementations of PA [45], Liljestrand et al. estimate that the PAC calculations incur an average overhead of four cycles on a 1.2GHz CPU [35]. We employ the PA-analogue introduced by Liljestrand et al. to estimate the run-time overhead of PACStack.
7.1 SPEC CPU 2017
We ran benchmarks on Amazon EC2 using the SPEC CPU 2017 benchmark package88 8 https://www.spec.org/cpu2017. To guarantee exclusive access to the hardware, we used Amazon EC2 a1.metal99 9 https://aws.amazon.com/ec2/instance-types/a1/instances, each with 16 64-bit ARMv8.2-A cores. As these CPUs do not support PA, we instrumented benchmarks with the PA-analogue. For comparison, we measured run-time overheads of:
1) ShadowCallStack(a AArch64 production-ready software shadow call stack implementation for Clang 9 [14]), 2) -mbranch-protection(Clang’s built-in PA-based return address protection), and 3) -mstack-protector-strong(stack canaries).We measured PACStack by instrumenting all function entry and exit points, excluding leaf functions that do not spill or the (this is similar to the heuristic used by -mbranch-protection). We measured both full PACStack and PACStack without masking (PACStack-nomask).
ShadowCallStack saves a function’s return address in a separately-allocated shadow stack and then uses the protected return address when performing a return. On 64-bit ARM the register is reserved to hold a reference to the shadow stack. To perform a comparison against PACStack using the GNU C library (glibc) we ported ShadowCallStack support to glibc. Due to compatibility issues [52], we did not run the perlbench benchmarks with ShadowCallStack.
Our measurements include all C SPECrate and SPECspeed benchmarks, compiled with -O2 optimizations and flags to enable the measured instrumentation. The suite is self-contained, avoiding the need to instrument system libraries. For each benchmark, we compared the performance of the baseline (with all evaluated instrumentations disabled) to the measured configuration. Figure 5 shows the mean overheads (w.r.t the baseline). Table 2 shows the geometric mean of the overheads, excluding perlbench which was incompatible with ShadowCallStack. On C++ benchmarks we observed overheads of 2.0% (PACStack) and 0.9% (PACStack-nomask). Due to compatibility issues with ShadowCallStack and -mbranch-protection, we limit our comparison to the C benchmarks.
As expected, -mstack-protector-strong outperforms other instrumentations (but provides the weakest protection). In terms of added instructions, -mbranch-protection is similar to PACStack-nomask; the performance difference is likely due to PACStack reserving the register and the additional store when saving it the stack. PACStack-nomask and ShadowCallStack have similar memory requirements (i.e., one extra store per function call), and show similar performance overheads. The overhead of PACStack is proportional to the frequency of function calls; benchmarks with few function calls are affected less than the benchmarks with frequent function calls. For instance, the 519.lbm_r benchmark involves computations related to fluid dynamics and consists of large nested loops with few function calls. Consequently we see little effect on the performance of 519.lbm_r.
Based on these results, we expect the overhead for both PACStack configurations to be
a) comparable to ShadowCallStack, and b) negligible on PA-capable hardware.
| SPECrate | SPECspeed | |
|---|---|---|
| PACStack | 2.75% | 3.28% |
| PACStack-nomask | 0.86% | 1.56% |
| ShadowCallStack | 0.85% | 0.77% |
| -mbranch-protection | 0.43% | 0.72% |
| -mstack-protector-strong | 0.43% | 0.25% |
7.2 Real-world evaluation: NGINX
We evaluated the efficacy of PACStack in a real-world setting using a SSL/TLS transactions per second (SSL TPS) test on the NGINX 1010 10 https://www.nginx.com/ open source web server software. SSL TPS measures a web server’s capacity to create new SSL/TLS connections back to clients. Clients send a series of HTTPS requests, each on a new connection. The web server sends a 0-byte response to each request. The connection is closed after the response is received. We chose the SSL TPS test (instead of measuring throughput) to ensure that the load on the web server is CPU-bound, allowing us to estimate the upper bound for PACStack’s impact on NGINX performance.
We conducted our tests on two separate Amazon EC2 A1 instances connected via elastic network interfaces with up to 10 Gbps capacity. The web server (on an a1.metal instance, running NGINX 1.17.8 with OpenSSL 1.1.1d) and the client (on an a1.4xlarge instance) ran the 64-bit ARM version of Ubuntu 18.04. We configured the server to use the ECDHE-RSA-AES256-GCM-SHA384 cipher with a 2,048-bit RSA key for HTTPS. The client used wrk1111 11 https://github.com/wg/wrk (version of April 18, 2019), a modern HTTP benchmarking tool, to generate traffic. We configured wrk in the same way as in a test on NGINX performance conducted by F5 Networks.1212 12 https://www.nginx.com/blog/nginx-plus-sizing-guide-how-we-tested/ We ran a total of 15 copies of wrk on the client machine for 3 minutes each.
We repeated the test with four and eight NGINX worker processes instrumented with PACStack and PACStack-nomask, and compared the results with uninstrumented baseline performance. In both configurations we also instrumented NGINX’s dependencies (OpenSSL, pcre and zlib libraries). All binaries were compiled with -O2 optimizations. We summarize the results in Table 3, showing a 4–7% overhead for PACStack-nomask and 6–13% overhead for PACStack. These results are consistent with the performance overheads measures for SPEC CPU 2017 (Section 7.1).
| # of | Baseline | PACStack-nomask | PACStack | |||||
|---|---|---|---|---|---|---|---|---|
| workers | req./sec. | req./sec. | overhead | req./sec. | overhead | |||
| 4 | 14.2k | 142 | 13.7k | 124 | 3.8% | 13.5k | 117 | 5.5% |
| 8 | 30.7k | 722 | 28.6k | 658 | 7.1% | 27.2k | 612 | 12.7% |
7.3 Compatibility testing using ConFIRM
ConFIRM is a set small micro-benchmarking suite designed to test compatibility and relevance of CFI solutions [52]. The suite is designed to test various corner-cases—e.g., function pointers, setjmp/longjmp and exception handling—that often cause compatibility issues for CFI solutions. ConFIRM is designed for x86-based architectures and includes some tests that are exclusive to the Microsoft Windows operating system. Of the 18 64-bit Linux tests 11 compiled and worked on AArch64; these included virtual and indirect function calls, setjmp/longjmp, calling conventions, tail calls and load-time dynamic linking. We ran these benchmarks on the FVP (to guarantee functional equivalence to PA-capable hardware) and confirmed that the tests passed with or without PACStack.
8 Related Work
Control-flow hijacking have been known for more than two decades [48]. Most current CFI solutions are stateless: they validate each control-flow transfer in isolation without distinguishing among different paths in the control-flow graph (CFG). Fully-precise static CFI [11] is the most restrictive stateless policy possible without breaking the intended functionality of the protected program. In fully-precise static CFI the best possible policy for return instructions is to allow returns within a function to target any instruction that follows a call to . All stateless CFI schemes, including fully-precise static CFI, are vulnerable to control-flow bending [11].
Stateful CFI can express policies that take previous control-flow transfers into account. HAFIX [19] is a hardware-assisted CFI scheme that confines function returns to active call sites. Context-sensitive CFI [51, 20, 28] further ensures that each control-flow transfer taken by the program is consistent with a non-malicious trace. Despite its better precision, context-sensitive CFI enforcement is considered impractical for real-world adoption [1]. Hardware-assisted branch recording features available in modern 64-bit Intel microprocessors can be used to enable context-sensitive CFI enforcement on commodity hardware, but suffer from
i) limited branch history used to make CFI decisions, ii) over-approximation of the program CFG, and iii) reliance on complex run-time monitoring.HAFIX, on the other hand, requires changes to the processor.
As dynamic schemes, PACStack and shadow call stacks [13, 22, 23, 24, 15, 38, 1, 17, 18, 50, 39, 29, 14, 6] are not vulnerable to control-flow bending. Stateless forward-edge CFI enforcement is often combined with a shadow stack to enforce the integrity of return addresses stored on the call stack. In fact, the results by Carlini et al. [11] show that a shadow stack (or equivalent mechanism) is essential for the security of CFI. However, traditional shadow stacks incur significant performance overhead and lead to false positives for programming constructs that cause mismatches between calls and returns (C++ exceptions with stack unwinding, setjmp/longjmp). Recent designs improve performance by either leveraging a parallel shadow stack [17], or using a dedicated register for shadow stack addressing [10]. But since the shadow stack in this schemes resides in the same address space as the target application, it can be compromised if knows its location. A typical solution for dealing with mismatches between calls and returns is to pop return addresses off the shadow stack until a match is found, or the shadow stack is empty (e.g., binary RAD [13]). This not only increases the complexity and run-time of the shadow stack instrumentation placed in the function epilogue, but also sacrifices precision, e.g., it allows to redirect longjmp to any previously active call site. This can be avoided by storing and validating both the return address and stack pointer [15, 40, 50]. So far, only hardware-assisted shadow stacks promise to achieve negligible overhead without security trade-offs (e.g., Intel CET[29]).
Park et al. [42] present a micro-architectural shadow stack implementation using the branch predictor return address stack, a common hardware feature found in modern speculative superscalar processor designs. The return address stack is typically a circular buffer; to avoid losing stored return addresses when the maximum capacity is reached, Park et al. modify the return address stack to spill a portion of its content to backup storage in main memory. A Merkle-tree caching scheme is used to efficiently authenticate the backup storage before it is read back to the return address stack. The latency of spill/fill operations on backup memory is offset by the 100% hit rate for branch prediction since return addresses that exceed the return address stack capacity are retained.
The idea of using of MACs to protect the return address at run-time was introduced in Cryptographic CFI (CCFI) [37] which uses MACs to protect return addresses and other control-flow data (e.g., function pointers and C++ vtable pointers). CCFI’s return address protection is similar to PA-based return address signing [45]; both bind the return address to the address of the function’s stack frame and thus provide only coarse-grained resistance against pointer reuse [35]. In contrast to PACStack, these approaches cannot prevent reuse attacks (See Section 6.1). Independently to our work, Li et al. [34] propose a chain structure to protect return addresses but do not prevent the attacker from exploiting MAC collisions, and require custom hardware to realize their solution.
Program Counter Encoding [33, 16, 22, 44, 41] protects return addresses on the stack by encoding them with either a register-resident secret key [33], a read-only key stored in memory [16], the [44], or the address at which the return address itself is stored (a.k.a. the self-address) [41]. It is efficient, but relying on a secret key resident in user space makes such encoding schemes susceptible to buffer over-reads, and or self-address encoding suffer the same drawbacks as -msign-return-address [45, 35] (Section 2.2.1).
Other prominent defenses against control-flow attacks include fine-grained code randomization [32], and code-pointer integrity (CPI) [31]. Code randomization makes it more difficult for to find suitable gadgets to exploit, but ineffective if knows the program memory layout. CPI protects code pointers by storing them in a separate safe stack, which requires similar integrity guarantees as shadows stacks to remain effective [21]. Roessler et al. propose a metadata-tagged architecture to isolate stack-objects based on the stack-depth [46]. However, similar to the value (Section 6.2), the stack-depth will repeat frequently during program execution.
PACStack targets the ARM architecture, which has received less attention compared to the x86 family of computer architectures in terms of CFI research. MoCFI [18] is a software-based CFI approach specifically targeting ARM application processors used in smartphones. It uses a combination of a shadow stack, static analysis and run-time heuristics to determine the set of valid targets for control-flow transfers, but suffers from the same drawbacks that plague traditional shadow stack schemes. CFI CaRE [39] is a CFI solution targeting small, embedded ARM-based microcontrollers (MCUs). It uses the ability to perform hardware-enforced isolated execution on ARMv8-M MCUs to isolate the shadow stack to a secure processor state. The ARMv8-M [5] architecture enforces that calls to secure functions must target secure gate instructions placed at the beginning of such functions. The ARMv8.5-A architecture introduces similar branch target indicators (BTI) [4] to ARM application processors. BTI constitutes one way to meet the PACStack pre-requisite of coarse-grained CFI (Section 3).
9 Discussion
9.1 Support for software exceptions
The setjmp / longjmp interface has traditionally been used to provide exception-like functionality in C. However, modern coding standards for C and C++ that aim to facilitate code safety, security, and reliability consider them harmful and forbid their use, e.g., MISRA C:2004 [26, Rule 20.7] and JSF AV C++ [36, Rule 20]. Recall from Section 4.4 that calling longjmp with an expired jmp_buf is undefined behavior. For PACStack, this means that although the in jmp_buf is tied to the corresponding and , its freshness cannot be guaranteed. can modify jmp_buf to contain the previously used and , but must also modify the stack-frame at , such that it contains the prior . This allows a control-flow transfer to a previously valid setjmp return site and value. To prevent reuse of expired jmp_buf buffers, longjmp can be rewound step-by-step, i.e., conceptually performing returns until the correct stack-frame is reached.
We plan to extend PACStack support to LLVM libunwind 1313 13 https://github.com/llvm/llvm-project/tree/master/libunwind – it does frame-by-frame unwinding of the call stack. By validating the ACS on each stack frame unwinding, PACStack can ensure that a fresh and valid state is reached.
As C++ exceptions also cause irregular stack unwinding they pose a similar challenge. But C++ does finer-grained stack unwinding to correctly destroy objects in unwound stack frames. The LLVM libcxxabi library will, depending on configuration, use libunwind for this purpose. With PACStack support in libunwind, we will be able to secure both setjmp / longjmp and support C++ exception handling.
9.2 Interoperability with unprotected code
Interoperability with unprotected (uninstrumented) code is an important deployment consideration. On one hand, PACStack-protected applications may need to interoperate with unprotected shared libraries. On the other, unprotected applications may need to interoperate with PACStack-protected shared libraries. The latter scenario is relevant for deployment in mobile operating systems like Android, where multiple stakeholders provide application binaries to consumer devices. The deployment of PACStack, or any other run-time protection mechanism, is likely to be driven by OEMs that enable specific protection schemes for the operating system and system applications. However, OEMs are not in control of native code deployed as part of applications. It should be possible for one version of the shared libraries shipped with the operating system to remain interoperable with both PACStack-protected, and unprotected apps.
In Section 5.1 we explain how the use of callee-saved registers allows PACStack to remain interoperable with unprotected code. Recall that because is a callee-saved register it will be restored upon return. However, PACStack cannot guarantee that remains unmodified during the execution of the unprotected code that could temporarily store its value on the stack. To meet the security guarantees (Section 6), PACStack instrumentation must be applied to both the application and any shared libraries. But partial protection, e.g. PACStack-protected shared libraries can significantly raise the bar for the attacker, as calls into protected functions can still benefit from return address authentication. Common shared libraries like libc are a popular source for gadgets for run-time attacks because of their size and availability. Because functions in a PACStack-protected library validate the return address in returns from library functions, they effectively remove a potentially large set of reusable gadgets from ’s disposal.
10 Conclusion
ACS achieves security on-par with hardware-assisted shadow stacks (Section 6). With PACStack, we demonstrate how the general-purpose security PA security mechanism can realize our design, without requiring additional hardware support or compromising security. Other general-purpose primitives like memory tagging and branch target indicators are being rolled out. Creative uses of such primitives hold the promise of significantly improving software protection.
Acknowledgments
This work was supported in part by NSERC (RGPIN-2020-04744), Intel Collaborative Research Institute for Collaborative Autonomous & Resilient Systems (ICRI-CARS), and Google (ASPIRE program). We acknowledge the computational resources provided by the Aalto Science-IT project.
References
- [1] Martín Abadi et al. Control-flow integrity principles, implementations, and applications. ACM Trans. Inf. Syst. Secur., 13(1):4:1–4:40, November 2009.
- [2] ARM Ltd. Fast models version 11.4 reference manual. https://developer.arm.com/documentation/100964/1104-00/, 2018.
- [3] ARM Ltd. Developments in the Arm A-profile architecture: Armv8.6-A. https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/arm-architecture-developments-armv8-6-a, 2019.
- [4] ARM Ltd. ARM architecture reference manual (ARM DDI 0487F.c). https://developer.arm.com/documentation/ddi0487/fc, 2020.
- [5] ARM Ltd. Armv8-M architecture reference manual (ARM DDI 0553B.l). https://developer.arm.com/documentation/ddi0553/bl/, 2020.
- [6] Sergei Arnautov and Christof Fetzer. ControlFreak: Signature chaining to counter control flow attacks. In Proc. IEEE SRDS ’15, pages 84–93, 2015.
- [7] Roberto Avanzi. The QARMA block cipher family. almost MDS matrices over rings with zero divisors, nearly symmetric even-mansour constructions with non-involutory central rounds, and search heuristics for low-latency s-boxes. IACR Trans. Symmetric Cryptol., 2017(1):4–44, 2017.
- [8] Brandon Azad. Google Project Zero: Examining pointer authentication on the iPhone XS. https://googleprojectzero.blogspot.com/2019/02/examining-pointer-authentication-on.html, 2019.
- [9] Erik Bosman and Herbert Bos. Framing signals - a return to portable shellcode. In Proc. IEEE S&P ’14, pages 243–258, 2014.
- [10] Nathan Burow, Xingping Zhang, and Mathias Payer. SoK: Shining light on shadow stacks. In Proc. IEEE S&P ’19, pages 985–999, 2019.
- [11] Nicolas Carlini et al. Control-flow bending: On the effectiveness of control-flow integrity. In Proc. USENIX Security ’15, pages 161–176, 2015.
- [12] Shuo Chen et al. Non-control-data attacks are realistic threats. In Proc. USENIX Security ’05, pages 177–191, 2005.
- [13] Tzi-Cker Chiueh and Fu-Hau Hsu. RAD: A compile-time solution to buffer overflow attacks. In Proc. IEEE ICDCS ’01, pages 409–417, 2001.
- [14] Clang 9.0 Documentation. ShadowCallStack. https://releases.llvm.org/9.0/tools/clang/docs/ShadowCallStack.html, 2019.
- [15] Marc L. Corliss, E. Christopher Lewis, and Amir Roth. Using DISE to protect return addresses from attack. ARM SIGARCH Comput. Archit. News, 33(1):65–72, 2005.
- [16] Crispin Cowan et al. PointGuard: Protecting pointers from buffer overflow vulnerabilities. In Proc. USENIX Security ’03, pages 91–104, 2003.
- [17] Thurston H.Y. Dang, Petros Maniatis, and David Wagner. The performance cost of shadow stacks and stack canaries. In Proc.ACM ASIA CCS ’15, pages 555–566, 2015.
- [18] Lucas Davi et al. MoCFI: A framework to mitigate control-flow attacks on smartphones. In Proc. NDSS ’12, 2012.
- [19] Lucas Davi et al. HAFIX: Hardware-assisted flow integrity extension. In Proc. ACM/EDAC/IEEE DAC ’15, pages 74:1–74:6, 2015.
- [20] Ren Ding et al. Efficient protection of path-sensitive control security. In Proc. USENIX Security ’17, pages 131–148, 2017.
- [21] Isaac Evans et al. Missing the point(er): On the effectiveness of code pointer integrity. In Proc. IEEE S&P ’15, pages 781–796, 2015.
- [22] Michael Frantzen and Michael Shuey. StackGhost: Hardware facilitated stack protection. In Proc. USENIX Security ’01, pages 55–66, 2001.
- [23] Jonathon T. Giffin, Somesh Jha, and Barton P. Miller. Detecting manipulated remote call streams. In Proc. USENIX Security ’02, pages 61–79, 2002.
- [24] Jonathon T. Giffin, Somesh Jha, and Barton P. Miller. Efficient context-sensitive intrusion detection. In Proc. NDSS ’04, 2004.
- [25] William H. Hawkins, Jason D. Hiser, and Jack W. Davidson. Dynamic canary randomization for improved software security. In Proc. ACM CISRC ’16, pages 9:1–9:7, 2016.
- [26] HORIBA MIRA Ltd. Guidelines for the use of the C language in critical systems, 2004.
- [27] Hong Hu et al. Data-oriented programming: On the expressiveness of non-control data attacks. In Proc. IEEE S&P ’16, pages 969–986, 2016.
- [28] Hong Hu et al. Enforcing unique code target property for control-flow integrity. In Proc. ACM CCS ’15, pages 1470–1486, 2018.
- [29] Intel Corporation. Control-flow Enforcement Technology specification, revision 3.0. https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf, 2019.
- [30] Tim Kornau. Return Oriented Programming for the ARM Architecture. PhD thesis, Ruhr-Universität Bochum, 2009.
- [31] Volodymyr Kuznetsov et al. Code-pointer integrity. In Proc. USENIX OSDI ’14, pages 147–163, 2014.
- [32] Per Larsen et al. SoK: Automated software diversity. In Proc. IEEE S&P ’14, pages 276–291, 2014.
- [33] Gyungho Lee and Akhilesh Tyagi. Encoded program counter: Self-protection from buffer overflow attacks. In Proc. CSREA ICIC ’00, pages 387–394, 2000.
- [34] Jinfeng Li et al. Zipper stack: Shadow stacks without shadow. arXiv:1902.00888 [cs.CR], 2019.
- [35] Hans Liljestrand et al. PAC it up: Towards pointer integrity using ARM pointer authentication. In Proc. USENIX Security ’19, pages 177–194, 2019.
- [36] Lockheed Martin Corporation. Joint Strike Fighter Air Vehicle C++ Coding Standards (Revision C), 2005.
- [37] Ali Jose Mashtizadeh et al. CCFI: Cryptographically enforced control flow integrity. In Proc. ACM CCS ’15, pages 941–951, 2015.
- [38] Danny Nebenzahl, Mooly Sagiv, and Avishai Wool. Install-time vaccination of windows executables to defend against stack smashing attacks. IEEE Trans. Dependable Secur. Comput., 3(1):78–90, 2006.
- [39] Thomas Nyman et al. CFI CaRE: Hardware-supported call and return enforcement for commercial microcontrollers. In Proc. RAID ’17, pages 259–284. Springer International Publishing, 2017.
- [40] H. Ozdoganoglu et al. SmashGuard: A hardware solution to prevent security attacks on the function return address. IEEE Trans. Comput., 55(10):1271–1285, 2006.
- [41] Seho Park, Yongsuk Lee, and Gyungho Lee. Program counter encoding for ARM® architecture. Journal of Information Security, 8:42–55, 2017.
- [42] Yong-Joon Park and Gyungho Lee. Repairing return address stack for buffer overflow protection. In Proc. ACM CF ’04, pages 335–342, 2004.
- [43] Theofilos Petsios et al. DynaGuard: Armoring canary-based protections against brute-force attacks. In Proc. ACM ACSAC ’15, pages 351–360, 2015.
- [44] Changwoo Pyo and Gyungho Lee. Encoding function pointers and memory arrangement checking against buffer overflow attack. In Proc. ICICS ’02, pages 25–36, 2002.
- [45] Qualcomm. Pointer authentication on ARMv8.3. https://www.qualcomm.com/media/documents/files/whitepaper-pointer-authentication-on-armv8-3.pdf, 2017.
- [46] Nick Roessler and Andre DeHon. Protecting the stack with metadata policies and tagged hardware. In Proc. IEEE S&P ’18, pages 478–495, 2018.
- [47] Nigel P. Smart. Cryptography Made Simple. Springer Publishing Company, 1st edition, 2015.
- [48] Solar Designer. lpr LIBC RETURN exploit. http://insecure.org/sploits/linux.libc.return.lpr.sploit.html, 1997.
- [49] László Szekeres et al. SoK: Eternal war in memory. In Proc. IEEE S&P ’13, pages 48–62, 2013.
- [50] Caroline Tice et al. Enforcing forward-edge control-flow integrity in GCC & LLVM. In Proc. USENIX Security ’14, pages 941–955, 2014.
- [51] Victor van der Veen et al. Practical Context-Sensitive CFI. In Proc. ACM CCS ’15, pages 927–940, 2015.
- [52] Xiaoyang Xu et al. CONFIRM: Evaluating compatibility and relevance of control-flow integrity protections for modern software. In Proc. USENIX Security ’19, pages 1805–1821, 2019.
Appendix A Security proofs
In Section 6.2, we gave an informal analysis of the security of ACS; here we give a more detailed proof of security, and in particular prove that authentication token masking prevents from obtaining exploitable authentication token collisions.
The argument proceeds as follows: we suppose that , after obtaining authentication tokens, can find a pair of inputs and whose authentication tokens collide. This can be used to construct a distinguisher of the masks from a random string. The structure of the authentication tags is such that this further reduces to a semantic security game for one-time pad encryption of the masks. Then, we show that any violation of the integrity of an ACS-protected call stack also yields values whose authentication tokens collide as described above, allowing us to bound the probability of an integrity violation.
We summarize our notation in Table 4.
| Games | ||
|---|---|---|
| (Figure 11) | Security game for ACS integrity. | |
| (Figure 6) | Security game for the identification of colliding authentication tokens. | |
| (Figure 7) | Security game for the distinguishability of from a random oracle. | |
| (Figure 8) | Semantic security games for the mask . | |
| Adversary interfaces | ||
| Get path through the call-graph for which wants the final authenticated return address pushed to the stack. | ||
| Return a previously-requested authenticated return address. | ||
| Return to the challenger authenticated return values that can be used to violate call stack integrity. | ||
| Get a value for which wants a masked authentication token. | ||
| Return a previously-requested masked authentication token. | ||
| Return to the challenger two authenticated return values with colliding authentication tokens. | ||
| Get a value for which wants an authentication tag. | ||
| Return a previously-requested authentication token. | ||
| Return to the challenger a single bit identifying whether the given tokens were from a random oracle or . | ||
| Identify the authentication token function used to generate masked authentication tokens. | ||
| As for , but with the inputs represented as strings, not functions. | ||
\got@maxcolwd // Give masked authentication tokens // of their choice. // is challenged to provide inputs whose authentication tokens collide.
Theorem 1 (PAC-masking prevents collision-finding).
Suppose that after queries, an adversary can distinguish from a random oracle with advantage no greater than , as given in Figure 7. Then, assuming a key-length of for , and given access to masked authentication tokens, can identify a pair of inputs and whose corresponding unmasked authentication tokens collide with advantage at most .
Proof.
We begin with a collision-game , shown in Figure 6 in which the adversary is given oracle access to the authentication token generator and then asked to provide values such that .
\got@maxcolwd // is given values of their choice from either // or a random oracle // is challenged to determine whether it received // values from or the random oracle.
An adversary that selects at random from , such that , will win with probability ; ’s advantage is therefore
We will bound this advantage by reduction to a semantic security game for the masks. We consider the following games, shown in Figure 8, and described in Figure 9.
| // The adversary is given and and challenged to | |||
| // determine which is used to calculate . | |||
| // The adversary is given and and challenged to | |||
| // determine which is used to calculate . | |||
| // The adversary is given and and challenged to | |||
| // determine which is used to calculate . | |||
- :
-
obtains masked authentication tokens for up to pairs of ’s choice, and must then distinguish the masks from a random oracle.
- :
-
This is the same as the previous game, except that is replaced by a random oracle and is not limited in their number of queries. must now distinguish between two random oracles, one of which is used in computing the authentication tokens, and one of which is independent of the authentication tokens.
- :
-
This is the semantic security game for repeated one-time-pad encryptions of a random string.
The first hop, from to , is based on indistinguishability and relaxation: we suppose that can be distinguished from a random oracle with probability no more than , and that the adversary is not limited in the number of queries that can be made to the masked authentication token oracle. Then,
The second hop, from to , is a mere reformulation of such that random oracles are represented as strings, and that rather than allowing to request arbitrarily many authentication tokens from the challenger, we instead give direct access to the oracle, as represented by the sequence of strings .
The third game is a semantic security game for the one-time pad, where is given encryptions of and then asked to distinguish between and a random string. The perfect secrecy of the one-time pad means that and so
| (2) |
Finally, we provide a reduction from to . Suppose can win with advantage . Then, we define an adversary for , shown in Figure 10.
\got@maxcolwd \got@maxcolwd \got@maxcolwd
This adversary wins with probability at least , and so by (2)
If the MAC is a pseudo-random function family with respect to , then is negligible, and thus so is . ∎
With a bound on ’s probability of successfully obtaining a PAC collision, we may now obtain a bound on their probability of violating the integrity of an ACS-protected call stack.
Theorem 2 (Security of ACS).
Consider a program whose call stack is protected by ACS, which has a call-graph and -bit masked authentication tokens . Then, an adversary with arbitrary control over memory can violate backward-edge control-flow integrity with probability
Proof.
We begin with a security game for ACS, shown in Figure 11.
Our goal is to provide a black-box reduction from to .
Appendix B Mitigation of sigreturn attacks
A solution for precluding sigreturn attacks against PACStack would be to include the signal return value to the PACStack chain via the value stored on the signal frame:
Upon signal delivery, the kernel stores a copy of securely in kernel space as a reference value. If the process was already executing a signal handler, and thus the kernel already has a reference copy of on record, it stores in the new signal frame and overwrites the secure copy with . On sigreturn the kernel attempts to validate the and values in the signal frame as though the reference value was . If successful it performs the signal return to and restores to . Otherwise the kernel assumes a return to a nested signal handler, and retrieves and from the signal frame, validates them by calculating = and comparing the result against the stored reference value. If successful the kernel replaces with in the secure kernel store and performs the signal return to . If the validation fails the kernel terminates the process. This prevents from
1) overwriting , and 2) forging the values in signal frames.For general protection against sigreturn attacks corrupting any register stored in the signal frame, all register values could be included in the calculation using the pacga instruction and validated at the time of sigreturn.
\got@maxcolwd // Give tokens from call-graph traversals. // Is the request for a real path through the call-graph? // The substituted masked authenticated return address must be different. // Does the return pointer authenticate correctly with the adversary’s // new masked authenticated return address as the modifier? // Did the adversary provide a valid masked authenticated return address?