Commit Graph

6 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 39ce4dff09 modules: per-module OPSEC notes — telemetry footprint per exploit
Adds .opsec_notes to every module's struct skeletonkey_module
(31 entries across 26 module files). One paragraph per exploit
describing the runtime footprint a defender/SOC would see:

  - file artifacts created/modified (exact paths from source)
  - syscall observables (the unshare / socket / setsockopt /
    splice / msgsnd patterns the embedded detection rules look for)
  - dmesg signatures (silent on success vs KASAN oops on miss)
  - network activity (loopback-only vs none)
  - persistence side-effects (/etc/passwd modification, dropped
    setuid binaries, backdoors)
  - cleanup behaviour (callback present? what it restores?)

Each note is grounded in the module's source code + its existing
auditd/sigma/yara/falco detection rules — the OPSEC notes are
literally the inverse of those rules (the rules describe what to
look for; the notes describe what the exploit triggers).

Three intelligence agents researched the modules in parallel,
reading source + MODULE.md, then their proposals were embedded
verbatim via tools/inject_opsec.py (one-shot script, not retained).

Where surfaced:
  - --module-info <name>: '--- opsec notes ---' section between
    detect-rules summary and the embedded auditd/sigma rule bodies.
  - --module-info / --scan --json: 'opsec_notes' top-level string.

Audience uses:
  - Red team: see what footprint each exploit leaves so they pick
    chains that match the host's telemetry posture.
  - Blue team: the notes mirror the existing detection rules from the
    attacker side — easy diff to find gaps in their SIEM coverage.
  - Researchers: per-exploit footprint catalog for technique analysis.

copy_fail_family gets one shared note across all 5 register entries
(copy_fail, copy_fail_gcm, dirty_frag_esp, dirty_frag_esp6,
dirty_frag_rxrpc) since they share exploit infrastructure.

Verification:
  - macOS local: clean build, --module-info nf_tables shows full
    opsec section + CWE + ATT&CK + KEV row from previous commit.
  - Linux (docker gcc:latest): 33 + 54 = 87 passes, 0 fails.

