
Plattformübergreifender C-Port des Copy Fail Linux LPE (CVE-2026-31431). Offengelegt 2026-04-29 von Theori / Xint.
Englisch (en) ∙ 日本語 (ja) ∙ 简体中文 (zh-cn) ∙ 한국어 (ko) ∙ Русский (ru)
Eine plattformunabhängige C-Neuimplementierung des Copy-Fail-Linux-LPE (CVE-2026-31431), offengelegt am 29.04.2026 von Theori / Xint. Die vollständige Schwachstellenbeschreibung, der Zeitplan und der Entdeckungsprozess von Theori finden Sie im offiziellen Writeup unter copy.fail.
Der öffentlich veröffentlichte Proof-of-Concept ist ein 732 Byte großes Python-Skript. Diese C-Portierung zeigt, dass derselbe Exploit als portables C ausgedrückt werden kann, das auf jede Architektur kompilierbar ist, die nolibc unterstützt – ohne architekturspezifische Hex-Blobs oder Inline-Assembler im eigenen Quellcode des Projekts.
Autor dieser Portierung: Tony Gies [email protected]. Entdeckung und ursprüngliche Offenlegung: Theori / Xint.
copy-fail-c/
├── exploit.c the dropper (binary-mutation variant)
├── exploit-passwd.c the dropper (/etc/passwd UID-flip variant)
├── vulnerable.c non-destructive vulnerability checker
├── payload.c the body that gets dropped (setgid+setuid+execve sh)
├── utils.c, utils.h shared AF_ALG/splice page-cache mutation primitive
├── Makefile build orchestration
├── nolibc/ vendored from torvalds/linux tools/include/nolibc
└── README.md this file
After make:
├── payload tiny static ELF, embedded into the dropper as bytes
├── payload.o payload wrapped as a relocatable .o by `ld -r -b binary`
├── exploit dropper, binary-mutation variant
├── exploit-passwd dropper, /etc/passwd UID-flip variant
└── vulnerable non-destructive vulnerability checker
exploit.c öffnet die Ziel-Binärdatei schreibgeschützt und führt dann für jedes 4-Byte-Fenster des
eingebetteten Payloads eine einzelne fehlerhafte AEAD-Entschlüsselung über AF_ALG durch, deren
Chiffrat-Eingabe per splice() von den Page-Cache-Seiten des Ziels bereitgestellt wird. Die
In-Place-Optimierung der authencesn-Vorlage behandelt die gesplicten Quellseiten sowohl als
Chiffrat-Eingabe als auch als Klartext-Ziel, sodass die (fehlschlagende) Entschlüsselung die
Page-Cache-Seite bereits überschrieben hat, wenn die Authentifizierungsprüfung die Anforderung
ablehnt. Nach 4 * N Iterationen wurde das zwischengespeicherte Image des Ziels Byte für Byte
durch den Payload ersetzt. Ein execve() des Ziels lädt die mutierten Seiten; der Inode auf der
Festplatte ist immer noch setuid root, sodass der Kernel Root-Berechtigungen gewährt und den
Payload ausführt.
payload.c ist einfaches portables C: setgid(0); setuid(0); execve("/bin/sh", ...). nolibc liefert _start, die Syscall-Mechanik und das architekturspezifische
Register-Management.
Eine zweite Variante, exploit-passwd.c, mutiert vier Bytes des Page-Caches von /etc/passwd
anstelle des Images einer setuid-Binärdatei. Sie benötigt keinen eingebetteten Payload und
funktioniert auf Systemen, auf denen der Binärmutationsweg blockiert ist, aber ihre
„Cashout“-Oberfläche ist viel schmaler.
vulnerable.c ist kein Exploit. Es erstellt eine lokale testfile mit dem String init und
führt dann dieselbe patch_chunk()-Primitive gegen den Page-Cache dieser Datei aus, um die
Bytes mit vulnerable zu überschreiben. Stimmen die zurückgelesenen Inhalte überein, befindet
sich der laufende Kernel im Fenster von CVE-2026-31431. Der Inode auf der Festplatte wird nie
geändert; testfile wird beim Beenden gelöscht; die Page-Cache-Mutation verschwindet mit
ihm. Läuft ohne Privilegien. Beendet sich mit 100, wenn anfällig, mit 0, wenn die Primitive
lief, aber die Mutation nicht wirksam wurde, mit 2, wenn die AF_ALG-Socket-Familie oder die
authencesn-Vorlage nicht verfügbar ist, sodass der Patch-Status nicht bestimmt werden kann,
und mit 1 für andere Laufzeitfehler.
Standard (Host-Architektur nativ):
make
Cross-Kompilieren nach aarch64 (oder eine andere Linux-Architektur, für die ein Cross-Toolchain installiert ist):
make CC=aarch64-linux-gnu-gcc LD=aarch64-linux-gnu-ld
Von der mitgelieferten nolibc unterstützte Architekturen (laut Upstream): x86_64, i386,
arm, aarch64, riscv32/64, mips, ppc, s390x, loongarch, m68k, sh, sparc.
nolibc wählt anhand der Architektur-Makros des Compilers aus, daher reicht die Auswahl der
richtigen CC/LD.
Erforderlich zum Bauen:
cc, gcc oder eine beliebige Cross-Variante)ld -r -b binary unterstützt (sowohl binutils ld als auch lld tun das)linux/if_alg.h und <asm/unistd.h> (Debian/Ubuntu: linux-libc-dev;
Cross-Varianten: normalerweise durch das Cross-Toolchain-Paket mitgeliefert)Headersets älter als Linux 5.6 haben __kernel_old_time_t und struct __kernel_old_timespec
nicht, die die mitgelieferte nolibc verwendet. compat.h (wird beim Payload-Build per
Force-Include eingebunden) liefert sie, wenn sie fehlen, sodass auch ein älteres
linux-libc-dev noch baut. Bei 5.6+ Headern ist es ein No-Op.
Es gibt keine externen Bibliotheksabhängigkeiten. Der Payload wird freistehend gegen nolibc
gebaut; der Dropper linkt nur für fprintf und perror gegen die Host-libc.
Einige kleine Toolchain-Funktionen tragen den Großteil dazu bei, den Quellcode portabel und den Payload klein zu halten.
nolibc/ ist der winzige Header-Only-libc-Ersatz des Kernels, aus torvalds/linux
tools/include/nolibc/ übernommen. Es stellt _start, ein portables syscall()-Makro und
Inline-Syscall-Wrapper bereit, wobei die architekturspezifischen Registerkonventionen in
nolibc/arch-*.h codiert sind. Das Bauen des Payloads mit -nostdlib -static -ffreestanding -Inolibc erzeugt ein winziges statisches ELF, das direkt in den Kernel aufruft, ohne
glibc-Startup, TLS-Init oder Stack-Canary-Infrastruktur mitzuschleppen. Ergebnis nach Packen
und Section-Stripping (beide unten): ~720 Bytes auf x86_64, ~1,2 KB auf aarch64, gegenüber
~17 KB für dasselbe payload.c gelinkt gegen musl-static oder ~700 KB gegen glibc-static.
ld -r -b binary zum EinbettenDas Makefile verwandelt das gebaute payload-ELF in payload.o mittels ld -r -b binary -o payload.o payload. Der Linker gibt die Eingabe-Bytes wörtlich als Datensektion einer
verschiebbaren Objektdatei aus und synthetisiert drei Symbole aus dem Eingabedateinamen:
_binary_payload_start Adresse des ersten Payload-Bytes
_binary_payload_end Adresse ein Byte hinter dem letzten Payload-Byte
_binary_payload_size absolutes Symbol, dessen Wert die Größe in Bytes ist
exploit.c deklariert die ersten beiden als extern const unsigned char[] und berechnet die
Größe als _binary_payload_end - _binary_payload_start.
-Wl,-N plus enges max-page-sizeDer Payload wird statisch gelinkt mit -Wl,-N -Wl,-z,max-page-size=0x10,
was .text/.rodata/.data in ein einzelnes LOAD-Segment mit 16-Byte-Dateiausrichtung
zusammenfasst, anstatt der Standard-Kernelseiten-Ausrichtung von 4 KB pro Segment. Dies
erzeugt eine „RWX permissions“-Warnung von ld, die nur informativ ist – der
Laufzeitspeicherschutz des Payloads spielt für sein einziges Ziel keine Rolle. Ohne dieses
Flag linkt derselbe Code auf ~13 KB auf x86_64 (meistens Zwischensegment-Nullfüllung); damit
auf ~1,3 KB vor dem unten beschriebenen Section-Header-Strip.