8 Commits

Author SHA1 Message Date
leviathan 5d48a7b0b5 release v0.7.1: arm64-static binary + per-module arch_support
Two additions on top of v0.7.0:

1. skeletonkey-arm64-static is now published alongside the existing
   x86_64-static binary. Built native-arm64 in Alpine via GitHub's
   ubuntu-24.04-arm runner pool (free for public repos as of 2024).
   install.sh auto-picks it based on 'uname -m'; SKELETONKEY_DYNAMIC=1
   fetches the dynamic build instead. Works on Raspberry Pi 4+, Apple
   Silicon Linux VMs, AWS Graviton, Oracle Ampere, Hetzner ARM, etc.

   .github/workflows/release.yml refactor: the previous single
   build-static-x86_64 job becomes a build-static matrix with two
   entries (x86_64-static on ubuntu-latest, arm64-static on
   ubuntu-24.04-arm). Both share the same Alpine container + build
   recipe.

2. .arch_support field on struct skeletonkey_module — honest per-module
   labeling of which architectures the exploit() body has been verified
   on. Three categories:

     'any' (4 modules): pwnkit, sudo_samedit, sudoedit_editor,
       pack2theroot. Purely userspace; arch-independent.

     'x86_64' (1 module): entrybleed. KPTI prefetchnta side-channel;
       x86-only by physics. Already source-gated (returns
       PRECOND_FAIL on non-x86_64).

     'x86_64+unverified-arm64' (26 modules): kernel exploitation
       code. The bug class is generic but the exploit primitives
       (msg_msg sprays, finisher chain, struct offsets) haven't been
       confirmed on arm64. detect() still works (just reads ctx->host);
       only the --exploit path is in question.

   --list now has an ARCH column (any / x64 / x64?) and the footer
   prints 'N arch-independent (any)'.
   --module-info prints 'arch support: <value>'.
   --scan --json adds 'arch_support' to each module record.

This is the honest 'arm64 works for detection on every module +
exploitation on 4 of them today; the rest await empirical arm64
sweep' framing — not pretending the kernel exploits already work
there, but not blocking the arm64 binary on that either. arm64
users get the full triage workflow + a handful of userspace exploits
out of the box, plus a clear roadmap for the rest.

Future work to promote modules from 'x86_64+unverified-arm64' to
'any': add an arm64 Vagrant box (generic/debian12-arm64 etc.) to
tools/verify-vm/ and run a verification sweep on Apple Silicon /
ARM Linux hardware.
2026-05-23 21:10:54 -04:00
leviathan e4a600fef2 module metadata: CWE + ATT&CK + CISA KEV triage from federal sources
Adds per-CVE triage annotations that turn SKELETONKEY's JSON output
into something a SIEM/CTI/threat-intel pipeline can route on, and a
KEV badge in --list so operators see at-a-glance which modules
cover actively-exploited bugs.

New tool — tools/refresh-cve-metadata.py:

  - Discovers CVEs by scanning modules/<dir>/ (no hardcoded list).
  - Fetches CISA's Known Exploited Vulnerabilities catalog
    (https://www.cisa.gov/.../known_exploited_vulnerabilities.csv).
  - Fetches CWE classifications from NVD's CVE API 2.0
    (services.nvd.nist.gov), throttled to the anonymous
    5-req/30s limit (~3 minutes for 26 CVEs).
  - Hand-curated ATT&CK technique mapping (T1068 default; T1611 for
    container escapes, T1082 for kernel info leaks — MITRE doesn't
    publish a clean CVE→technique feed).
  - Generates three outputs:
      docs/CVE_METADATA.json   machine-readable, drift-checkable
      docs/KEV_CROSSREF.md     human-readable table
      core/cve_metadata.c      auto-generated lookup table
  - --check mode diffs the committed JSON against a fresh fetch for
    CI drift detection.

New core API — core/cve_metadata.{h,c}:

  struct cve_metadata { cve, cwe, attack_technique, attack_subtechnique,
                        in_kev, kev_date_added };
  const struct cve_metadata *cve_metadata_lookup(const char *cve);

Lookup keyed by CVE id, not module name — the metadata is properties
of the CVE (two modules covering the same bug see the same metadata).
The opsec_notes field stays on the module struct because exploit
technique varies per-module (different footprints).

Output surfacing:
  - --list: new KEV column shows ★ for KEV-listed CVEs.
  - --module-info (text): prints cwe / att&ck / 'in CISA KEV: YES (added
    YYYY-MM-DD)' between summary and operations.
  - --module-info / --scan (JSON): emits a 'triage' subobject with the
    full record, plus an 'opsec_notes' field at top level when set.

