
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.
Nach dem Linken entfernt objcopy --strip-section-headers die Section-Header-Tabelle und
.shstrtab des Payloads. Der Kernel-ELF-Loader mappt ein Programm ausschließlich aus seinen
Programm-Headern, daher werden diese Bytes nie zur Laufzeit geladen, und da payload wörtlich
eingebettet wird, blähen sie auch den Drop und die patch_chunk-Iterationsanzahl auf. Das
Entfernen reduziert den x86_64-Payload von ~1,3 KB auf 720 Bytes (322 Vier-Byte-Iterationen
auf 180). Zwei Linker-Flags sparen den Rest: -Wl,--build-id=none entfernt die Build-Id-Notiz
und -fcf-protection=none entfernt die x86-CET-Notiz, wo der Compiler dies unterstützt.
Der Strip benötigt binutils >= 2.40. Cross-Builds übergeben das objcopy des Ziels mittels
OBJCOPY= (z. B. OBJCOPY=aarch64-linux-gnu-objcopy); wenn objcopy dies nicht kann, gibt der
Build einen Hinweis aus und behält einen gültigen, größeren Payload.
Dieses Repository enthält zwei Exploit-Varianten, die sich die AF_ALG/Splice-Page-Cache-Mutationsprimitive teilen, aber unterschiedlich in Root-Ausführung münden. Ihre Zuverlässigkeitsprofile sind nicht gleichwertig, und der Unterschied ist wichtig, wenn man über reale Bedrohungsmodelle nachdenkt.
exploit)Mutiert den Page-Cache einer Ziel-setuid-Binärdatei mit den eingebetteten Payload-Bytes und führt dann die Binärdatei aus. Der Kernel gewährt Root-Berechtigungen aus dem unberührten setuid-Bit der Binärdatei auf der Festplatte, lädt das korrumpierte In-Memory-Image und führt den Payload aus.
Funktioniert überall dort, wo der Angreifer open(target, O_RDONLY) für eine beliebige
Root-setuid-Binärdatei im System aufrufen kann. Wird mehr oder weniger ausgehebelt durch
Umgebungen, die setuid-Binärdateien hinter Verzeichnissen mit eingeschränktem Lesezugriff
absichern, sowie durch setuid-freie Systemdesigns.
exploit-passwd)Mutiert vier Bytes des Page-Caches von /etc/passwd, um das UID-Feld des laufenden Benutzers auf „0000“ zu setzen. /etc/passwd ist auf jedem Standard-Linux-System weltweit lesbar, daher ist die Mutation universell. Die Umwandlung in Root-Ausführung hängt davon ab, dass ein Prozess mit Root-Rechten den Benutzer über getpwnam/getpwuid auflöst und basierend auf der aufgelösten UID handelt, ohne Quervalidierung. Viele solche Verbraucher existieren; viele davon prüfen defensiv gegen die Kernel-Sicht der aufrufenden UID oder gegen Dateibesitzer auf der Festplatte, was das Cashout unterbricht.
su fehlschlägtexploit-passwd führt nach der Mutation su <user> aus, als denkbar einfachstes Cashout.
Das funktioniert gegen util-linux su, schlägt aber gegen shadow-utils su mit
„Cannot determine your user name“ fehl. Die Page-Cache-Mutation ist zu diesem Zeitpunkt noch
vorhanden, und ein Umstieg auf jedes andere Cashout (z. B. Verwendung eines Daemons, der
Benutzer via getpwnam ohne Quervalidierung auflöst) ist zu diesem Zeitpunkt möglich. Führen Sie
echo 3 > /proc/sys/vm/drop_caches als root aus, um den korrumpierten Page-Cache nach
Abschluss der Tests zu leeren.
Untergrenze: torvalds/linux 72548b093ee3 August 2017, v4.14
(AF_ALG iov_iter-Umarbeitung, die
die Dateiseiten-Schreibprimitive via
splice in die AEAD-Streuliste einführte)
Obergrenze: torvalds/linux a664bf3d603d April 2026, mainline
(macht die In-Place-Optimierung von
algif_aead von 2017 rückgängig; trennt
Quell- und Ziel-Streulisten, sodass
Page-Cache-Seiten nicht länger ein
beschreibbares Crypto-Ziel sein können)
Dazwischen: alle großen Distro-Kernel, die den Fix nicht zurückportiert haben.
Ubuntu, RHEL, SUSE, Amazon Linux und Debian wurden zum Zeitpunkt der Offenlegung in ihren
Standard-Cloud-Image-Kernels als anfällig bestätigt. Backports auf Distro-Ebene begannen
um den 29.04.2026 zusammen mit der öffentlichen Offenlegung. Um zu überprüfen, ob ein
Zielkernel im Fenster liegt, prüfen Sie, ob a664bf3d603d (oder der distrospezifische
Backport) im Git-Log des Kernels oder im Changelog der Distribution vorhanden ist.
Für kostenpflichtige Sicherheitsüberprüfungen, kundenspezifische Portierungsarbeiten oder private Sicherheitshinweise kontaktieren Sie den Autor über seine Beratungsfirma Crash United, LLC.
Kontakt: [email protected] · https://crashunited.com
GitHub: @tgies · X/Twitter: @me_irl
Entdeckung und ursprüngliche Offenlegung von CVE-2026-31431: Theori / Xint. Öffentliches Writeup: https://copy.fail/.
Diese C-Portierung: Tony Gies [email protected]
nolibc/: aus dem Linux-Kernel-Baum übernommen, dual-lizenziert LGPL-2.1-or-later
ODER MIT (siehe nolibc/nolibc.h und individuelle Datei-SPDX-Header).
Die Dropper- und Payload-Quellen in diesem Repository werden unter denselben dualen LGPL-2.1-or-later ODER MIT-Bedingungen wie der nolibc-Baum veröffentlicht, von dem sie abhängen, um die Lizenzkompatibilität für jeden zu gewährleisten, der dieses gesamte Verzeichnis in eigene Arbeiten übernimmt.
Der Exploit und der Payload werden zu Sicherheitsforschungs- und defensiven Erkennungszwecken veröffentlicht. Die Verwendung gegen Systeme, die Ihnen nicht gehören oder für die Sie keine ausdrückliche Testberechtigung haben, ist Ihr Problem, nicht das des Autors.
| Cashout | Vorab-Root-Setup nötig | Anmerkungen |
|---|
| WSL2-Sitzungsstart | Nein | WSLs sitzungsbezogener setuid(getpwnam(default_user)->pw_uid) führt keine Validierung durch. Funktioniert sauber. |
util-linux su | Nein | Nachsichtige Handhabung der Aufruferidentität. |
shadow-utils su | Ja | getpwuid(getuid())-Aufruferidentitätsprüfung schlägt fehl, weil die Mutation die echte UID unmappt. |
sshd (Standard StrictModes yes) | Ja (StrictModes deaktivieren) | StrictModes erfordert, dass das Home-Verzeichnis root oder pw->pw_uid gehört. Mutation setzt pw_uid=0; Besitzer auf der Festplatte bleibt bei ursprünglicher UID; Konflikt verweigert Authentifizierung. |
| MTA-Lokalzustellung (postfix, exim usw.) | Variabel | Abhängig von den Home-Berechtigungsprüfungen des MDA. Pro MTA testen. |