Next: --explain mode (uses these notes + the triage metadata to
render a single 'why is this verdict, what would patch fix it, and
what would the SOC see' page per module).
2026-05-23 10:45:38 -04:00
leviathan 8938a74d04 detection rules: YARA + Falco for the 6 highest-rank modules + playbook
Closes the 'rules in the box' gap — the README has claimed YARA +
Falco coverage but detect_yara and detect_falco were NULL on every
module. This commit lights up both formats for the 6 highest-value
modules (covering 10 of 31 registered modules via family-shared
rules), and the existing operational playbook gains the
format-specific deployment recipes + the cross-format correlation
table.

YARA rules (8 rules, 9 module-headers, 152 lines):
- copy_fail_family — etc_passwd_uid_flip + etc_passwd_root_no_password
  (shared across copy_fail / copy_fail_gcm / dirty_frag_esp /
   dirty_frag_esp6 / dirty_frag_rxrpc)
- dirty_pipe — passwd UID flip pattern, dirty-pipe-specific tag
- dirtydecrypt — 28-byte ELF prefix match on tiny_elf[] + setuid+execve
  shellcode tail, detects the page-cache overlay landing
- fragnesia — 28-byte ELF prefix on shell_elf[] + setuid+setgid+seteuid
  cascade, detects the 192-byte page-cache overlay
- pwnkit — gconv-modules cache file format (small text file with
  module UTF-8// X// /tmp/...)
- pack2theroot — malicious .deb (ar archive + SUID-bash postinst) +
  /tmp/.suid_bash artifact scan

Falco rules (13 rules, 9 module-headers, 219 lines):
- pwnkit — pkexec with empty argv + GCONV_PATH/CHARSET env from non-root
- copy_fail_family — AF_ALG socket from non-root + NETLINK_XFRM from
  unprivileged userns + /etc/passwd modified by non-root
- dirty_pipe — splice() of setuid/credential file by non-root
- dirtydecrypt — AF_RXRPC socket + add_key(rxrpc) by non-root
- fragnesia — TCP_ULP=espintcp from non-root + splice of setuid binary
- pack2theroot — SUID bit set on /tmp/.suid_bash + dpkg invoked by
  packagekitd with /tmp/.pk-*.deb + 2x InstallFiles on same transaction

Wiring: each module's .detect_yara and .detect_falco struct fields
now point at the embedded string. The dispatcher dedups by pointer,
so family-shared rules emit once across the 5 sub-modules.

docs/DETECTION_PLAYBOOK.md augmented (302 -> 456 lines):
- New 'YARA artifact scanning' subsection under SIEM integration
  with scheduled-scan cron pattern + per-rule trigger table
- New 'Falco runtime detection' subsection with deploy + per-rule
  trigger table
- New 'Per-module detection coverage' table — 4-format matrix
- New 'Correlation across formats' section — multi-format incident
  signature per exploit (the 3-of-4 signal pattern)
- New 'Worked example: catching DirtyDecrypt end-to-end' walkthrough
  from Falco page through yara confirmation, recovery, hunt + patch

The existing operational lifecycle / SIEM patterns / FP tuning
content is preserved unchanged — this commit only adds.

Final stats:
- auditd: 109 rule statements across 27 modules
- sigma:  16 sigma rules across 19 modules
- yara:    8 yara rules across 9 module headers (5 family + 4 distinct)
- falco:  13 falco rules across 9 module headers

The remaining 21 modules can gain YARA / Falco coverage incrementally
by populating their detect_yara / detect_falco struct fields.
2026-05-23 00:47:13 -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 a26f471ecf dirtydecrypt + fragnesia: pin CVE fix commits, version-based detect()
Both modules' detect() was precondition-only because we didn't know the
mainline fix commits at port time. Debian's security tracker now
provides them — pinning here turns detect() into a proper version-
based verdict (still with --active for empirical override).

dirtydecrypt (CVE-2026-31635):
- Fix commit a2567217ade970ecc458144b6be469bc015b23e5 in mainline 7.0
  ('rxrpc: fix oversized RESPONSE authenticator length check').
- Debian tracker confirms older stable branches (5.10 / 6.1 / 6.12) as
  <not-affected, vulnerable code not present>: the rxgk RESPONSE-
  handling code was added in 7.0.
- kernel_range table: { {7, 0, 0} }
- detect() pre-checks 'kernel < 7.0 -> SKELETONKEY_OK (predates)' then
  consults the table. With --active, the /tmp sentinel probe overrides
  empirically (catches pre-fix 7.0-rc kernels the version check
  reports as patched).

fragnesia (CVE-2026-46300):
- Fix in mainline 7.0.9 per Debian tracker ('linux unstable: 7.0.9-1
  fixed'). Older Debian-stable branches (bullseye 5.10 / bookworm 6.1
  / trixie 6.12) are still marked vulnerable as of 2026-05-22 - no
  backports yet.
- kernel_range table: { {7, 0, 9} }
- detect() keeps the userns + carrier preconditions, then consults
  the table: 7.0.9+ -> OK; older branches without an explicit backport
  entry -> VULNERABLE (version-only). --active confirms empirically.
- Table is intentionally minimal so distros that DO backport in the
  future flow into 'patched' once their branch lands an entry; until
  then, the conservative VULNERABLE verdict on unfixed branches is
  correct.

Other changes:
- module struct .kernel_range strings updated from 'fix commit not
  yet pinned' to the actual pinned-version prose.
- module_safety_rank bumped 86 -> 87 for both modules (version-pinned
  detect is now real; still below the verified copy_fail family at
  88 so --auto prefers verified modules when both apply).
- Both modules now #include core/kernel_range.h inside their
  #ifdef __linux__ block.
- MODULE.md verification-status sections rewritten: detect() is now
  version-pinned; only the exploit body remains unverified.
- CVES.md note + inventory rows updated: dropped the 'precondition-
  only' language for the pair; all three ported modules now have
  pinned fix references.
- README  tier description + module list aligned to the new state.

Both detect()s smoke-tested in docker gcc:latest on kernel 6.12.76-
linuxkit: dirtydecrypt correctly reports OK ('predates the rxgk code
added in 7.0'); fragnesia + pack2theroot correctly report
PRECOND_FAIL (no userns / no D-Bus in container). Local macOS + Linux
builds both clean.
2026-05-22 23:06:15 -04:00
leviathan a8c8d5ef1f modules: add dirtydecrypt (CVE-2026-31635) + fragnesia (CVE-2026-46300)
Two new page-cache-write LPE modules, both ported from the public V12
security PoCs (github.com/v12-security/pocs):

- dirtydecrypt (CVE-2026-31635): rxgk missing-COW in-place decrypt.
  rxgk_decrypt_skb() decrypts spliced page-cache pages before the HMAC
  check, corrupting the page cache of a read-only file. Sibling of
  Copy Fail / Dirty Frag in the rxrpc subsystem.

- fragnesia (CVE-2026-46300): XFRM ESP-in-TCP skb_try_coalesce() loses
  the SHARED_FRAG marker, so the ESP-in-TCP receive path decrypts
  page-cache pages in place. A latent bug exposed by the Dirty Frag
  fix (f4c50a4034e6). Retires the old _stubs/fragnesia_TBD stub.

Both wrap the PoC exploit primitive in the skeletonkey_module
interface: detect/exploit/cleanup, an --active /tmp sentinel probe,
--no-shell support, and embedded auditd + sigma rules. The exploit
body runs in a forked child so the PoC's exit()/die() paths cannot
tear down the dispatcher. The fragnesia port drops the upstream PoC's
ANSI TUI (incompatible with a shared dispatcher); the exploit
mechanism is reproduced faithfully. Linux-only code is guarded with
#ifdef __linux__ so the modules still compile on non-Linux dev boxes.

VERIFICATION: ported, NOT yet validated end-to-end on a
vulnerable-kernel VM. The CVE fix commits are not pinned, so detect()
is precondition-only (PRECOND_FAIL / TEST_ERROR, never a blind
VULNERABLE) and --auto will not fire them unless --active confirms.
macOS stub-path compiles verified locally; the Linux exploit-path
build is covered by CI (build.yml, ubuntu) only. See each MODULE.md.

Wiring: core/registry.h, skeletonkey.c, Makefile, CVES.md, ROADMAP.md.
2026-05-22 18:22:30 -04:00