15 Commits

Author SHA1 Message Date
leviathan 97be306fd2 release: bump version to v0.6.0
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
This release captures the session's reliability + accuracy work
on top of v0.5.0:

- Shared host fingerprint (core/host.{h,c}): kernel/distro/userns
  gates / sudo + polkit versions, populated once at startup; every
  module consults ctx->host instead of doing its own probes.
- Test harness (tests/test_detect.c, make test): 44 unit tests over
  mocked host fingerprints, wired into CI as a non-root step.
- --auto upgrades: auto-enables --active, per-detect 15s timeout,
  fork-isolated detect + exploit so a crashing module can't tear
  down the dispatcher, per-module verdict table + scan summary.
- --dry-run flag (preview without firing; --i-know not required).
- Pinned mainline fix commits for the 3 ported modules
  (dirtydecrypt / fragnesia / pack2theroot) — detect() is now
  version-pinned with kernel_range tables, not precondition-only.
- New modules: dirtydecrypt (CVE-2026-31635), fragnesia
  (CVE-2026-46300), pack2theroot (CVE-2026-41651).
- macOS dev build works for the first time (all Linux-only code
  wrapped in #ifdef __linux__).
- docs/JSON_SCHEMA.md: stable consumer contract for --scan --json.

Version bump:
- SKELETONKEY_VERSION = '0.6.0' in skeletonkey.c
- README status line updated with the v0.6.0 changelog
- docs/JSON_SCHEMA.md example refreshed
2026-05-23 00:22:18 -04:00
leviathan c63ee72aa1 docs: JSON output schema (consumer contract for --scan --json)
Adds docs/JSON_SCHEMA.md documenting the shape and stability promises
of the JSON document --scan --json emits on stdout. The schema is
already what the binary produces — this commit pins the contract so
fleet-scan / SIEM consumers can rely on it across releases.

What it covers:
- Top-level object: { version, modules } and field stability.
- Per-module entry: { name, cve, result } with type + stability.
- The 6-value result enum (OK / TEST_ERROR / VULNERABLE /
  EXPLOIT_FAIL / PRECOND_FAIL / EXPLOIT_OK) and what each means
  semantically.
- Process exit-code semantics for --scan (worst observed result
  becomes the exit code — lets a SIEM treat the binary exit as a
  single-host alert level).
- Bash + jq one-liners for the common fleet-roll-up patterns.
- A recommended Python consumer pattern with the forward-compat
  guidance (ignore unknown fields, treat unknown result strings as
  TEST_ERROR-equivalent).
- Explicit stability promises: which fields cannot change without
  a major-version bump, what may be added in future minor
  releases, what consumers MUST tolerate.

Verified against the live binary: --scan --json produces exactly
the documented shape (top-level keys {modules, version}; per-module
keys {cve, name, result}; result values come from the documented
enum). 31 modules / 30 unique CVEs at v0.5.0.

README's 'Sysadmins' audience row now links the schema doc:
'JSON output for CI gates ([schema](docs/JSON_SCHEMA.md))'.
2026-05-23 00:07:45 -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 9a4cc91619 pack2theroot (CVE-2026-41651) + --auto accuracy work
Adds the third ported module — Pack2TheRoot, a userspace PackageKit
D-Bus TOCTOU LPE — and spends real effort hardening --auto so its
detect step gives an accurate, robust verdict before deploying.

pack2theroot (CVE-2026-41651):
- Ported from the public Vozec PoC
  (github.com/Vozec/CVE-2026-41651). Original disclosure by the
  Deutsche Telekom security team.
- Two back-to-back InstallFiles D-Bus calls (SIMULATE then NONE)
  overwrite the cached transaction flags between polkit auth and
  dispatch. GLib priority ordering makes the overwrite deterministic,
  not a timing race; postinst of the malicious .deb drops a SUID bash
  in /tmp.
- detect() reads PackageKit's VersionMajor/Minor/Micro directly over
  D-Bus and compares against the pinned fix release 1.3.5 (commit
  76cfb675). This is a high-confidence verdict, not precondition-only.
- Debian-family only (PoC builds its own .deb in pure C; ar/ustar/
  gzip-stored inline). Cleanup removes /tmp .debs + best-effort
  unlinks /tmp/.suid_bash + sudo -n dpkg -r the staging packages.
- Adds an optional GLib/GIO build dependency. The top-level Makefile
  autodetects via `pkg-config gio-2.0`; when absent the module
  compiles as a stub returning PRECOND_FAIL.
- Embedded auditd + sigma rules cover the file-side footprint
  (/tmp/.suid_bash, /tmp/.pk-*.deb, non-root dpkg/apt execve).

--auto accuracy improvements:
- Auto-enables --active before the scan. Per-module sentinel probes
  (page-cache /tmp files, fork-isolated namespace mounts) turn
  version-only checks into definitive verdicts, so silent distro
  backports don't fool the scan and --auto won't pick blind on
  TEST_ERROR.
- Per-module verdict printing — every module's result is shown
  (VULNERABLE / patched / precondition / indeterminate), not just
  VULNERABLE rows. Operator sees the full picture.
- Scan-end summary line: "N vulnerable, M patched/n.a., K
  precondition-fail, L indeterminate" with a separate callout when
  modules crashed.
- Distro fingerprint added to the auto banner (ID + VERSION_ID from
  /etc/os-release alongside kernel/arch).
- Fork-isolated detect() — each detector runs in a child process so
  a SIGILL/SIGSEGV in one module's probe is contained and the scan
  continues. Surfaced live while testing: entrybleed's prefetchnta
  KASLR sweep SIGILLs on emulated CPUs (linuxkit on darwin); without
  isolation the whole --auto died at module 7 of 31. With isolation
  the scan reports "detect() crashed (signal 4) — continuing" and
  finishes cleanly.

module_safety_rank additions:
- pack2theroot: 95 (userspace D-Bus TOCTOU; dpkg + /tmp SUID footprint
  — clean but heavier than pwnkit's gconv-modules-only path).
- dirtydecrypt / fragnesia: 86 (page-cache writes; one step below the
  verified copy_fail/dirty_frag family at 88 to prefer verified
  modules when both apply).

Docs:
- README badge / tagline / tier table /  block / example output /
  v0.5.0 status — all updated to "28 verified + 3 ported".
- CVES.md counts line, the ported-modules note (now calling out
  pack2theroot's high-confidence detect vs. precondition-only for
  the page-cache pair), inventory row, operations table row.
- ROADMAP Phase 7+: pack2theroot moved out of carry-overs into the
  "landed (ported, pending VM verification)" group; added a new
  "--auto accuracy work" subsection documenting the dispatcher
  hardening landed in this commit.
- docs/index.html: scanning-count example bumped to 31, status line
  updated to mention 3 ported modules.

Build verification: full `make clean && make` in `docker gcc:latest`
with libglib2.0-dev installed: links into a 31-module skeletonkey
ELF (413KB), `--list` shows all modules including pack2theroot,
`--detect-rules --format=auditd` emits the new pack2theroot section,
`--auto --i-know --no-shell` exercises the new banner + active
probes + verdict table + fork isolation + scan summary end-to-end.
Only build warning is the pre-existing
`-Wunterminated-string-initialization` in dirty_pipe (not introduced
here).
2026-05-22 22:42:07 -04:00
leviathan ac557b67d0 review pass: fidelity + credits + count consistency for ported modules
Three-agent rigorous review of the dirtydecrypt + fragnesia ports plus
repo-wide doc consistency, followed by a full Linux build verification.

dirtydecrypt (NOTICE + detection rules):
- NOTICE.md: removed an unsupported "Zellic co-founder" detail and a
  fabricated disclosure-date narrative; tightened phrasing of the
  Zellic + V12 credit; noted that upstream poc.c carries no
  author/license header of its own.
- Embedded auditd + sigma rules and detect/sigma.yml broadened to
  cover every binary in dd_targets[] (added /usr/bin/mount,
  /usr/bin/passwd, /usr/bin/chsh) and added the b32 splice rule, so
  the embedded ruleset matches the on-disk reference and the carrier
  list the exploit actually targets.
- Exploit primitive verified byte-for-byte against the V12 PoC
  (tiny_elf[] identical, all rxgk/XDR/fire/pagecache_write logic
  token-identical). docker gcc:latest compile of the Linux path:
  COMPILE_OK, zero warnings.

fragnesia: review found no defects. Exploit primitive byte-identical
to the V12 PoC (shell_elf[] 192 bytes identical, AF_ALG GCM keystream
table + userns/netns/XFRM + receiver/sender/run_trigger_pair all
faithful). The deliberate omissions (ANSI TUI, CLI arg parsing) drop
nothing exploit-critical. docker gcc:latest compile: COMPILE_OK; full
project build links into a working skeletonkey ELF and --list shows
the module registered correctly.

Repo docs (README.md / CVES.md / ROADMAP.md):
- Chose to keep "28 verified" as the headline; the two ported
  modules are represented as a separate clearly-labelled tier
  ("ported-but-unverified") that is explicitly excluded from the
  28-module verified counts. README + CVES.md + ROADMAP.md now tell
  one consistent story.
- Filled a pre-existing documentation gap: sudo_samedit, sequoia,
  sudoedit_editor, vmwgfx were registered + built but absent from
  CVES.md's inventory + operations tables. Added rows synthesized
  from each module's .cve / .summary / .kernel_range fields.
- ROADMAP Phase 8 "7 🟡 PRIMITIVE modules" → "14"; added a "Landed
  since v0.1.0" group; moved vmwgfx out of the stale carry-overs.

docs site (docs/index.html):
- Stat box "28 / total modules" → "28 / verified modules" (the 14+14
  breakdown now sums to the headline consistently).
- Terminal example "scanning 28 modules" → "scanning 30 modules"
  (was factually wrong — the binary literally prints module_count()
  which is 30).
- Status line: updated to mention the 2 ported-but-unverified
  modules and mirror the README phrasing.
- docs/LAUNCH.md left as a dated v0.5.0 launch snapshot.

Build verification: `docker run gcc:latest make clean && make` —
links into a 30-module skeletonkey ELF on Linux. macOS dev box still
hits the pre-existing dirty_pipe header gap; unchanged.

.gitignore: added /skeletonkey to exclude the top-level build
artifact (the existing modules/*/skeletonkey only covered per-module
binaries; the root one was getting picked up by `git add -A`).
2026-05-22 18:41:37 -04:00
leviathan 33f81aeb69 site: revert CVE table → pill grid
The sortable table was denser but lost the visual scan-ability of
the color-coded pill grid. Restoring the pill view: two grouped
sections (🟢 / 🟡) each showing every module name as a pill.

Drops the table-sort JS (~25 lines) and the .cve-table CSS block.
2026-05-17 02:25:25 -04:00
leviathan 58fb2e0951 site: simplify nav + add sortable CVE chart
nav: removed Releases / CVEs / Defenders links — kept only a
    right-aligned GitHub link with the Octocat SVG icon.
  index.html: replaced pill-grid corpus view with a proper sortable
    table — Year, CVE, Bug, Module, Tier columns. Click headers to
    sort. Defaults to Year descending. 28 rows covering 2016 → 2026.
  style.css: added .nav-github (border-pill style) + table styles
    (sortable headers with arrow indicators, hover rows, mobile-
    responsive font-size + overflow-x scroll).

JS for sort is ~25 lines vanilla — no library.
2026-05-17 02:22:54 -04:00
leviathan 2904fa159c site: GitHub Pages landing page
Single-page static site under /docs/, served by GitHub Pages from
the main branch /docs source.

  docs/index.html: hero with one-liner + copy button, why-this-exists,
    corpus stats + module pills (14 🟢 + 14 🟡), audience cards
    (red/blue/sysadmin/CTF), terminal-shape worked example,
    verified-vs-claimed bar, quickstart commands, status, footer.
  docs/style.css: dark theme matching GitHub's color palette
    (#0d1117 bg, #c9d1d9 text). System sans for prose, ui-monospace
    for code. Mobile-responsive with grid breakpoints. No JS framework,
    no external fonts, no analytics.
  docs/.nojekyll: disable Jekyll so the static HTML is served
    verbatim and the existing /docs/*.md files stay as raw markdown
    (viewable via GitHub UI, not the Pages site).
2026-05-17 02:14:15 -04:00
leviathan 95135213e5 launch: README polish + CONTRIBUTING + LAUNCH.md
README.md: badges (release / license / module-count / platform),
    sharpened hero stating value prop in one sentence, audience
    framing for red team / sysadmin / blue team.
  CONTRIBUTING.md (new): what we accept (offsets, modules, detection
    rules, bug reports) and what we don't (untested EXPLOIT_OK,
    fabricated offsets, 0days, undisclosed CVEs).
  docs/LAUNCH.md (new): ~600-word HN/blog launch post. Copy-paste
    ready. Explains the verified-vs-claimed bar + --auto + the
    operator-populated offset table approach.

GitHub repo description + 11 topics set via gh repo edit so the
repo is discoverable in topic searches (linux-security,
privilege-escalation, cve, redteam, blueteam, etc.).
2026-05-17 01:59:25 -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 9d88b475c1 v0.3.1: --dump-offsets tool + NOTICE.md per module
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
The README has been claiming "each module credits the original CVE
reporter and PoC author in its NOTICE.md" since v0.1.0, but only
copy_fail_family actually shipped one. Fixed.

  modules/<name>/NOTICE.md (×19 new + 1 existing): per-module
    research credit covering CVE ID, discoverer, original advisory
    URL where public, upstream fix commit, IAMROOT's role.

  iamroot.c: new --dump-offsets subcommand. Resolves kernel offsets
    via the existing core/offsets.c four-source chain (env →
    /proc/kallsyms → /boot/System.map → embedded table), then emits
    a ready-to-paste C struct entry for kernel_table[]. Run once
    as root on a target kernel build; upstream via PR. Eliminates
    fabricating offsets — every shipped entry traces back to a
    `iamroot --dump-offsets` invocation on a real kernel.

  docs/OFFSETS.md: documents the --dump-offsets workflow.
  CVES.md: notes the NOTICE.md convention + offset dump tool.

  iamroot.c: bump IAMROOT_VERSION 0.3.0 → 0.3.1.
2026-05-16 22:33:43 -04:00
leviathan e2a3d6e94f release: v0.2.0 — --full-chain root-pop opt-in across 7 🟡 modules
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
iamroot.c: bump IAMROOT_VERSION 0.1.0 → 0.2.0
  CVES.md: redefine 🟡 to note --full-chain capability + docs/OFFSETS.md
  README.md: update Status section for v0.2.0
  docs/OFFSETS.md: new doc — env-var/kallsyms/System.map/embedded-table
                   resolution chain + operator workflow for populating
                   offsets per kernel build + sentinel-based success
                   arbitration.

All 7 🟡 modules now expose `--full-chain`. Default behavior unchanged.
2026-05-16 22:06:14 -04:00
leviathan ea5d021f0c tools/iamroot-fleet-scan.sh + docs/DETECTION_PLAYBOOK.md
iamroot-fleet-scan.sh — bash wrapper that scp's the iamroot binary
to a host list, ssh-runs --scan --json on each, aggregates results
into a single JSON document. Supports:
- hosts list from file or stdin
- user@host:port syntax
- parallel xargs execution (default -P 4)
- ssh key / extra ssh opts pass-through
- --no-sudo for hosts where root isn't required
- --summary-only to suppress per-host detail
- --no-cleanup to leave the binary on disk

Critical fix during smoke-test: iamroot's exit codes are SEMANTIC
(0=OK, 2=VULNERABLE, 4=PRECOND_FAIL, 5=EXPLOIT_OK). The wrapper
must NOT treat nonzero exit as a transport failure; success is
defined by 'stdout contains valid JSON', failure by 'stdout empty'.

Verified end-to-end on kctf-mgr → kctf-fuzz:
  fleet-scan reports ok=1, failed=0,
  summary.vulnerable groups by CVE: copy_fail_gcm, dirty_frag_esp×2,
  entrybleed. Per-host detail included.

docs/DETECTION_PLAYBOOK.md — operational integration guide:
- Lifecycle diagram (inventory → scan → fleet scan → deploy/mitigate/upgrade → monitor)
- Recipes by team size: single host, small fleet, large fleet
- SIEM integration patterns: Splunk, Elastic, Sigma
- Auditd-event lookup commands per module key
- VULNERABLE decision tree (patch vs mitigate vs compensate)
- Mitigation revert procedures + side-effect table
- False-positive tuning table per rule key
- Pre-patch quarantine pattern
- Maintenance contract / module-shipping SLA
2026-05-16 20:29:48 -04:00
leviathan f1bd896ca8 Phase 7: Pwnkit FULL exploit (Qualys-style PoC) + DEFENDERS.md
Pwnkit: 🔵🟢
- Implements the canonical Qualys-style PoC end-to-end:
  1. Locate setuid pkexec
  2. mkdtemp working directory under /tmp
  3. Detect target's gcc/cc (fail-soft if absent)
  4. Write payload.c (gconv constructor: unsetenv hostile vars,
     setuid(0), execle /bin/sh -p with clean PATH)
  5. gcc -shared -fPIC payload.c -o pwnkit/PWNKIT.so
  6. Write gconv-modules cache pointing UTF-8// → PWNKIT//
  7. execve(pkexec, NULL_argv, envp{GCONV_PATH=workdir/pwnkit,
     PATH=GCONV_PATH=., CHARSET=PWNKIT, SHELL=pwnkit})
     → argc=0 triggers argv-overflow-into-envp; pkexec re-execs
     with PATH set to our tmpdir; libc's iconv loads PWNKIT.so
     as root; constructor pops /bin/sh with uid=0.
- Cleanup: removes /tmp/iamroot-pwnkit-* workdirs.
- Auto-refuses on patched hosts (re-runs detect() first).
- GCC -Wformat-truncation warnings fixed by sizing path buffers
  generously (1024/2048 bytes — way more than needed in practice).

Verified end-to-end on kctf-mgr (polkit 126 = patched):
  iamroot --exploit pwnkit --i-know
  → detect() says fixed → refuses cleanly. Correct behavior.
Vulnerable-kernel validation is Phase 4 CI matrix work.

docs/DEFENDERS.md — blue-team deployment guide:
- TL;DR: scan, deploy rules, mitigate, watch
- Operations cheat sheet (--list, --scan, --detect-rules, --mitigate)
- Audit-key table mapping rule keys to modules to caught behavior
- Fleet-scanning recipe (ssh + jq aggregation)
- Known false-positive shapes per rule with tuning hints

CVES.md: pwnkit row updated 🔵🟢.
ROADMAP.md: Phase 7 Pwnkit checkbox marked complete.
2026-05-16 20:13:11 -04:00
leviathan cf30b249de Initial skeleton: README, CVE inventory, roadmap, ARCH, ethics + copy_fail_family module absorbed from DIRTYFAIL 2026-05-16 19:26:24 -04:00