2b1e96336e
Three coordinated changes that build on the host_kernel_at_least
landed in 1571b88:
1. core/host gains skeletonkey_host_kernel_in_range(h, lo..., hi...)
— a [lo, hi) bounded-interval check for modules that want the
'vulnerable window' semantics directly. Implemented in terms of
host_kernel_at_least (so the comparison logic stays in one place).
No module uses it yet; available for new modules that want it.
2. 13 modules migrated off the manual
if (v->major < X || (v->major == X && v->minor < Y)) { ... }
pattern onto
if (!skeletonkey_host_kernel_at_least(ctx->host, X, Y, 0)) { ... }
One-line replacements, mechanical, no behavior change.
Migrated: af_packet2, dirty_pipe, fuse_legacy, netfilter_xtcompat,
nf_tables, nft_fwd_dup, nft_payload, nft_set_uaf, overlayfs,
overlayfs_setuid, ptrace_traceme, stackrot, vmwgfx. The repo now
has zero manual 'v->major < X' patterns — every predates-check
reads the same way.
3. tests/test_detect.c expanded from 17 to 29 cases. Adds:
Above-fix coverage on h_kernel_6_12 (10 modules previously
untested): af_packet, af_packet2, af_unix_gc, netfilter_xtcompat,
nft_set_uaf, nft_fwd_dup, nft_payload, stackrot, sequoia, vmwgfx.
Ancient-kernel predates coverage on h_kernel_4_4 (2 more cases):
nft_set_uaf (introduced 5.1), stackrot (introduced 6.1).
Detect-path test coverage now spans most of the corpus that
has a testable host-fingerprint gate. Untested modules from
here on are either userspace bugs whose detect() doesn't gate
on host fields (pwnkit, sudo_samedit, sudoedit_editor),
entrybleed (sysfs-direct, no host gate), or the copy_fail_family
bridge (no ctx->host integration yet).
Verification: Linux (docker gcc:latest, non-root user): 29/29 pass.
macOS (local): 31-module build clean, suite reports 'skipped —
Linux-only' as designed.
128 lines
6.0 KiB
C
128 lines
6.0 KiB
C
/*
|
|
* 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 <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <sys/types.h>
|
|
|
|
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);
|
|
|
|
/* True iff h->kernel >= the (major, minor, patch) provided. Returns
|
|
* false if h is NULL or its kernel version was never populated (major
|
|
* == 0). Replaces the manual `v->major < X` / `(v->major == X &&
|
|
* v->minor < Y)` patterns scattered across detect()s — cleaner reads
|
|
* and one place to get the comparison right.
|
|
*
|
|
* Examples:
|
|
* if (!host_kernel_at_least(h, 7, 0, 0)) // kernel predates 7.0
|
|
* return SKELETONKEY_OK;
|
|
* if ( host_kernel_at_least(h, 6, 8, 0)) // kernel post-fix
|
|
* return SKELETONKEY_OK;
|
|
*/
|
|
bool skeletonkey_host_kernel_at_least(const struct skeletonkey_host *h,
|
|
int major, int minor, int patch);
|
|
|
|
/* True iff h->kernel is in [lo, hi). Useful for "vulnerable range"
|
|
* gates where the simple `kernel_range_is_patched` backport model
|
|
* doesn't apply — e.g. a feature added in X.Y and removed/superseded
|
|
* in W.Z, or a per-module "vulnerable only on these specific kernel
|
|
* lines" check.
|
|
*
|
|
* Equivalent to:
|
|
* host_kernel_at_least(h, lo...) && !host_kernel_at_least(h, hi...)
|
|
*
|
|
* For "predates the bug" alone use host_kernel_at_least directly; the
|
|
* `in_range` form is for the bounded interval case.
|
|
*
|
|
* Example:
|
|
* if (host_kernel_in_range(h, 5, 8, 0, 5, 17, 0))
|
|
* // kernel 5.8 ≤ K < 5.17 — vulnerable window per the mainline
|
|
* // introduction/fix dates (ignoring stable backports)
|
|
*/
|
|
bool skeletonkey_host_kernel_in_range(const struct skeletonkey_host *h,
|
|
int lo_major, int lo_minor, int lo_patch,
|
|
int hi_major, int hi_minor, int hi_patch);
|
|
|
|
#endif /* SKELETONKEY_HOST_H */
|