Initial snapshot:
  - 10 of 26 modules cover KEV-listed CVEs (dirty_cow, dirty_pipe,
    pwnkit, sudo_samedit, ptrace_traceme, fuse_legacy, nf_tables,
    overlayfs, overlayfs_setuid, netfilter_xtcompat).
  - 24 of 26 have NVD CWE mappings; 2 unmapped (NVD has no weakness
    record for CVE-2019-13272 and CVE-2026-46300 yet).
  - All 26 mapped to an ATT&CK technique.

Verification:
  - macOS local: 33 kernel_range + clean build, --module-info shows
    'in CISA KEV: YES (added 2024-05-30)' for nf_tables, --list KEV
    column renders.
  - Linux (docker gcc:latest): 33 + 54 = 87 passes, 0 fails.

Follow-up commits will add per-module OPSEC notes and --explain mode.
2026-05-23 10:38:01 -04:00
leviathan 4f30d00a1c core/host: shared host fingerprint refactor
Adds core/host.{h,c} — a single struct skeletonkey_host populated once
at startup and handed to every module callback via ctx->host. Replaces
the per-detect uname / /etc/os-release / sysctl / userns-fork-probe
calls scattered across the corpus with O(1) cached lookups, and gives
the dispatcher one consistent view of the host.

What's in the fingerprint:

- Identity: kernel_version (parsed from uname.release), arch (machine),
  nodename, distro_id / distro_version_id / distro_pretty (parsed once
  from /etc/os-release).
- Process state: euid, real_uid (defeats userns illusion via
  /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 (file-existence checks once).
- Capability gates (Linux): unprivileged_userns_allowed (live
  fork+unshare probe), apparmor_restrict_userns,
  unprivileged_bpf_disabled, kpti_enabled, kernel_lockdown_active,
  selinux_enforcing, yama_ptrace_restricted.
- System services: has_systemd, has_dbus_system.

Wiring:

- core/module.h forward-declares struct skeletonkey_host and adds the
  pointer to skeletonkey_ctx. Modules opt-in by including
  ../../core/host.h.
- core/host.c is fully POD (no heap pointers) — uses a single file-
  static instance, returns a stable pointer on every call. Lazily
  populated on first skeletonkey_host_get().
- skeletonkey.c calls skeletonkey_host_get() at main() entry, stores
  in ctx.host before any register_*() runs.
- cmd_auto's bespoke distro-fingerprint code (was an inline
  read_os_release helper) is replaced with skeletonkey_host_print_banner(),
  which emits a two-line banner of identity + capability gates.

Migrations:

- dirtydecrypt: kernel_version_current() -> ctx->host->kernel.
- fragnesia: removed local fg_userns_allowed() fork-probe in favour of
  ctx->host->unprivileged_userns_allowed (no per-scan fork). Also
  pulls kernel from ctx->host. The PRECOND_FAIL message now notes
  whether AppArmor restriction is on.
- pack2theroot: access('/etc/debian_version') -> ctx->host->is_debian_family;
  also short-circuits when ctx->host->has_dbus_system is false (saves
  the GLib g_bus_get_sync attempt on systems without system D-Bus).
- overlayfs: replaced the inline is_ubuntu() /etc/os-release parser
  with ctx->host->distro_id comparison. Local helper preserved for
  symmetry / standalone builds.

