Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
copy-fail-c — Cross-platform C port of the Copy Fail Linux LPE (CVE-2026-31431). Disclosed 2026-04-29 by Theori / Xint. | Kitploit
Tools/GitHubGitHub/tgies/copy-fail-c
Privilege EscalationExploit FrameworksVulnerability AnalysisExploitationPapers & ResearchLearning & EducationPayload DevelopmentBinary Exploitation
GitHubtgies/copy-fail-c

copy-fail-c

Cross-platform C port of the Copy Fail Linux LPE (CVE-2026-31431). Disclosed 2026-04-29 by Theori / Xint.

View Repository
44012182 months agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Copy Fail (CVE-2026-31431) - C port

English (en) ∙ 日本語 (ja) ∙ 简体中文 (zh-cn) ∙ 한국어 (ko) ∙ Русский (ru)

A cross-platform C reimplementation of the Copy Fail Linux LPE (CVE-2026-31431), disclosed 2026-04-29 by Theori / Xint. See the canonical writeup at copy.fail for the full vulnerability description, timeline, and Theori's discovery process.

The publicly-released proof-of-concept is a 732-byte Python script. This C port demonstrates that the same exploit can be expressed as portable C compilable to any architecture nolibc supports, with no per-arch hex blobs or inline assembly in the project's own source.

Author of this port: Tony Gies [email protected]. Discovery and original disclosure: Theori / Xint.

Repository layout

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 opens the target binary read-only, then for each 4-byte window of the embedded payload runs one bogus AEAD-decrypt through AF_ALG whose ciphertext input is supplied via splice() from the target's page-cache pages. The authencesn template's in-place optimization treats the splice'd source pages as both the ciphertext input and the plaintext destination, so the (failing) decrypt has already overwritten the page-cache page by the time authentication verification rejects the request. After 4 * N iterations the target's cached image has been replaced byte-for-byte with the payload. execve()'ing the target loads the mutated pages; the on-disk inode is still setuid root, so the kernel grants root credentials and runs the payload.

payload.c is plain portable C: setgid(0); setuid(0); execve("/bin/sh", ...). nolibc supplies the _start, the syscall machinery, and the per-arch register-juggling.

A second variant, exploit-passwd.c, mutates four bytes of /etc/passwd's page cache instead of a setuid binary's image. It needs no embedded payload and works on systems where the binary-mutation route is blocked, but its cashout surface is much narrower.

vulnerable.c is not an exploit. It creates a local testfile containing the string init, then runs the same patch_chunk() primitive against that file's own page cache to overwrite the bytes with vulnerable. If the read-back contents match, the running kernel is in-window for CVE-2026-31431. The on-disk inode is never modified; testfile is removed on exit; the page-cache mutation evaporates with it. Runs unprivileged. Exits 100 if vulnerable, 0 if the primitive ran but the mutation did not take, 2 if the AF_ALG socket family or authencesn template is unavailable so patch state cannot be determined, and 1 for other runtime errors.

Build

Default (host-arch native):

make

Cross-compile to aarch64 (or any other Linux arch a cross-toolchain is installed for):

make CC=aarch64-linux-gnu-gcc LD=aarch64-linux-gnu-ld

Architectures supported by the vendored nolibc (per upstream): x86_64, i386, arm, aarch64, riscv32/64, mips, ppc, s390x, loongarch, m68k, sh, sparc. nolibc dispatches on the compiler's arch macros, so picking the right CC/LD is sufficient.

Required to build:

  • a C compiler (cc, gcc, or any cross variant)
  • a linker that supports ld -r -b binary (binutils ld and lld both do)
  • kernel UAPI headers providing linux/if_alg.h and <asm/unistd.h> (Debian/Ubuntu: linux-libc-dev; cross variants: typically pulled in by the cross-toolchain package)

Header sets older than Linux 5.6 predate __kernel_old_time_t and struct __kernel_old_timespec, which the vendored nolibc uses. compat.h (force-included into the payload build) supplies them when absent, so an older linux-libc-dev still builds. It is a no-op on 5.6+ headers.

There are no external library dependencies. The payload is built freestanding against nolibc; the dropper links against the host libc only for fprintf and perror.

Architectural choices

A few small toolchain features carry most of the weight in keeping the source portable and the payload small.

nolibc

nolibc/ is the kernel's tiny header-only libc replacement, vendored from torvalds/linux tools/include/nolibc/. It provides _start, a portable syscall() macro, and inline syscall wrappers, with the per-arch register conventions encoded in nolibc/arch-*.h. Building the payload with -nostdlib -static -ffreestanding -Inolibc produces a tiny static ELF that calls into the kernel directly without dragging in glibc startup, TLS init, or stack-canary plumbing. Result once packed and section-stripped (both below): ~720 bytes on x86_64, ~1.2 KB on aarch64, versus ~17 KB for the same payload.c linked against musl-static or ~700 KB against glibc-static.

ld -r -b binary for embedding

The Makefile turns the built payload ELF into payload.o via ld -r -b binary -o payload.o payload. The linker emits the input bytes verbatim as the data section of a relocatable object file and synthesizes three symbols from the input filename:

_binary_payload_start    address of first payload byte
_binary_payload_end      address one past the last payload byte
_binary_payload_size     absolute symbol whose value is the size in bytes

exploit.c declares the first two as extern const unsigned char[] and computes the size as _binary_payload_end - _binary_payload_start.

-Wl,-N plus tight max-page-size

The payload is statically linked with -Wl,-N -Wl,-z,max-page-size=0x10, which collapses .text/.rodata/.data into a single LOAD segment with 16-byte file-alignment instead of the kernel-page-aligned 4 KB-per-segment default. This produces an "RWX permissions" warning from ld, which is informational only - the payload's runtime memory protection doesn't matter to its single-purpose program. Without this flag, the same code links to ~13 KB on x86_64 (mostly inter-segment zero padding); with it, ~1.3 KB before the section-header strip below.

Stripping section headers

After linking, objcopy --strip-section-headers removes the payload's section header table and .shstrtab. The kernel ELF loader maps a program from its program headers alone, so those bytes never load at runtime, and because payload is embedded verbatim they also inflate the drop and the patch_chunk iteration count. Stripping them takes the x86_64 payload from ~1.3 KB to 720 bytes (322 four-byte iterations down to 180). Two link-time flags shave the rest: -Wl,--build-id=none drops the build-id note, and -fcf-protection=none drops the x86 CET note where the compiler supports it.

The strip needs binutils >= 2.40. Cross builds pass the target's objcopy via OBJCOPY= (e.g. OBJCOPY=aarch64-linux-gnu-objcopy); when objcopy cannot do it, the build prints a note and keeps a valid, larger payload.

Variants and cashout viability

Download Tool