Control-flow integrity
M. Abadi, M. Budiu, Ú. Erlingsson, and J. Ligatti. were the first to publish a practical control-flow integrity scheme in 2005.
PaX had RAP, a forward and backward edges
type-based arbitrary read/write impervious CFI, privately since at least 2012, and publicly since 2015. The
idea of CFI was even mentioned in
pax_future.txt from 2003, in
its c
section.
Windows added CFG in Windows 8.1 and Windows 10 in 2015, but even if they removed it from the scope of its bugbounty in 2018, it’s coming back as XFG since 2019,taking inspiration for PaX’s RAP.
Android uses clang’s CFI for some userland applications since 2017, cross-DSO since 2018, and in kernel-land since 2019.
In the BSD-land, HardenedBSD has non-cross-DSO clang CFI since 2017.
In 2018, Apple launched the Apple A12, with Pointer Authentication, protecting return addresses, functions pointers, …… enabled by default on all iPhones.
In October 2019, Android added support for shadow-stack in kernel-land.
Microsoft’s Windows 10 19H1 (version 1903) had support for hardware-assisted shadow stack via
Intel CET
since at
least March
2020,
and has made it available in Visual Studio 2019 via the
/cetcompat
compiler flag.
Ubuntu is compiling binaries with -fcf-protection
since Ubuntu 20.04 (Focal Fossa), released in
April 2020, and support
for kernel-land on Linux landed in 5.18,
in 2022.
In September 2020, Android 11 was released, with tagged pointers support.
In February 2022, LLVM implemented support for KCFI, which landed in Linux the 3rd of October 2022:
Unlike the current CFI schemes implemented in LLVM, KCFI does not require LTO, does not alter function references to point to a jump table, and never breaks function address equality. KCFI is intended to be used in low-level code, such as operating system kernels, where the existing schemes can cause undue complications because of the aforementioned properties. However, unlike the existing schemes, KCFI is limited to validating only function pointers and is not compatible with executable-only memory.
This forward-edge control flow integrity scheme for indirect calls is type-based, and does look like a subset of PaX’ RAP.
In November 2022, Linux gained support for FineIBT in kernel-land, which was merged in Linux 6.2, compiled in by default, and automatically enabled on supported CPU at boot-time.
In February 2023, Deepak Gupta from rivos sent a serie of patches to make use of RISC-V Zisslpcfi (a subset of the Zicsr extension.) on Linux. The scheme is based on the now classic shadow-stack/landing-pad duo, but with hardware-support for functions signatures.
The 22th of June 2023, Peter Zijlstra reported this hilarious issue affecting Linux:
Alyssa Milburn noticed that when building the kernel with CFI_CLANG+IBT and booting on IBT enabled hardware to obtain FineIBT, the indirect functions look like:
__cfi_foo: endbr64 subl $hash, %r10d jz 1f ud2 nop 1: foo: endbr64
This is because the compiler generates code for kCFI+IBT. In that case the caller does the hash check and will jump to
+0
, so there must be anENDBR
there. The compiler doesn’t know about FineIBT at all; also it is possible to actually use kCFI+IBT when booting with'cfi=kcfi'
on IBT enabled hardware.Having this second
ENDBR
however makes it possible to elide the CFI check. Therefore, we should poison this secondENDBR
when switching to FineIBT mode.
In his CanSecWest 2023 talk, De Raadt said, completely ignoring the state of the art (PaX’ RAP (2015), Apple’s PAC (2017), FineIBT (2021), …)
There isn’t a simple complete solution to block ROP.
He added, completely ignoring all iPhones released after 2018 having forward and backward edge CFI by default, and Ubuntu making use of Intel CET by default, and Android, …
Shadow stacks and branch-target instructions aren’t really deployed yet
In 2023, Linux 6.4 got support for shadow stack in userland on x86.
Mid-2023, De Raadt and others added arm64 BTI and Intel IBT support for userland, in a serie of commits, after everyone else, but couldn’t help adding a snarky comment anyway:
Over the last 6 months we’ve worked on adding arm64 BTI & Intel IBT support in the kernels and all userland binaries. We have been fixing all the applications along the way. Many developers were involved. There is an innovative and substantial difference in our approach compared to how Linux is doing it:
- On OpenBSD, IBT/BTI enforcement is on by default (meaning mandatory), unless a binary is linked to request opt-out (using
-Wl,-z,nobtcfi
). After all our fixes, very few application binaries need that, and that count is expected to shrink quickly as we (or upstreams) fix the outstanding issues.- On Linux they are rehashing the same design as their executable-stack mechanism: if a single .o file in a resulting binary isn’t marked as IBT/BTI enforcement, the system will (silently) execute the program without enforcement and noone knows this is happening. So for an issue from around 2001, today Linux binaries with executable stack exist and work unsafely. I expect that 20 years from now Linux binaries without IBT/BTI enforcement will also exist and work unsafely..
Amusingly, the presence of executable stack on Linux in some libraries was used by Qualys in 2023 for their ssh-agent exploit..
Something even funnier is that RETGUARD isn’t disabled when PAC_RET is enabled.
The 3rd of October 2023, FreeBSD added support for Armv8.3-A Pointer Authentication (PAuth) and Branch Target Identification (BTI), disabled by default for now.
The 18th of October, the mold linker 2.3.0 gained an experimental
feature, -z rewrite-endbr
:
When given the -fcf-protection flag, GCC conservatively places an endbr64 at the beginning of every global function. This is because the function’s address might be taken as a pointer by other translation units. However, in most cases, function addresses are not actually taken. This conservative approach results in an overabundance of unnecessary endbr64 instructions, leading to not only code bloating but also a potential decrease in security as there are more locations for an attacker to exploit.
The new linker option, -z rewrite-endbr, aims to alleviate this issue. The linker can carry out a whole-program analysis on the input files to identify functions whose addresses are never taken. If -z rewrite-endbr is specified, mold will conduct this analysis and replace the initial endbr64 with a nop for functions whose addresses aren’t taken. (17f0d85)