Documentation: docs/ARCHITECTURE.md gains a 'Host fingerprint'
section describing the struct, the opt-in include pattern, and
example detect() usage. ROADMAP --auto accuracy log notes the
landing and flags remaining modules as an incremental follow-up.

Build verification:

- macOS (local): make clean && make -> Mach-O x86_64, 31 modules,
  banner prints with distro=?/? (no /etc/os-release).
- Linux (docker gcc:latest + libglib2.0-dev): make clean && make ->
  ELF 64-bit, 31 modules. Banner prints with kernel + distro=debian/13
  + 7 capability gates. dirtydecrypt correctly says 'predates the
  rxgk code added in 7.0'; fragnesia PRECOND_FAILs with
  '(host fingerprint)' annotation; pack2theroot PRECOND_FAILs on
  no-DBus; overlayfs reports 'not Ubuntu (distro=debian)'.
2026-05-22 23:18:00 -04:00
leviathan 3e6e0d869b skeletonkey: add --dry-run flag
Preview-only mode for --auto / --exploit / --mitigate / --cleanup.
Walks the full scan (with active probes, fork isolation, verdict
table — everything the real --auto does) and prints what would be
launched, without ever calling the exploit/mitigate/cleanup callback.

Wiring:
- struct skeletonkey_ctx gains a 'dry_run' field (core/module.h).
- Long option --dry-run, getopt case 10.
- cmd_auto: after picking the safest, if dry_run, print
    [*] auto: --dry-run: would launch `--exploit <NAME> --i-know`; not firing.
  plus the remaining ranked candidates, then return 0.
- cmd_one (used for --exploit/--mitigate/--cleanup) shorts on dry_run
  with [*] <module>: --dry-run: would run --<op>; not firing.

UX: --auto --dry-run does NOT require --i-know (nothing fires). The
refusal message for bare --auto now points to --dry-run for the
preview path:
  [-] --auto requires --i-know (or --dry-run for a preview that never fires).

ROADMAP --auto accuracy section updated with the dry-run + the
version-pinned detect work from the previous commit.

Smoke-tested locally on macOS: scanning runs, verdicts print, the
'would launch' line fires, exit 0.
2026-05-22 23:08:24 -04:00
leviathan 9593d90385 rename: IAMROOT → SKELETONKEY across the entire project
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
Breaking change. Tool name, binary name, function/type names,
constant names, env vars, header guards, file paths, and GitHub
repo URL all rebrand IAMROOT → SKELETONKEY.

Changes:
  - All "IAMROOT" → "SKELETONKEY" (constants, env vars, enum
    values, docs, comments)
  - All "iamroot" → "skeletonkey" (functions, types, paths, CLI)
  - iamroot.c → skeletonkey.c
  - modules/*/iamroot_modules.{c,h} → modules/*/skeletonkey_modules.{c,h}
  - tools/iamroot-fleet-scan.sh → tools/skeletonkey-fleet-scan.sh
  - Binary "iamroot" → "skeletonkey"
  - GitHub URL KaraZajac/IAMROOT → KaraZajac/SKELETONKEY
  - .gitignore now expects build output named "skeletonkey"
  - /tmp/iamroot-* tmpfiles → /tmp/skeletonkey-*
  - Env vars IAMROOT_MODPROBE_PATH etc. → SKELETONKEY_*

New ASCII skeleton-key banner (horizontal key icon + ANSI Shadow
SKELETONKEY block letters) replaces the IAMROOT banner in
skeletonkey.c and README.md.

VERSION: 0.3.1 → 0.4.0 (breaking).

