algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

CET — shadow stacks, IBT, and what still works

2026-06-053 min readpwn internals

The stack canary guards one value; PAC/MTE raised the bar on ARM. x86's answer is CET, Control-flow Enforcement Technology — two hardware features that, together, delete the gadget soup ROP and JOP rely on. Knowing precisely what they delete (and what they leave) is the difference between "mitigated" and "exploited."

target
arch
amd64-64-little
CET
shadow stack (SHSTK) + indirect branch tracking (IBT)
compiler
-fcf-protection (endbr64 at every indirect target)
kills
classic ROP and JOP/COP to mid-function gadgets

shadow stack — return addresses you can't reach#

With SHSTK, every call pushes the return address to two places: the normal stack and a separate shadow stack in specially-protected memory you can't write with an ordinary store. Every ret pops both and compares them — mismatch raises a control-protection fault (#CP):

why a stack overflow stops redirecting rettxt
1
2
3
call f   ->  push retaddr to normal stack  AND  to shadow stack
overflow ->  attacker rewrites retaddr on the NORMAL stack
ret      ->  normal[top] != shadow[top]    ->  #CP, process killed

Overwriting the saved return address simply doesn't steer execution anymore — the shadow copy is untouched and authoritative. Classic ROP, which is nothing but overwritten return addresses, is dead.

IBT — every indirect branch must land on endbr64#

The second half covers indirect call/jmp. Under IBT, an indirect branch may only target an instruction that is endbr64; land anywhere else and you get #CP. The compiler drops an endbr64 at every legal indirect target — function entries, basically:

a legal indirect targetasm
1
2
3
4
endbr64                 ; without this first byte, an indirect call here faults
push rbp
mov  rbp, rsp
; ... function body ...

JOP and COP — chaining jmp/call gadgets in the middle of functions — can no longer aim at mid-function gadgets. Every indirect hop has to land on a function entry.

what still works#

CET is a real, large tax. It is not a wall:

  • Whole-function calls (COP to entries). IBT permits indirect calls to any endbr64, and every function starts with one. If you control an indirect call site and its arguments, calling system — or any useful function — outright is still legal control flow. You lost gadgets, not functions.
  • Data-only attacks. Neither feature looks at data. Overwrite a modprobe_path, a __malloc_hook-era pointer that gets called through a legal site, file-stream fields, or a length used in a memcpy — no return address forged, no indirect branch broken.
  • Loops and back-edges already in the program. The intended control flow does plenty; bend the data it flows over.
  • Shadow stack writes, where they exist. If wrss is enabled, or a bug gives a write to shadow-stack memory, or incssp gadgets let you skip entries, the return-address guarantee weakens. And if SHSTK isn't enabled for the thread at all, you're back to ordinary ROP.
the shape of a CET-era exploit

Stop forging return addresses; find an indirect call you control and aim it at a real function entry, or skip control flow entirely and corrupt the data it trusts:

txt
segfault{endbr64_is_at_the_start_of_system_too}

CET ends the era of stitching mid-function gadgets into return-oriented chains — genuinely, that technique is over where it's deployed. What replaces it is older than ROP: call the functions that already exist with the arguments you want, or don't touch control flow at all and let corrupted data carry the exploit. The bug still decides; CET just narrows the menu.