Runtime Security Mitigations for C Binaries

Because the C programming language lacks built-in memory safety, compiled binaries are historically susceptible to critical memory corruption vulnerabilities such as buffer overflows, use-after-free, and code reuse attacks. To counter these threats without requiring complete code rewrites, modern operating systems and compilers implement runtime security mitigations. This article outlines the primary defense mechanisms—including Control Flow Integrity, execution prevention, and memory randomization—that safeguard native C binaries during execution.

Control Flow Integrity (CFI)

Control Flow Integrity prevents attackers from hijacking a program's execution path, primarily stopping Return-Oriented Programming (ROP) and Jump-Oriented Programming (JOP) attacks. CFI mechanisms monitor both sides of control transfer:

  • Forward-Edge CFI: Protects indirect calls and jumps (such as function pointers and virtual tables). Compilers generate a graph of valid call targets at compile time and insert runtime checks before indirect branches to ensure the target destination matches a legitimate entry point. Hardware-assisted implementations include Intel Indirect Branch Tracking (IBT) and ARM Branch Target Identification (BTI).
  • Backward-Edge CFI (Shadow Stacks): Protects function return addresses. A secondary, isolated stack (the shadow stack) records the return address when a function is called. Before returning, the runtime compares the standard stack address with the shadow stack copy. If a stack-based buffer overflow alters the standard stack address, execution halts immediately. This is supported via software implementations or hardware features like Intel Control-flow Enforcement Technology (CET) and ARM Pointer Authentication Code (PAC).

Data Execution Prevention (DEP / NX)

Data Execution Prevention, implemented at the hardware level via the No-Execute (NX) or Execute-Disable (XD) bit, enforces the "Write XOR Execute" (\(W \oplus X\)) policy. Memory pages allocated for data—such as the stack and the heap—are marked as non-executable. Even if an adversary writes malicious shellcode into a buffer, the processor raises a fault if it attempts to execute instructions located within those memory boundaries.

Address Space Layout Randomization (ASLR) and PIE

Address Space Layout Randomization randomizes the memory addresses of the stack, heap, and shared libraries every time a program runs.

  • To extend this protection to the program code itself, binaries are compiled as Position-Independent Executables (PIE).
  • By eliminating predictable memory layouts, ASLR prevents attackers from reliably jumping to specific memory locations or crafting static ROP chains, forcing them to rely on memory disclosure vulnerabilities to locate targets.

Stack Canaries (Stack Smashing Protection)

Stack canaries protect against linear stack buffer overflows that target function return pointers. The compiler inserts a small, pseudo-random value onto the stack directly before the saved frame pointer and return address. When the function finishes executing, a runtime check verifies whether the canary value is intact. If an overflow has overwritten the canary, the process terminates immediately before the corrupted return address can be used.

Relocation Read-Only (RELRO)

Dynamic linkers rely on the Global Offset Table (GOT) to resolve the addresses of functions in shared libraries. In standard compilation, the GOT is writable, allowing attackers to overwrite entries with addresses of malicious payloads. RELRO mitigates this:

  • Partial RELRO: Reorders internal data sections so that the GOT is placed ahead of the program variables, protecting it against adjacent buffer overflows.
  • Full RELRO: Instructs the dynamic linker to resolve all symbols at application startup and then marks the entire GOT as read-only before program execution begins, entirely preventing GOT overwrite attacks.

Pointer Authentication (ARM PAC)

On modern 64-bit ARM architectures, Pointer Authentication protects pointers from being modified in memory. PAC utilizes unused upper bits in 64-bit virtual addresses to store a cryptographic signature (a Pointer Authentication Code) calculated from the pointer value, a modifier (such as the stack pointer), and an internal secret key. Before a pointer is dereferenced or jumped to, the runtime authenticates the signature; any tampering invalidates the code and triggers a crash.