Home · ASPLOS 2024 · Security, Privacy & Trusted Execution

Enforcing C/C++ Type and Scope at Runtime for Control-Flow and Data-Flow Integrity

Mohannad Ismail, Christopher Jelesnianski, Yeongjin Jang, Changwoo Min and Wenjie Xiong

Conference
ASPLOS 2024
Published
2024
Pages
283-300
Citations
3

Attackers break C and C++ programs by abusing pointers — using them as the wrong type, outside the scope they belonged to, or to read data they should not — and the newest data-oriented attacks can leak secrets like an SSL key without ever hijacking the program's control flow; this work from Virginia Tech observes that every such abuse violates the type, scope, and permission the programmer originally intended but that machine code forgets, and enforces that intent at runtime by cryptographically signing every pointer with ARM's hardware pointer authentication, blocking the attacks at a few percent overhead.

The argument in one glance

  1. Problem

    Attacks abuse pointers by violating the intent machine code forgot

    C and C++ attacks corrupt pointers — code pointers to hijack control flow, or data pointers to leak secrets like an SSL key without changing control flow. Every such abuse uses a pointer against its intended type, scope, or permission. Those restrictions are explicit in the source but discarded at compile time, so machine code enforces none of them.

  2. What people did

    Add data-pointer defenses

    Deploy existing defenses that protect data pointers, using programmer annotations, runtime metadata, or instrumentation of some pointers to catch corruption.

  3. Why it fell short

    They are partial, heavy, annotated, or slow

    These defenses rely on programmer annotations (bad for legacy code and error-prone), carry large runtime metadata an attacker can target, protect only a subset of pointers (so an attacker abuses the uninstrumented rest), or impose high overhead. Data-oriented attacks slip through because data pointers are so abundant.

  4. This paper's idea

    Sign every pointer with its type, scope, and permission (STI/RSTI)

    Scope-Type Integrity (STI) is a policy that forces all pointers, code and data, to conform to the programmer's intent — the type, scope, and permission recorded in the source. Runtime Scope-Type Integrity (RSTI) enforces it cheaply with ARM Pointer Authentication, signing each pointer with a code derived from its intended scope, type, and permission, checked in hardware on every use. Three variants trade security precision against overhead, protecting all pointers at 5.29, 2.97, and 11.12 percent.

Why this problem is worth solving

Memory-corruption attacks have evolved past the defenses built to stop them, and the hardest ones never touch control flow at all.

C and C++ give programmers direct control over memory, and with it a long history of memory-corruption bugs like buffer overflows. Attackers exploit these in two broad ways. In a control-flow hijacking attack, the attacker corrupts a code pointer — a function pointer, say — so that when the program follows it, execution jumps into attacker-chosen code, potentially running anything. Many defenses now target this. The harder family is data-oriented attacks, such as DOP and NEWTON, which corrupt data pointers rather than code pointers. By manipulating where data pointers point, an attacker can make the program leak sensitive information — an SSL key, for example — or perform malicious computation, all without ever diverting the program’s control flow, which means control-flow defenses do not see them. Data-oriented attacks are especially difficult to stop for two reasons: data pointers vastly outnumber code pointers in a typical program, so there is far more to protect, and it is hard to distinguish a security-sensitive data pointer, like one guarding a key, from an ordinary one. As these attacks grow more sophisticated, they routinely bypass state-of-the-art defenses.

The reason pointers can be abused is that the rules the programmer wrote down simply vanish.

In C and C++ source code, every variable and pointer carries restrictions the programmer intended: it has a specific type, it is valid only within a specific scope, and it has specific permissions such as read or write. Taken together, these define what the pointer is allowed to do — call it the programmer’s intent. The trouble is that compilation discards this information. Once the source is turned into machine code, a pointer is just an address in a register or memory, with no attached record of its type, scope, or permission, and the processor enforces none of those restrictions. This is exactly the gap attacks exploit: every pointer-abuse attack, whether it hijacks control flow or manipulates data, works by using a pointer in a way that violates its original type, scope, or permission — treating a pointer to one kind of object as another, using it outside the scope it belonged to, or writing through a pointer that should only be read. Prior defenses that try to close this gap have drawbacks: some require the programmer to annotate the code, which is impractical for legacy software and error-prone; some maintain large runtime metadata, which an attacker can itself target to bypass the defense; some protect only a subset of pointers, leaving the rest as an attack path; and some are simply too slow.

