Defenses Against Return Address Manipulation in C
In C programming, memory safety vulnerabilities such as buffer overflows frequently allow malicious actors to overwrite saved return addresses on the call stack, enabling arbitrary code execution through techniques like Return-Oriented Programming (ROP). To prevent this execution hijacking, modern systems employ several hardware- and software-based defenses. This article outlines the primary mechanisms used to protect return addresses in compiled C binaries, including compiler-inserted stack canaries, hardware and software shadow stacks, backward-edge Control Flow Integrity (CFI), and ARM Pointer Authentication.
Stack Canaries
Stack canaries (also known as Stack Smashing Protection) are one of
the oldest and most widely deployed defenses against linear buffer
overflows. Supported by compilers like GCC and Clang via flags such as
-fstack-protector-strong, this mechanism places an
integrity check value—the canary—on the stack directly between local
variables and the saved return address.
Before a function executes its ret instruction, it
verifies that the canary value remains unchanged. If a buffer overflow
overwrites the return address, it inevitably alters the canary first.
The application detects the mismatch and immediately terminates
execution before the corrupted return address can be used. However,
canaries cannot stop targeted non-linear writes or memory disclosure
attacks that leak the canary value.
Shadow Stacks
A shadow stack is a dedicated memory region used exclusively for storing return addresses, completely isolated from normal program data. When a function call occurs, the return address is pushed onto both the standard call stack and the shadow stack. Upon function return, the system compares the two values or pops the address directly from the shadow stack.
Hardware Shadow Stacks
Modern architectures integrate shadow stacks directly into the processor to minimize performance overhead and prevent tampering:
- Intel Control-flow Enforcement Technology (CET):
Implements a hardware-enforced shadow stack. The CPU maintains an
internal shadow stack pointer (
SSP) that cannot be modified by ordinary memory write instructions. If the return address on the data stack does not match the one on the shadow stack during a return instruction, the CPU triggers a control protection exception (#CP). - ARM Guarded Control Stack (GCS): Introduced in ARMv9-A, GCS functions similarly to Intel CET, utilizing privileged hardware instructions and dedicated memory page permissions to ensure that the return stack remains inaccessible to standard store operations.
Software Shadow Stacks
Compilers can also generate software-managed shadow stacks when
hardware support is absent. For example, Clang offers
ShadowCallStack, which reserves a dedicated CPU
register (such as x18 on AArch64) to point to a private
shadow stack, ensuring return addresses are saved and restored without
writing them to the standard data stack.
Pointer Authentication Codes (PAC)
Introduced in the ARMv8.3-A architecture, Pointer Authentication uses cryptographic signatures to ensure pointer integrity. When compiling C code with PAC support:
- When entering a function, the compiler inserts an instruction (e.g.,
PACIASP) that generates a cryptographic hash of the return address combined with a secret processor key and a context modifier (such as the current stack pointer). - The resulting signature is stored in the unused upper bits of the 64-bit return address.
- Before returning, the function executes an authentication
instruction (e.g.,
AUTIASPorRETAA).
If an attacker overwrites the return address in memory, the cryptographic signature becomes invalid. When authentication fails, the processor corrupts the address into a designated illegal value, causing a fatal segmentation fault when the branch occurs.
Backward-Edge Control Flow Integrity (CFI)
Control Flow Integrity restricts the execution path of a binary to a strictly defined control-flow graph (CFG) generated at compile time. While forward-edge CFI protects indirect calls and jumps, backward-edge CFI focuses specifically on function returns.
Backward-edge CFI mechanisms record the set of valid call sites for every function. Upon returning, the binary verifies that the destination address corresponds to an allowed call site for that specific routine. While effective, software-only backward-edge CFI often incurs noticeable performance costs, leading most production environments to favor hardware shadow stacks or Pointer Authentication as more efficient alternatives.