1571b88725
core/host helper:
- Adds bool skeletonkey_host_kernel_at_least(h, M, m, p) — the
canonical 'kernel >= X.Y.Z' check. Replaces the manual
'v->major < X || (v->major == X && v->minor < Y)' pattern that
many modules use for their 'predates the bug' pre-check. Returns
false when h is NULL or h->kernel.major == 0 (degenerate cases),
true otherwise iff the host kernel sorts at or above the supplied
version.
- dirtydecrypt migrated as the demo: the 'kernel < 7.0 → predates'
pre-check now reads 'if (!host_kernel_at_least(ctx->host, 7, 0, 0))'.
Other modules still using the manual pattern continue to work
unchanged; migrating them is incremental polish.
tests/test_detect.c expansion (8 → 17 cases):
New fingerprints:
- h_kernel_4_4 — ancient (Linux 4.4 LTS); used for 'predates the
bug' on dirty_pipe.
- h_kernel_6_12 — recent (Linux 6.12 LTS); above every backport
threshold in the corpus — modules report OK via
the 'patched by mainline inheritance' branch of
kernel_range_is_patched.
- h_kernel_5_14_no_userns — vulnerable-era kernel (5.14.0, past
every relevant predates check while below every
backport entry) with unprivileged_userns_allowed
deliberately false; lets the userns gate fire
after the version check confirms vulnerable.
New tests (9):
- dirty_pipe + kernel 4.4 → OK (predates 5.8 introduction)
- dirty_pipe + kernel 6.12 → OK (above every backport)
- dirty_cow + kernel 6.12 → OK (above 4.9 fix)
- ptrace_traceme + kernel 6.12 → OK (above 5.1.17 fix)
- cgroup_release_agent + kernel 6.12 → OK (above 5.17 fix)
- nf_tables + vuln kernel + userns=false → PRECOND_FAIL
- fuse_legacy + vuln kernel + userns=false → PRECOND_FAIL
- cls_route4 + vuln kernel + userns=false → PRECOND_FAIL
- overlayfs_setuid + vuln kernel + userns=false → PRECOND_FAIL
Process note: initial 8th and 9th userns tests failed because the
chosen test kernel (5.10.0) tripped each module's predates check
(nf_tables bug introduced 5.14; overlayfs_setuid 5.11). Switched to
5.14.0, which is past every predates threshold AND below every
backport entry in this batch — the version verdict is now genuinely
'vulnerable' and the userns gate fires next. The bug-finding tests
caught a real-but-narrow modeling gap in the original picks.
Verification:
- Linux (docker gcc:latest, non-root user): 17/17 pass.
- macOS (local): builds clean, suite reports 'skipped — Linux-only'
as designed.
107 lines
5.1 KiB
C
107 lines
5.1 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);
|
|
|
|
#endif /* SKELETONKEY_HOST_H */
|