What people did before, and where it broke

  • Deploy control-flow integrity defenses. Protect code pointers so execution cannot be redirected. Where it broke. Data-oriented attacks corrupt data pointers and never divert control flow, so control-flow defenses do not see them.
  • Protect data pointers with annotations or metadata. Instrument data pointers using programmer annotations or large runtime metadata. Where it broke. Annotations are impractical for legacy code and error-prone; large metadata can itself be targeted by an attacker, and both add overhead.
  • Instrument only a subset of pointers. Protect the pointers judged most sensitive to keep overhead down. Where it broke. A creative attacker abuses the uninstrumented pointers instead, and it is hard to tell which pointers are security-sensitive in the first place.
  • The gap. No defense enforced the programmer’s full intent — type, scope, and permission — on all pointers, code and data, at low overhead and without heavy metadata. Providing that, by hardening intent into machine code with hardware pointer authentication, is what STI and RSTI do.

How STI and RSTI work

From address-only pointers to pointers signed with the programmer's intent

  1. new

  2. new

  3. new

  4. new

  5. new

The idea

Recover the intent that compilation discarded. STI’s foundation is that the source code fully specifies how each pointer may be used — its type, its scope, and its permissions — but this information is lost when the program is compiled, leaving machine code unable to tell a legitimate pointer use from an abuse; STI collects that intent at compile time and carries it into the running program. Reconstructing the source-level structure that the compiled form threw away, so that a property expressed in the original program can be recovered and used at the machine level, is the same undertaking the binary-lifting work covered elsewhere on this site pursues, recovering variables and their structure that are absent from raw machine code so the program can once again be reasoned about in the terms the programmer wrote.

Bind each pointer to its intended use with a cryptographic code. RSTI enforces the policy with ARM Pointer Authentication: it signs each pointer with an authentication code computed from a secret key and the pointer’s intended scope, type, and permission, stored in the pointer’s unused high bits, so a pointer that is corrupted or used out of context fails the hardware check because the attacker cannot forge the matching code. Attaching an unforgeable cryptographic code that binds a value to its legitimate context, so that any tampering or out-of-context use is detected on the spot, is the same mechanism the trusted-network-card work covered elsewhere on this site is built on, signing each message with an authentication code so a recipient can verify it came from the right sender and was not altered.

Leave no pointer unprotected. Because attackers simply abuse whatever a defense leaves uninstrumented, STI protects all pointers — both code and data — rather than a chosen subset, closing the escape routes that partial defenses leave open. Hardening the entire attack surface so that no overlooked case remains for an adversary to exploit is the same principle the leaky-instruction work covered elsewhere on this site follows, rewriting every instance of the dangerous pattern rather than a few, on the logic that an attacker needs only the one spot a defense forgot.

Following a data-oriented attack through RSTI

Stopping a data pointer from being redirected to leak a secret

A trace of RSTI defeating a data-oriented attack, following the paper's use of ARM Pointer Authentication.

Step 1 of 5

A program holds a secret and a data pointer

A program holds an SSL key and a data pointer that an attacker wants to redirect to leak it, exploiting a buffer overflow to corrupt that pointer.

A data-oriented attack never changes control flow, so control-flow defenses miss it.

Why it works

  • Attacks violate the programmer’s intent. Because every pointer attack ultimately uses a pointer outside its intended type, scope, or permission → enforcing those three properties catches the abuse at its root → so a single policy defends against both control-flow and data-oriented attacks.
  • The intent exists in the source but is lost in compilation. Because C and C++ source records each pointer’s type, scope, and permission but machine code discards them → the runtime has no way to check legitimate use → so recovering and carrying that information into machine code restores the missing enforcement.
  • Hardware authentication makes checks cheap. Because ARM Pointer Authentication signs and verifies pointers in hardware, storing the code in unused pointer bits → integrity can be checked without large external metadata → so all pointers can be protected at only a few percent overhead.
  • Signing with intent stops forgery. Because the pointer’s code is derived from its intended scope, type, and permission and depends on a secret key → an attacker who corrupts a pointer cannot produce a matching code → so a misused pointer simply fails authentication.
  • Covering every pointer closes the gap. Because attackers abuse whatever pointers a defense leaves uninstrumented → protecting all pointers, both code and data, removes that escape → so partial-coverage bypasses are eliminated.

