2026-09-04
Shadow Stack (covered earlier) protects returns. But attackers building JOP (Jump-Oriented Programming) or COP (Call-Oriented Programming) chains don't need ret — they can pivot through indirect call rax or jmp [rbx] instructions and stitch together "gadgets" that begin anywhere. Indirect Branch Tracking (IBT) is the other half of Intel CET: it constrains where indirect branches are allowed to land.
The mechanism is brutally simple. Every indirect call or jmp arms a tiny hardware state machine called WAIT_FOR_ENDBRANCH. The very next instruction executed must be ENDBR64 (or ENDBR32) — a 4-byte NOP-on-old-CPUs, landing-pad-on-new-CPUs instruction. If it isn't, the CPU raises a #CP (Control Protection) fault. Direct branches (call func, jmp label) don't arm the tracker, so ENDBR is only required at legitimate indirect targets: function entry points, jump table entries, and computed goto labels.
Compiler cooperation is mandatory. GCC's -fcf-protection=branch and MSVC's /CETCOMPAT emit an ENDBR64 at the top of every function whose address is taken, and at every switch-table case. Functions the compiler proves are only ever called directly don't get one — saving 4 bytes and preventing them from being used as gadgets. This is the killer feature: any address that isn't marked ENDBR is unreachable via indirect branch, period. A gadget that starts 3 bytes into a function? Dead.
Real example — glibc's PLT. Before IBT, every PLT stub began with a plain jmp *got_entry, and any attacker who could overwrite a function pointer could redirect execution to any byte in the binary. After IBT, the indirect jump's target must itself begin with ENDBR64. Linux distributions (Fedora 32+, Ubuntu 23.10+) now ship glibc and userspace binaries with IBT enabled. On a Tiger Lake or newer chip, running readelf -n /bin/ls | grep IBT shows the IBT property flag in the GNU note section.
Rule of thumb for gadget scarcity: IBT typically reduces available indirect-jump landing sites by ~95%. A stripped 10 MB binary might contain ~2 million bytes reachable by a naive JOP scan; with IBT, only the ~50,000 bytes at ENDBR64-tagged function entries qualify. That's the difference between "trivially exploitable" and "attacker needs a separate bug to disable CET first."
The compatibility trick: ENDBR64 encodes as F3 0F 1E FA, which older CPUs decode as a valid NOP-family instruction. So IBT-enabled binaries run unchanged on pre-Tiger-Lake hardware — the landing pads are simply ignored.
ENDBR64 instruction, shrinking the JOP/COP gadget surface by ~95% while remaining a NOP on older CPUs.