Build clean on Debian 6.12.86. `skeletonkey --version` → 0.4.0.
All 24 modules still register; no functional code changes — pure
rename + banner refresh.
2026-05-16 22:43:49 -04:00
leviathan 125ce8a08b core: add shared finisher + offset resolver + --full-chain flag
Adds the infrastructure the 7 🟡 PRIMITIVE modules can wire into for
full-chain root pops.

  core/offsets.{c,h}: four-source kernel-symbol resolution chain
    1. env vars (IAMROOT_MODPROBE_PATH, IAMROOT_INIT_TASK, …)
    2. /proc/kallsyms (only useful when kptr_restrict=0 or root)
    3. /boot/System.map-$(uname -r) (world-readable on some distros)
    4. embedded table keyed by uname-r glob (entries are
       relative-to-_text, applied on top of an EntryBleed kbase leak;
       seeded empty in v0.2.0 — schema-only — to honor the
       no-fabricated-offsets rule).

  core/finisher.{c,h}: shared root-pop helpers given a module's
    arb-write primitive.
      Pattern A (modprobe_path):
        write payload script /tmp/iamroot-mp-<pid>.sh, arb-write
        modprobe_path ← that path, execve unknown-format trigger,
        wait for /tmp/iamroot-pwn-<pid> sentinel + setuid bash copy,
        spawn root shell.
      Pattern B (cred uid): stub — needs arb-READ too; modules use
        Pattern A unless they have read+write.
    On offset-resolution failure: prints a verbose how-to-populate
    diagnostic and returns EXPLOIT_FAIL honestly.

  core/module.h: + bool full_chain in iamroot_ctx

  iamroot.c: + --full-chain flag (longopt 7, sets ctx.full_chain)
             + help text describing primitive-only-by-default + the
               opt-in to attempt the full chain.

  Makefile: add core/offsets.o + core/finisher.o to CORE_SRCS.

Build clean on Debian 6.12.86; --help renders the new flag.
2026-05-16 21:56:03 -04:00
leviathan cee368d5a4 Phase 5: --detect-rules export with dedup
- core/module.h: struct iamroot_module gains detect_{auditd,sigma,yara,falco}
  fields. NULL = module doesn't ship a rule for that format.
  Embedded as C string literals in each module's iamroot_modules.c so
  the binary is self-contained (no data-dir install needed).
- iamroot.c: --detect-rules [--format=<f>] command. Walks module
  registry, deduplicates by pointer (family-shared rules emit once,
  siblings get a 'see family rules above' marker), writes to stdout
  for redirect into /etc/audit/rules.d/ or SIEM ingestion.
- Embedded rules for:
  - copy_fail_family (shared across 5 modules): auditd watches on
    passwd/shadow/sudoers/su + AF_ALG socket creation + xfrm setsockopt;
    Sigma rule covers the file-modification footprint.
  - dirty_pipe: auditd watches on same files + splice() syscalls;
    Sigma rule for non-root file modification.
  - entrybleed: Sigma INFORMATIONAL note (side-channel — no syscall
    trace; reliable detection needs perf-counter EDR).

Verified end-to-end on kctf-mgr:
  iamroot --detect-rules --format=auditd → 2 / 7 rules emit (deduped)
  iamroot --detect-rules --format=sigma  → 2 / 7 rules emit
2026-05-16 19:58:26 -04:00
leviathan 52e8c99022 Phase 1: module interface + registry + top-level dispatcher
- core/module.h: struct iamroot_module + iamroot_result_t
- core/registry.{h,c}: flat-array module registry with find-by-name
- modules/copy_fail_family/iamroot_modules.{h,c}: bridge layer
  exposing 5 modules (copy_fail, copy_fail_gcm, dirty_frag_esp,
  dirty_frag_esp6, dirty_frag_rxrpc) wired to the absorbed DIRTYFAIL
  detect/exploit functions; df_result_t/iamroot_result_t share numeric
  values intentionally for zero-cost translation
- iamroot.c: top-level CLI dispatcher with --scan / --list / --exploit /
  --mitigate / --cleanup, JSON output, --i-know gate
- Restored modules/copy_fail_family/src/ structure (DIRTYFAIL Makefile
  expects it; the initial flat copy broke that contract)
- Top-level Makefile builds one binary; filters out DIRTYFAIL's
  original dirtyfail.c main so it doesn't conflict with iamroot.c

Verified end-to-end on kctf-mgr (Linux): clean compile, 5 modules
register, --scan --json output ingest-ready, exit codes propagate.
2026-05-16 19:32:11 -04:00