What it costs (trade-offs and scope)

  • It requires ARM Pointer Authentication hardware. RSTI’s efficiency comes from a specific hardware feature, ARM Pointer Authentication, available on modern ARM processors. The approach depends on that hardware and on the number of authentication-code bits it provides.
  • Precision versus overhead is a genuine trade-off. The three variants make different trade-offs: the relaxed variant is fastest at 2.97 percent but coarser, the main variant is 5.29 percent, and the stricter, more precise variant costs 11.12 percent. Choosing among them balances how finely pointers are distinguished against performance.
  • Protection precision is bounded by equivalence classes. The paper measures protection with an equivalence-class metric: how unique each pointer’s type or variable is. Pointers that share a class can, in principle, be substituted for one another, so protection is only as fine-grained as the classes are, and pointer-substitution attacks within a class remain conceivable.
  • Casts add complexity. Because C and C++ allow casting a pointer to another type, the main mechanism must re-sign a pointer on a legitimate cast to keep type semantics consistent, which the design has to handle carefully.
  • It is a compiler-based defense. RSTI is prototyped in the LLVM compiler, so protecting a program means recompiling it with the pass; the security applies to code built this way.
  • Overheads are workload-specific. The reported averages come from specific benchmarks and applications (SPEC CPU, nbench, NGINX, CPython, PyTorch); other programs, especially pointer-heavy ones, could see different costs.

The evaluation

Three approaches to defending pointers in C and C++

PropertyNo protectionPartial pointer defensesRSTI (STI enforced)
Protects code pointers (control flow)NoYes
Protects data pointers (data flow)No
Enforces type, scope, and permissionPartial
Needs annotations or large metadataNot applicable
OverheadVaries, can be high
Three approaches to defending pointers in C and C++, as framed by the paper. With no protection, a corrupted pointer is used freely. Partial defenses focus on code pointers or a subset of data pointers, often needing annotations or large runtime metadata that attackers can target, and leaving uninstrumented pointers as an attack path. RSTI enforces Scope-Type Integrity on all pointers — code and data — by signing each with an ARM Pointer Authentication Code derived from its intended type, scope, and permission, stored in the pointer's unused bits and checked in hardware, so a corrupted or misused pointer fails authentication. It needs no large external metadata, and re-signs pointers on legitimate casts. The paper presents three variants trading precision against speed — a relaxed one at 2.97 percent overhead, the main one at 5.29 percent, and a stricter one at 11.12 percent — evaluated against state-of-the-art control-flow hijacking and data-oriented attacks and real-world CVEs, and on benchmarks including SPEC CPU 2006 and 2017, nbench, NGINX, CPython, and PyTorch. Protection precision is quantified by an equivalence-class metric measuring how uniquely each pointer is distinguished.Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. Why are data-oriented attacks especially hard to defend against?

  2. 2. What is the root cause of pointer-abuse attacks according to the paper, and what does STI do about it?

  3. 3. How does RSTI enforce Scope-Type Integrity efficiently?

  4. 4. The paper offers three RSTI variants with overheads of 5.29, 2.97, and 11.12 percent. What does this illustrate?

Sources

The paper is “Enforcing C/C++ Type and Scope at Runtime for Control-Flow and Data-Flow Integrity” by Mohannad Ismail, Christopher Jelesnianski, Yeongjin Jang, Changwoo Min, and Wenjie Xiong, published at ASPLOS 2024, DOI 10.1145/3620666.3651342. The authors are at Virginia Tech and Oregon State University.

This explainer is based on the full text of the paper, obtained as the open-access (CC-BY) author copy deposited in Virginia Tech’s VTechWorks repository. The problem framing (control-flow hijacking corrupting code pointers, data-oriented attacks such as DOP and NEWTON corrupting data pointers to leak secrets without diverting control flow, the abundance of data pointers, and the root cause that pointer abuses violate the type, scope, and permission the programmer intended but that compilation discards), the drawbacks of prior defenses (annotations, large metadata, partial protection, high overhead), the Scope-Type Integrity policy (collecting each pointer’s type, scope, and permission at compile time and enforcing conformance at runtime), the Runtime Scope-Type Integrity mechanisms (using ARM Pointer Authentication to sign pointers with codes derived from their intended scope, type, and permission stored in the pointer’s bits; the three variants RSTI-STWC, RSTI-STC, and RSTI-STL trading security against performance; re-signing on casts; and the equivalence-class metric for protection precision), the LLVM prototype on a commercial processor, and the results (average overheads of 5.29, 2.97, and 11.12 percent, and evaluation against control-flow hijacking and data-oriented attacks, real-world CVEs, and benchmarks including SPEC CPU 2006 and 2017, nbench, NGINX, CPython, and PyTorch) are all drawn directly from the paper. Background explanations of pointers, memory-corruption vulnerabilities, control-flow and data-flow integrity, and ARM Pointer Authentication are standard and provided to make the paper’s contribution understandable.

Related papers on Security, Privacy & Trusted Execution

See all 153 →