diff --git a/Makefile b/Makefile index 3c0aff0..aaa2451 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ BUILD := build BIN := skeletonkey # core/ -CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c +CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c core/host.c CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS)) # Family: copy_fail_family diff --git a/ROADMAP.md b/ROADMAP.md index aa8eae6..75aef22 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -227,6 +227,22 @@ of the 28-module verified corpus):** `a2567217` (Linux 7.0); `fragnesia` against 7.0.9; `pack2theroot` against PackageKit 1.3.5. The `kernel_range` model now drives their verdicts; `--active` confirms empirically on top. +- [x] **`core/host` host-fingerprint refactor.** A single + `struct skeletonkey_host` is populated once at startup and + handed to every module via `ctx->host`: kernel version + arch + + distro id/version + capability gates (unprivileged_userns, + AppArmor restriction, BPF disabled, KPTI, lockdown, SELinux, + Yama ptrace) + service presence (systemd, system D-Bus). The + `--auto` / `--scan` banner now prints the fingerprint up front + so operators see at a glance which gates are open. 4 modules + migrated to consume the fingerprint (dirtydecrypt, fragnesia, + pack2theroot, overlayfs) — replacing per-detect `uname`s, + `/etc/os-release` parses, and userns fork-probes with O(1) + cached lookups. See `docs/ARCHITECTURE.md` for the pattern; + future modules can opt-in by including `core/host.h`. +- [ ] Migrate the remaining modules (cgroup_release_agent / + overlayfs_setuid / copy_fail_family bridge / others) to + consume `ctx->host` — incremental follow-up. **Carry-overs:** diff --git a/core/host.c b/core/host.c new file mode 100644 index 0000000..c1df73f --- /dev/null +++ b/core/host.c @@ -0,0 +1,265 @@ +/* + * SKELETONKEY — host fingerprint implementation + * + * Lives behind a one-shot lazy-init: skeletonkey_host_get() probes on + * first call, stores into a file-static, and returns the same pointer + * forever after. Single-threaded (skeletonkey is single-threaded), so + * no synchronisation needed. + */ + +#include "host.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#endif + +static struct skeletonkey_host g_host; +static bool g_host_ready = false; + +/* ── small parser helpers ─────────────────────────────────────────── */ + +/* Copy the value of a `KEY=VAL` line (stripping leading quotes and + * trailing quote / newline) into `dst`. Caller passes the start of the + * value (after `=`). Cap is the size of dst including NUL. */ +static void parse_os_release_value(const char *s, char *dst, size_t cap) +{ + const char *p = s; + if (*p == '"' || *p == '\'') p++; + size_t L = strcspn(p, "\"'\n"); + if (L >= cap) L = cap - 1; + memcpy(dst, p, L); + dst[L] = '\0'; +} + +static bool path_exists(const char *p) +{ + struct stat st; + return stat(p, &st) == 0; +} + +#ifdef __linux__ +/* Sysctl/sys-fs readers — Linux-only consumers (populate_caps). */ +static bool read_int_file(const char *path, int *out) +{ + FILE *f = fopen(path, "r"); + if (!f) return false; + int v; + int n = fscanf(f, "%d", &v); + fclose(f); + if (n != 1) return false; + *out = v; + return true; +} + +static bool read_first_line(const char *path, char *dst, size_t cap) +{ + FILE *f = fopen(path, "r"); + if (!f) return false; + if (!fgets(dst, (int)cap, f)) { fclose(f); return false; } + fclose(f); + size_t n = strlen(dst); + while (n > 0 && (dst[n-1] == '\n' || dst[n-1] == '\r')) dst[--n] = '\0'; + return true; +} +#endif + +/* ── populators ───────────────────────────────────────────────────── */ + +static void populate_kernel(struct skeletonkey_host *h) +{ + struct utsname u; + if (uname(&u) == 0) { + /* utsname.machine/nodename can be up to 65 bytes on glibc; the + * %.*s precision spec tells gcc the snprintf is bounded so it + * does not warn about possible truncation (we WANT truncation; + * the snprintf already caps). */ + snprintf(h->arch, sizeof h->arch, + "%.*s", (int)sizeof(h->arch) - 1, u.machine); + snprintf(h->nodename, sizeof h->nodename, + "%.*s", (int)sizeof(h->nodename) - 1, u.nodename); + } + /* kernel_version_current owns the static release-string buffer + * and the parser — reuse it to keep one source of truth. */ + kernel_version_current(&h->kernel); +} + +static void populate_distro(struct skeletonkey_host *h) +{ + snprintf(h->distro_id, sizeof h->distro_id, "?"); + snprintf(h->distro_version_id, sizeof h->distro_version_id, "?"); + snprintf(h->distro_pretty, sizeof h->distro_pretty, "?"); + + FILE *f = fopen("/etc/os-release", "r"); + if (!f) return; + char line[256]; + while (fgets(line, sizeof line, f)) { + if (strncmp(line, "ID=", 3) == 0) + parse_os_release_value(line + 3, + h->distro_id, sizeof h->distro_id); + else if (strncmp(line, "VERSION_ID=", 11) == 0) + parse_os_release_value(line + 11, + h->distro_version_id, sizeof h->distro_version_id); + else if (strncmp(line, "PRETTY_NAME=", 12) == 0) + parse_os_release_value(line + 12, + h->distro_pretty, sizeof h->distro_pretty); + } + fclose(f); +} + +static void populate_user(struct skeletonkey_host *h) +{ + h->euid = geteuid(); + h->egid = getegid(); + h->is_root = (h->euid == 0); + h->is_ssh_session = (getenv("SSH_CONNECTION") != NULL); + + h->username[0] = '\0'; + struct passwd *pw = getpwuid(h->euid); + if (pw && pw->pw_name) + snprintf(h->username, sizeof h->username, "%s", pw->pw_name); + + /* Default: real_uid == euid (no userns). Try /proc/self/uid_map to + * discover the outer uid if we're inside a user namespace. Format + * + * "0 0 4294967295" → init ns, outer == 0 + * "0 1000 1" → userns mapped, outer == 1000 + * + * Only trust outer != 0 and != -1 as the bypass-userns case. */ + h->real_uid = h->euid; + int fd = open("/proc/self/uid_map", O_RDONLY); + if (fd >= 0) { + char buf[256]; + ssize_t n = read(fd, buf, sizeof buf - 1); + close(fd); + if (n > 0) { + buf[n] = '\0'; + int inner = -1, outer = -1, count = 0; + if (sscanf(buf, "%d %d %d", &inner, &outer, &count) == 3 && + inner == 0 && outer > 0) + h->real_uid = (uid_t)outer; + } + } +} + +static void populate_platform_family(struct skeletonkey_host *h) +{ +#ifdef __linux__ + h->is_linux = true; +#else + h->is_linux = false; +#endif + h->is_debian_family = path_exists("/etc/debian_version"); + h->is_rpm_family = path_exists("/etc/redhat-release") || + path_exists("/etc/fedora-release") || + path_exists("/etc/rocky-release") || + path_exists("/etc/almalinux-release"); + h->is_arch_family = path_exists("/etc/arch-release"); + h->is_suse_family = path_exists("/etc/SuSE-release") || + path_exists("/etc/SUSE-brand"); +} + +#ifdef __linux__ +/* fork+unshare(CLONE_NEWUSER) probe. Forks once; ~1ms cost. */ +static bool userns_probe(void) +{ + pid_t pid = fork(); + if (pid < 0) return false; + if (pid == 0) { + _exit(unshare(CLONE_NEWUSER) == 0 ? 0 : 1); + } + int st; + if (waitpid(pid, &st, 0) < 0) return false; + return WIFEXITED(st) && WEXITSTATUS(st) == 0; +} +#endif + +static void populate_caps(struct skeletonkey_host *h) +{ + h->unprivileged_userns_allowed = false; + h->apparmor_restrict_userns = false; + h->unprivileged_bpf_disabled = false; + h->kpti_enabled = false; + h->kernel_lockdown_active = false; + h->selinux_enforcing = false; + h->yama_ptrace_restricted = false; + +#ifdef __linux__ + h->unprivileged_userns_allowed = userns_probe(); + + int v = 0; + if (read_int_file("/proc/sys/kernel/apparmor_restrict_unprivileged_userns", &v)) + h->apparmor_restrict_userns = (v != 0); + if (read_int_file("/proc/sys/kernel/unprivileged_bpf_disabled", &v)) + h->unprivileged_bpf_disabled = (v != 0); + if (read_int_file("/sys/fs/selinux/enforce", &v)) + h->selinux_enforcing = (v != 0); + if (read_int_file("/proc/sys/kernel/yama/ptrace_scope", &v)) + h->yama_ptrace_restricted = (v > 0); + + char buf[256]; + if (read_first_line("/sys/devices/system/cpu/vulnerabilities/meltdown", buf, sizeof buf)) + h->kpti_enabled = (strstr(buf, "Mitigation: PTI") != NULL); + + /* /sys/kernel/security/lockdown format: "[none] integrity confidentiality" + * — whichever level is bracketed is the active one. */ + if (read_first_line("/sys/kernel/security/lockdown", buf, sizeof buf)) + h->kernel_lockdown_active = (strstr(buf, "[none]") == NULL); +#endif +} + +static void populate_services(struct skeletonkey_host *h) +{ + h->has_systemd = path_exists("/run/systemd/system"); + h->has_dbus_system = path_exists("/run/dbus/system_bus_socket"); +} + +/* ── public entrypoints ───────────────────────────────────────────── */ + +const struct skeletonkey_host *skeletonkey_host_get(void) +{ + if (g_host_ready) return &g_host; + + memset(&g_host, 0, sizeof g_host); + populate_kernel(&g_host); + populate_distro(&g_host); + populate_user(&g_host); + populate_platform_family(&g_host); + populate_caps(&g_host); + populate_services(&g_host); + g_host.probe_source = "skeletonkey core/host.c"; + g_host_ready = true; + return &g_host; +} + +void skeletonkey_host_print_banner(const struct skeletonkey_host *h, bool json) +{ + if (json || h == NULL) return; + fprintf(stderr, "[*] host: %s%s%s kernel=%s arch=%s distro=%s/%s\n", + h->nodename[0] ? h->nodename : "?", + h->is_root ? " (ROOT)" : "", + h->is_ssh_session ? " (SSH)" : "", + h->kernel.release ? h->kernel.release : "?", + h->arch[0] ? h->arch : "?", + h->distro_id[0] ? h->distro_id : "?", + h->distro_version_id[0] ? h->distro_version_id : "?"); + fprintf(stderr, "[*] gates: userns=%s aa_restrict=%s bpf_disabled=%s " + "kpti=%s lockdown=%s selinux=%s yama_ptrace=%s\n", + h->unprivileged_userns_allowed ? "yes" : "no", + h->apparmor_restrict_userns ? "on" : "off", + h->unprivileged_bpf_disabled ? "yes" : "no", + h->kpti_enabled ? "on" : "off", + h->kernel_lockdown_active ? "on" : "off", + h->selinux_enforcing ? "on" : "off", + h->yama_ptrace_restricted ? "yes" : "no"); +} diff --git a/core/host.h b/core/host.h new file mode 100644 index 0000000..cd47cf7 --- /dev/null +++ b/core/host.h @@ -0,0 +1,91 @@ +/* + * SKELETONKEY — host fingerprint + * + * Populated once at startup, before any module's detect() runs. Every + * module receives a stable pointer via skeletonkey_ctx.host and can + * consult it without re-parsing /proc, /etc/os-release, uname(2), or + * forking another userns probe. + * + * The struct is deliberately POD (no heap pointers, fixed-size + * arrays) so lifetime reasoning is trivial. A single static instance + * lives in core/host.c; skeletonkey_host_get() returns the same + * pointer on every call. The first call probes; subsequent calls + * are O(1) lookups. + * + * Fields that don't apply on a given platform (e.g. AppArmor sysctls + * on a non-Linux dev build, KPTI on aarch64) stay at their false / + * "?" defaults. Probing is best-effort: a missing sysctl never fails + * the call, just leaves the corresponding bool false. + */ + +#ifndef SKELETONKEY_HOST_H +#define SKELETONKEY_HOST_H + +#include "kernel_range.h" + +#include +#include +#include + +struct skeletonkey_host { + /* ── identity ─────────────────────────────────────────────── */ + + struct kernel_version kernel; /* uname.release parsed */ + char arch[32]; /* uname.machine ("x86_64", "aarch64") */ + char nodename[64]; /* uname.nodename (for log lines) */ + + char distro_id[64]; /* /etc/os-release ID ("ubuntu", "debian", "fedora", "?") */ + char distro_version_id[64]; /* /etc/os-release VERSION_ID ("24.04", "13", "?") */ + char distro_pretty[128]; /* /etc/os-release PRETTY_NAME for log lines */ + + /* ── process state ─────────────────────────────────────────── */ + + uid_t euid; /* geteuid() */ + uid_t real_uid; /* outer uid (defeats userns illusion via /proc/self/uid_map) */ + gid_t egid; /* getegid() */ + char username[64]; /* getpwuid(euid)->pw_name or "" */ + bool is_root; /* euid == 0 */ + bool is_ssh_session; /* SSH_CONNECTION env var set */ + + /* ── platform family ───────────────────────────────────────── */ + + bool is_linux; /* compiled / running on Linux */ + bool is_debian_family; /* /etc/debian_version exists */ + bool is_rpm_family; /* redhat / fedora / rocky / almalinux release file */ + bool is_arch_family; /* /etc/arch-release */ + bool is_suse_family; /* /etc/SuSE-release or /etc/SUSE-brand */ + + /* ── capability / gate flags (Linux) ──────────────────────── */ + + bool unprivileged_userns_allowed; /* fork+unshare(CLONE_NEWUSER) succeeded */ + bool apparmor_restrict_userns; /* sysctl: 1 = AA blocks unpriv userns */ + bool unprivileged_bpf_disabled; /* /proc/sys/kernel/unprivileged_bpf_disabled = 1 */ + bool kpti_enabled; /* /sys/.../meltdown contains "Mitigation: PTI" */ + bool kernel_lockdown_active; /* /sys/kernel/security/lockdown != [none] */ + bool selinux_enforcing; /* /sys/fs/selinux/enforce = 1 */ + bool yama_ptrace_restricted; /* /proc/sys/kernel/yama/ptrace_scope > 0 */ + + /* ── system services ──────────────────────────────────────── */ + + bool has_systemd; /* /run/systemd/system exists */ + bool has_dbus_system; /* /run/dbus/system_bus_socket exists */ + + /* Informational: the SKELETONKEY component that populated this + * snapshot (for log/JSON output). */ + const char *probe_source; +}; + +/* Get the host fingerprint. Returns a stable, non-null pointer that + * lives for the process lifetime. Probes happen lazily on the first + * call (~50ms; dominated by the userns fork-probe), are cached, and + * subsequent calls are free. + * + * Probing is best-effort: missing files / unsupported sysctls leave + * the corresponding bool false. The function does not fail. */ +const struct skeletonkey_host *skeletonkey_host_get(void); + +/* Print a two-line "host fingerprint" banner to stderr suitable for + * --auto / --scan verbose output. Silent on JSON mode. */ +void skeletonkey_host_print_banner(const struct skeletonkey_host *h, bool json); + +#endif /* SKELETONKEY_HOST_H */ diff --git a/core/module.h b/core/module.h index 879d610..6b37e80 100644 --- a/core/module.h +++ b/core/module.h @@ -40,9 +40,12 @@ typedef enum { SKELETONKEY_EXPLOIT_OK = 5, } skeletonkey_result_t; -/* Per-invocation context passed to module callbacks. Lightweight for - * now; will grow as modules need shared state (host fingerprint, - * leaked kbase, etc.). */ +/* Per-invocation context passed to module callbacks. The host + * fingerprint (kernel / distro / capability gates / service presence) + * is populated once at startup by core/host.c and handed to every + * module callback here — see core/host.h. */ +struct skeletonkey_host; /* forward decl; full def in core/host.h */ + struct skeletonkey_ctx { bool no_color; /* --no-color */ bool json; /* --json (machine-readable output) */ @@ -51,6 +54,12 @@ struct skeletonkey_ctx { bool authorized; /* user typed --i-know on exploit */ bool full_chain; /* --full-chain (attempt root-pop after primitive) */ bool dry_run; /* --dry-run (preview only; never call exploit/mitigate/cleanup) */ + + /* Host fingerprint — see core/host.h. Stable pointer, populated + * once by main() before any module callback runs. Modules that + * want to consult it #include "../../core/host.h". May be NULL + * only in degenerate test contexts; main() always sets it. */ + const struct skeletonkey_host *host; }; struct skeletonkey_module { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ef67687..43a22ff 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -82,7 +82,11 @@ Code that more than one module needs lives in `core/`: 1. Parse args (`--scan`, `--exploit `, `--mitigate`, `--detect-rules`, `--cleanup`, etc.) -2. Fingerprint the host +2. **Fingerprint the host** — `core/host.c` is called once at startup + to populate `struct skeletonkey_host` (kernel version + arch + + distro + capability gates + service presence). The result is + handed to every module via `ctx->host`. See "Host fingerprint" + below. 3. For `--scan`: iterate module registry, call each module's `detect()`, emit table of results 4. For `--exploit `: locate module, gate behind `--i-know`, @@ -90,6 +94,44 @@ Code that more than one module needs lives in `core/`: 5. For `--detect-rules`: walk module registry, concatenate detection files in the requested format +## Host fingerprint (`core/host.{h,c}`) + +A single `struct skeletonkey_host` is populated once at startup and +exposed to every module via `ctx->host` (a stable pointer for the +process lifetime). It carries: + +- **Identity:** `struct kernel_version kernel` + arch + nodename + + distro id/version/pretty (parsed from `/etc/os-release`). +- **Process state:** euid, real_uid (defeats the userns illusion by + reading `/proc/self/uid_map`), egid, username, is_root, + is_ssh_session. +- **Platform family:** is_linux, is_debian_family, is_rpm_family, + is_arch_family, is_suse_family. +- **Capability gates (Linux):** unprivileged_userns_allowed (live + fork-probe), apparmor_restrict_userns, unprivileged_bpf_disabled, + kpti_enabled, kernel_lockdown_active, selinux_enforcing, + yama_ptrace_restricted. +- **System services:** has_systemd, has_dbus_system. + +Modules that want to consult the fingerprint do: + +```c +#include "../../core/host.h" +/* ... */ +if (ctx->host && !ctx->host->unprivileged_userns_allowed) + return SKELETONKEY_PRECOND_FAIL; +if (ctx->host->kernel.major < 7) + return SKELETONKEY_OK; /* predates the bug */ +``` + +The migration is opt-in per module — modules that don't `#include` +host.h continue to do their own probes; modules that do save the +duplicate work and get a consistent view across the whole scan. + +`--auto` and `--scan` (in verbose mode) print a two-line banner of +the fingerprint via `skeletonkey_host_print_banner()` so operators +can see at a glance which gates are open. + ## CI matrix `.github/workflows/ci.yml` (planned, Phase 4) runs each module's diff --git a/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c b/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c index 34d2a41..b20500f 100644 --- a/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c +++ b/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c @@ -46,6 +46,7 @@ /* _GNU_SOURCE / _FILE_OFFSET_BITS are passed via -D in the top-level * Makefile; do not redefine here (warning: redefined). */ #include "../../core/kernel_range.h" +#include "../../core/host.h" #include #include #include @@ -684,19 +685,23 @@ static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx) { dd_verbose = !ctx->json; - struct kernel_version v; - if (!kernel_version_current(&v)) { + /* Consult the shared host fingerprint instead of calling + * kernel_version_current() ourselves — populated once at startup + * and identical across every module's detect(). */ + const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL; + if (!v || v->major == 0) { if (!ctx->json) - fprintf(stderr, "[!] dirtydecrypt: could not parse kernel version\n"); + fprintf(stderr, "[!] dirtydecrypt: host fingerprint missing kernel " + "version — bailing\n"); return SKELETONKEY_TEST_ERROR; } /* Predates the bug: rxgk RESPONSE-handling code was added in 7.0. */ - if (v.major < 7) { + if (v->major < 7) { if (!ctx->json) fprintf(stderr, "[i] dirtydecrypt: kernel %s predates the rxgk " "RESPONSE-handling code added in 7.0 — not applicable\n", - v.release); + v->release); return SKELETONKEY_OK; } @@ -718,7 +723,7 @@ static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx) return SKELETONKEY_PRECOND_FAIL; } - bool patched_by_version = kernel_range_is_patched(&dirtydecrypt_range, &v); + bool patched_by_version = kernel_range_is_patched(&dirtydecrypt_range, v); if (ctx->active_probe) { if (!ctx->json) @@ -729,7 +734,7 @@ static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx) if (!ctx->json) fprintf(stderr, "[!] dirtydecrypt: ACTIVE PROBE " "CONFIRMED — rxgk in-place decrypt corrupts " - "the page cache (kernel %s)\n", v.release); + "the page cache (kernel %s)\n", v->release); return SKELETONKEY_VULNERABLE; } if (p == 0) { @@ -748,14 +753,14 @@ static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx) if (!ctx->json) fprintf(stderr, "[+] dirtydecrypt: kernel %s is patched " "(commit a2567217 in Linux 7.0; version-only check — " - "use --active to confirm)\n", v.release); + "use --active to confirm)\n", v->release); return SKELETONKEY_OK; } if (!ctx->json) fprintf(stderr, "[!] dirtydecrypt: kernel %s appears VULNERABLE " "(in 7.0-rc window before commit a2567217; version-only)\n" " Confirm empirically: skeletonkey --scan --active\n", - v.release); + v->release); return SKELETONKEY_VULNERABLE; } diff --git a/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c b/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c index c9d68f9..8fd7f3a 100644 --- a/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c +++ b/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c @@ -53,6 +53,7 @@ /* _GNU_SOURCE / _FILE_OFFSET_BITS are passed via -D in the top-level * Makefile; do not redefine here (warning: redefined). */ #include "../../core/kernel_range.h" +#include "../../core/host.h" #include #include #include @@ -837,19 +838,10 @@ static void fg_evict(const char *path) if (dc >= 0) { if (write(dc, "3\n", 2) < 0) {} close(dc); } } -/* --- precondition check: unprivileged user namespaces --- */ - -static bool fg_userns_allowed(void) -{ - pid_t pid = fork(); - if (pid < 0) - return false; - if (pid == 0) - _exit(unshare(CLONE_NEWUSER) == 0 ? 0 : 1); - int st; - waitpid(pid, &st, 0); - return WIFEXITED(st) && WEXITSTATUS(st) == 0; -} +/* The unprivileged-userns precondition is now read from the shared + * host fingerprint (ctx->host->unprivileged_userns_allowed), which + * probes once at startup via core/host.c. The previous per-detect + * fork-probe helper was removed. */ /* ---- detect ------------------------------------------------------- */ @@ -927,18 +919,24 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx) { fg_verbose = !ctx->json; - struct kernel_version v; - if (!kernel_version_current(&v)) { + /* Pull kernel version and userns availability from the shared + * host fingerprint — populated once at startup, no per-detect + * fork or re-parse. */ + const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL; + if (!v || v->major == 0) { if (!ctx->json) - fprintf(stderr, "[!] fragnesia: could not parse kernel version\n"); + fprintf(stderr, "[!] fragnesia: host fingerprint missing kernel " + "version — bailing\n"); return SKELETONKEY_TEST_ERROR; } - if (!fg_userns_allowed()) { + if (!ctx->host->unprivileged_userns_allowed) { if (!ctx->json) fprintf(stderr, "[i] fragnesia: unprivileged user " - "namespaces are disabled — XFRM gate closed " - "here (CAP_NET_ADMIN unreachable)\n"); + "namespaces are disabled (host fingerprint) — " + "XFRM gate closed here (CAP_NET_ADMIN unreachable)%s\n", + ctx->host->apparmor_restrict_userns ? + "; AppArmor restriction is on" : ""); return SKELETONKEY_PRECOND_FAIL; } @@ -950,7 +948,7 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx) return SKELETONKEY_PRECOND_FAIL; } - bool patched_by_version = kernel_range_is_patched(&fragnesia_range, &v); + bool patched_by_version = kernel_range_is_patched(&fragnesia_range, v); if (ctx->active_probe) { if (!ctx->json) @@ -961,7 +959,7 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx) if (!ctx->json) fprintf(stderr, "[!] fragnesia: ACTIVE PROBE " "CONFIRMED — ESP-in-TCP coalesce corrupts " - "the page cache (kernel %s)\n", v.release); + "the page cache (kernel %s)\n", v->release); return SKELETONKEY_VULNERABLE; } if (p == 0) { @@ -982,14 +980,14 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx) if (!ctx->json) fprintf(stderr, "[+] fragnesia: kernel %s is patched " "(7.0.9+; version-only check — use --active to " - "confirm)\n", v.release); + "confirm)\n", v->release); return SKELETONKEY_OK; } if (!ctx->json) fprintf(stderr, "[!] fragnesia: kernel %s appears VULNERABLE " "(no backport entry for this branch; version-only)\n" " Confirm empirically: skeletonkey --scan --active\n", - v.release); + v->release); return SKELETONKEY_VULNERABLE; } diff --git a/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c b/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c index 7912650..a29d516 100644 --- a/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c +++ b/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c @@ -47,6 +47,7 @@ #ifdef __linux__ #include "../../core/kernel_range.h" +#include "../../core/host.h" #include #include #include @@ -132,10 +133,18 @@ static skeletonkey_result_t overlayfs_detect(const struct skeletonkey_ctx *ctx) /* Ubuntu-specific bug. Non-Ubuntu kernels are largely immune * because upstream didn't enable the userns-mount path until - * 5.11. Bail early for non-Ubuntu. */ - if (!is_ubuntu()) { + * 5.11. Bail early for non-Ubuntu. Consult the shared host + * fingerprint (distro_id == "ubuntu" — populated once at startup; + * the local is_ubuntu() helper is preserved for symmetry / future + * standalone use but the dispatcher path goes through ctx->host). */ + bool ubuntu = ctx->host + ? (strcmp(ctx->host->distro_id, "ubuntu") == 0) + : is_ubuntu(); + if (!ubuntu) { if (!ctx->json) { - fprintf(stderr, "[+] overlayfs: not Ubuntu — bug is Ubuntu-specific\n"); + fprintf(stderr, "[+] overlayfs: not Ubuntu (distro=%s) — bug is " + "Ubuntu-specific\n", + ctx->host ? ctx->host->distro_id : "?"); } return SKELETONKEY_OK; } diff --git a/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c b/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c index d0bd9a3..07a11a8 100644 --- a/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c +++ b/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c @@ -61,6 +61,7 @@ /* _GNU_SOURCE / _FILE_OFFSET_BITS are passed via -D in the top-level * Makefile; do not redefine here. */ +#include "../../core/host.h" #include #include #include @@ -310,10 +311,18 @@ static skeletonkey_result_t p2tr_detect(const struct skeletonkey_ctx *ctx) return SKELETONKEY_OK; } - if (access("/etc/debian_version", F_OK) != 0) { + /* Host fingerprint short-circuits — populated once at startup. */ + if (ctx->host && !ctx->host->is_debian_family) { if (!ctx->json) - fprintf(stderr, "[i] pack2theroot: not a Debian/Ubuntu host " - "(PoC's .deb builder is Debian-family-only)\n"); + fprintf(stderr, "[i] pack2theroot: not a Debian-family host " + "(distro=%s) — PoC's .deb builder is Debian-only\n", + ctx->host->distro_id); + return SKELETONKEY_PRECOND_FAIL; + } + if (ctx->host && !ctx->host->has_dbus_system) { + if (!ctx->json) + fprintf(stderr, "[i] pack2theroot: no system D-Bus socket at " + "/run/dbus/system_bus_socket — PackageKit unreachable\n"); return SKELETONKEY_PRECOND_FAIL; } diff --git a/skeletonkey.c b/skeletonkey.c index bce5b45..b5e7180 100644 --- a/skeletonkey.c +++ b/skeletonkey.c @@ -18,6 +18,7 @@ #include "core/module.h" #include "core/registry.h" #include "core/offsets.h" +#include "core/host.h" #include #include @@ -724,32 +725,8 @@ static skeletonkey_result_t run_detect_isolated( return SKELETONKEY_TEST_ERROR; } -/* Best-effort host distro fingerprint via /etc/os-release. Populates - * id_out and ver_out with up to 63 chars each; falls back to "?" when - * /etc/os-release is missing or unparseable. */ -static void read_os_release(char *id_out, size_t id_cap, - char *ver_out, size_t ver_cap) -{ - snprintf(id_out, id_cap, "?"); - snprintf(ver_out, ver_cap, "?"); - FILE *f = fopen("/etc/os-release", "r"); - if (!f) return; - char line[256]; - while (fgets(line, sizeof line, f)) { - const char *key = NULL; char *dst = NULL; size_t cap = 0; - if (strncmp(line, "ID=", 3) == 0) { - key = line + 3; dst = id_out; cap = id_cap; - } else if (strncmp(line, "VERSION_ID=", 11) == 0) { - key = line + 11; dst = ver_out; cap = ver_cap; - } else continue; - const char *v = key; - if (*v == '"' || *v == '\'') v++; - size_t L = strcspn(v, "\"'\n"); - if (L >= cap) L = cap - 1; - memcpy(dst, v, L); dst[L] = '\0'; - } - fclose(f); -} +/* Host fingerprint parsing (ID / VERSION_ID / kernel / arch) lives in + * core/host.c; cmd_auto consults ctx->host via the shared banner. */ static int cmd_auto(struct skeletonkey_ctx *ctx) { @@ -776,11 +753,8 @@ static int cmd_auto(struct skeletonkey_ctx *ctx) bool prev_active = ctx->active_probe; ctx->active_probe = true; - struct utsname u; uname(&u); - char distro_id[64], distro_ver[64]; - read_os_release(distro_id, sizeof distro_id, distro_ver, sizeof distro_ver); - fprintf(stderr, "[*] auto: host=%s distro=%s/%s kernel=%s arch=%s\n", - u.nodename, distro_id, distro_ver, u.release, u.machine); + /* Two-line host fingerprint banner (identity + capability gates). */ + skeletonkey_host_print_banner(ctx->host, ctx->json); fprintf(stderr, "[*] auto: active probes enabled — brief /tmp file " "touches and fork-isolated namespace probes\n"); fprintf(stderr, "[*] auto: scanning %zu modules for vulnerabilities...\n", @@ -958,6 +932,12 @@ int main(int argc, char **argv) const char *target = NULL; int i_know = 0; + /* Probe the host once, up front. ctx.host is a stable pointer + * shared by every module callback; populating now means each + * detect() sees the same fingerprint and no module has to re-do + * uname/getpwuid/sysctl reads. See core/host.{h,c}. */ + ctx.host = skeletonkey_host_get(); + enum detect_format dr_fmt = FMT_AUDITD; static struct option longopts[] = { {"scan", no_argument, 0, 'S'},