diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a601f65..671ffa2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,14 +59,28 @@ jobs: skeletonkey-${{ matrix.target }} skeletonkey-${{ matrix.target }}.sha256 - # Portable static-musl build for x86_64. Runs in Alpine (native - # musl + linux-headers) so the resulting binary works on every - # libc — glibc 2.x of any version, musl, etc. This is what - # install.sh fetches by default (the dynamic binary above hits a - # glibc-version ceiling on older distros like Debian 12 / RHEL 8). - build-static-x86_64: - runs-on: ubuntu-latest - name: build (x86_64-static / musl) + # Portable static-musl builds. Run in Alpine (native musl + + # linux-headers) so the resulting binary works on every libc — + # glibc 2.x of any version, musl, etc. This is what install.sh + # fetches by default (the dynamic binary above hits a glibc- + # version ceiling on older distros like Debian 12 / RHEL 8). + # + # x86_64-static runs on the regular x86_64 runner pool. + # arm64-static runs on GitHub's native ARM Linux runners + # (free for public repos as of 2024). Both produce statically- + # linked binaries that just need an executable Linux kernel of + # the right ABI. + build-static: + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-static + runner: ubuntu-latest + - target: arm64-static + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + name: build (${{ matrix.target }} / musl) container: image: alpine:latest steps: @@ -88,18 +102,18 @@ jobs: - name: rename + checksum run: | - mv skeletonkey skeletonkey-x86_64-static - sha256sum skeletonkey-x86_64-static > skeletonkey-x86_64-static.sha256 + mv skeletonkey skeletonkey-${{ matrix.target }} + sha256sum skeletonkey-${{ matrix.target }} > skeletonkey-${{ matrix.target }}.sha256 - uses: actions/upload-artifact@v4 with: - name: skeletonkey-x86_64-static + name: skeletonkey-${{ matrix.target }} path: | - skeletonkey-x86_64-static - skeletonkey-x86_64-static.sha256 + skeletonkey-${{ matrix.target }} + skeletonkey-${{ matrix.target }}.sha256 release: - needs: [build, build-static-x86_64] + needs: [build, build-static] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -154,5 +168,7 @@ jobs: skeletonkey-x86_64-static.sha256 skeletonkey-arm64 skeletonkey-arm64.sha256 + skeletonkey-arm64-static + skeletonkey-arm64-static.sha256 install.sh fail_on_unmatched_files: false # install.sh may not exist at first tag diff --git a/core/module.h b/core/module.h index 949b5b2..935485b 100644 --- a/core/module.h +++ b/core/module.h @@ -119,6 +119,31 @@ struct skeletonkey_module { * core/cve_metadata.{h,c} — looked up by CVE id, refreshed via * tools/refresh-cve-metadata.py. */ const char *opsec_notes; + + /* Architecture support for the exploit() body. detect() works on + * any Linux arch (it just consults ctx->host); the question this + * field answers is: if this module says VULNERABLE, will the + * --exploit path actually fire on aarch64 / arm64? Values: + * + * "any" — userspace bug or arch-agnostic kernel + * primitive (pwnkit, sudo*, pack2theroot, + * dirty_pipe, dirty_cow, most netfilter/fs + * bugs that use msg_msg sprays + structural + * escapes). + * "x86_64" — strictly x86-only (entrybleed needs + * prefetchnta + KPTI, which doesn't apply + * to ARM's TTBR_EL0/EL1 model). + * "x86_64+unverified-arm64" — exploit body likely works on + * arm64 but hasn't been verified on a real + * arm64 host yet (e.g. copy_fail_family + * assumes some x86_64 struct offsets; + * --full-chain finisher uses x86_64-style + * kernel ROP gadgets). + * + * NULL = unmapped (treat as "x86_64+unverified-arm64" by default; + * a future arm64-on-Vagrant sweep will fill these in). Surfaced + * in --list (ARCH column) and --module-info. */ + const char *arch_support; }; #endif /* SKELETONKEY_MODULE_H */ diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index ef9d0b4..4420db9 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -1,4 +1,43 @@ -## SKELETONKEY v0.7.0 — empirical verification + operator briefing +## SKELETONKEY v0.7.1 — arm64-static binary + per-module arch_support + +Point release on top of v0.7.0. Two additions: + +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. Works on Raspberry Pi 4+, + Apple Silicon Linux VMs, AWS Graviton, Oracle Ampere, Hetzner ARM, + and any other aarch64 Linux. `install.sh` auto-picks it. + +2. **`arch_support` per module** — a new field on + `struct skeletonkey_module` that honestly labels 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 (ARM uses TTBR_EL0/EL1 split, not CR3). + Already gated in source — returns PRECOND_FAIL on non-x86_64. + - **`x86_64+unverified-arm64`** (26 modules): kernel-exploitation + code that hasn't been verified on arm64 yet. `detect()` works + everywhere (it just reads `ctx->host`); the `exploit()` body uses + primitives (msg_msg sprays, ROP-style finishers, specific struct + offsets) that are likely portable to aarch64 but unproven. + + `--list` adds an ARCH column; `--module-info` adds an `arch support:` + line; `--scan --json` adds an `arch_support` field per module. + +**What an arm64 user gets today:** the full detection/triage workflow +works as well as on x86_64 (`--scan`, `--explain`, `--module-info`, +`--detect-rules`, `--auto --dry-run`). Four exploit modules +(`pwnkit`, `sudo_samedit`, `sudoedit_editor`, `pack2theroot`) will fire +end-to-end. The remaining 26 modules currently mark themselves as +"x86_64 verified; arm64 untested" — the bug class is generic but the +exploitation hasn't been confirmed. Future arm64-Vagrant verification +sweeps will promote modules to `any` as they're confirmed. + +--- + +### From v0.7.0 — empirical verification + operator briefing The headline change since v0.6.0: **22 of 26 CVEs are now empirically confirmed against real Linux kernels in VMs**, with verification records diff --git a/install.sh b/install.sh index 3ab8f92..a1c725d 100755 --- a/install.sh +++ b/install.sh @@ -34,15 +34,15 @@ log() { printf '[\033[1;36m*\033[0m] %s\n' "$*" >&2; } ok() { printf '[\033[1;32m+\033[0m] %s\n' "$*" >&2; } fail() { printf '[\033[1;31m-\033[0m] %s\n' "$*" >&2; exit 1; } -# Detect architecture +# Detect architecture. Default to the musl-static binary on both +# x86_64 and arm64 — works on every libc (glibc 2.x of any version, +# musl, uclibc); costs ~800 KB extra vs dynamic but eliminates the +# GLIBC_2.NN portability ceiling that bites on Debian-stable, older +# RHEL hosts, and Alpine. Set SKELETONKEY_DYNAMIC=1 to fetch the +# smaller dynamic build (needs glibc >= 2.38 for x86_64 — Ubuntu +# 24.04 / Debian 13 / RHEL 10). arch=$(uname -m) case "$arch" in - # x86_64 default: the musl-static binary works on every libc - # (glibc 2.x of any version, musl, uclibc) — costs ~800 KB extra - # vs the dynamic build but eliminates the GLIBC_2.NN portability - # ceiling that bit users on Debian-stable / older RHEL hosts. - # Set SKELETONKEY_DYNAMIC=1 to fetch the smaller dynamic build - # (needs glibc >= 2.38, i.e. Ubuntu 24.04 / Debian 13 / RHEL 10). x86_64|amd64) if [ "${SKELETONKEY_DYNAMIC:-0}" = "1" ]; then target=x86_64 @@ -50,7 +50,13 @@ case "$arch" in target=x86_64-static fi ;; - aarch64|arm64) target=arm64 ;; + aarch64|arm64) + if [ "${SKELETONKEY_DYNAMIC:-0}" = "1" ]; then + target=arm64 + else + target=arm64-static + fi + ;; *) fail "Unsupported architecture: $arch (only x86_64 and arm64 currently)" ;; esac log "detected arch: $target" diff --git a/modules/af_packet2_cve_2020_14386/skeletonkey_modules.c b/modules/af_packet2_cve_2020_14386/skeletonkey_modules.c index 3d1332c..6179b2c 100644 --- a/modules/af_packet2_cve_2020_14386/skeletonkey_modules.c +++ b/modules/af_packet2_cve_2020_14386/skeletonkey_modules.c @@ -732,6 +732,7 @@ const struct skeletonkey_module af_packet2_module = { .detect_yara = af_packet2_yara, .detect_falco = af_packet2_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNET) + TPACKET_V2 ring on AF_PACKET; crafts nested-VLAN ETH_P_8021AD frames with 0x88A8/0x8100 TPIDs to trigger tpacket_rcv underflow; fires 256 frames + 64 sendmmsg via AF_UNIX socketpair spray. Tag 'skeletonkey-afp-fc-' visible in KASAN splats. Audit-visible via socket(AF_PACKET) + sendmsg/sendto from userns. No persistent artifacts; kernel cleans up on child exit.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_af_packet2(void) diff --git a/modules/af_packet_cve_2017_7308/skeletonkey_modules.c b/modules/af_packet_cve_2017_7308/skeletonkey_modules.c index 2fa1740..ebd411c 100644 --- a/modules/af_packet_cve_2017_7308/skeletonkey_modules.c +++ b/modules/af_packet_cve_2017_7308/skeletonkey_modules.c @@ -955,6 +955,7 @@ const struct skeletonkey_module af_packet_module = { .detect_yara = af_packet_yara, .detect_falco = af_packet_falco, .opsec_notes = "Creates AF_PACKET socket and TPACKET_V3 ring inside unshare(CLONE_NEWUSER|CLONE_NEWNET); triggers integer overflow with crafted tp_block_size/tp_block_nr and sprays ~200 loopback frames. Audit-visible via socket(AF_PACKET) (a0=17) + sendmmsg from a userns process; KASAN tag 'iamroot-afp-tag' may appear in dmesg if enabled. No persistent files. No cleanup callback - kernel state unwinds on child exit.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_af_packet(void) diff --git a/modules/af_unix_gc_cve_2023_4622/skeletonkey_modules.c b/modules/af_unix_gc_cve_2023_4622/skeletonkey_modules.c index c4fca4d..3621b44 100644 --- a/modules/af_unix_gc_cve_2023_4622/skeletonkey_modules.c +++ b/modules/af_unix_gc_cve_2023_4622/skeletonkey_modules.c @@ -898,6 +898,7 @@ const struct skeletonkey_module af_unix_gc_module = { .detect_yara = af_unix_gc_yara, .detect_falco = af_unix_gc_falco, .opsec_notes = "Two-threaded race: Thread A creates socketpair(AF_UNIX) with SCM_RIGHTS cycle then close; Thread B drives independent SCM_RIGHTS traffic on a held pair. ~5s budget (30s with --full-chain). msg_msg kmalloc-512 spray tagged 'SKELETONKEYU'. Writes /tmp/skeletonkey-af_unix_gc.log with empirical stats. Audit-visible via socketpair(AF_UNIX) + sendmsg(SCM_RIGHTS) + msgsnd triple. Dmesg may show UAF KASAN if kernel vulnerable. Cleanup callback unlinks the log.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_af_unix_gc(void) diff --git a/modules/cgroup_release_agent_cve_2022_0492/skeletonkey_modules.c b/modules/cgroup_release_agent_cve_2022_0492/skeletonkey_modules.c index 32c64e6..9faa02e 100644 --- a/modules/cgroup_release_agent_cve_2022_0492/skeletonkey_modules.c +++ b/modules/cgroup_release_agent_cve_2022_0492/skeletonkey_modules.c @@ -404,6 +404,7 @@ const struct skeletonkey_module cgroup_release_agent_module = { .detect_yara = cgroup_release_agent_yara, .detect_falco = cgroup_release_agent_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNS), mount cgroup v1 at /tmp/skeletonkey-cgroup-mnt, write payload path to release_agent file at cgroup root, echo 1 to notify_on_release in subdir, add PID to cgroup.procs and exit. Payload at /tmp/skeletonkey-cgroup-payload.sh runs as init-namespace root when cgroup empties, dropping setuid /tmp/skeletonkey-cgroup-sh. Audit-visible via unshare + mount(cgroup) + open/write of release_agent. Cleanup callback removes /tmp/skeletonkey-cgroup-* and umounts.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_cgroup_release_agent(void) diff --git a/modules/cls_route4_cve_2022_2588/skeletonkey_modules.c b/modules/cls_route4_cve_2022_2588/skeletonkey_modules.c index 7cbd7d2..1b147ee 100644 --- a/modules/cls_route4_cve_2022_2588/skeletonkey_modules.c +++ b/modules/cls_route4_cve_2022_2588/skeletonkey_modules.c @@ -889,6 +889,7 @@ const struct skeletonkey_module cls_route4_module = { .detect_yara = cls_route4_yara, .detect_falco = cls_route4_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNET); ip link/addr/route to make a dummy interface, htb qdisc + class + route4 filter with handle 0, delete filter (leaves dangling tcf_proto pointer), msg_msg spray kmalloc-1k tagged 'SKELETONKEY4', UDP sendto to trigger classify(). Writes /tmp/skeletonkey-cls_route4.log. Audit-visible via unshare + sendto(AF_INET) + msgsnd. Cleanup callback removes /tmp log + dummy interface.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_cls_route4(void) diff --git a/modules/copy_fail_family/skeletonkey_modules.c b/modules/copy_fail_family/skeletonkey_modules.c index a48acf4..bdebb5d 100644 --- a/modules/copy_fail_family/skeletonkey_modules.c +++ b/modules/copy_fail_family/skeletonkey_modules.c @@ -248,6 +248,7 @@ const struct skeletonkey_module copy_fail_module = { .detect_yara = copy_fail_family_yara, .detect_falco = copy_fail_family_falco, .opsec_notes = "Family-shared infrastructure (copy_fail, copy_fail_gcm, dirty_frag_esp/esp6, dirty_frag_rxrpc): all exploit a page-cache write primitive against /etc/passwd (UID flip to all-zeros) or install a persistent backdoor. Audit-visible via socket(AF_ALG) (a0=38), setsockopt(XFRM), AF_UNIX setup. Detection rules watch /etc/passwd, /etc/shadow, /etc/sudoers, /usr/bin/su for non-root writes. Family mitigation blacklists algif_aead/esp4/esp6/rxrpc and sets apparmor_restrict_unprivileged_userns=1. Cleanup evicts /etc/passwd from page cache and reverts mitigation conf.", + .arch_support = "x86_64+unverified-arm64", }; /* ----- copy_fail_gcm (variant, no CVE) ----- */ @@ -281,6 +282,7 @@ const struct skeletonkey_module copy_fail_gcm_module = { .detect_yara = copy_fail_family_yara, .detect_falco = copy_fail_family_falco, .opsec_notes = "Family-shared infrastructure (copy_fail, copy_fail_gcm, dirty_frag_esp/esp6, dirty_frag_rxrpc): all exploit a page-cache write primitive against /etc/passwd (UID flip to all-zeros) or install a persistent backdoor. Audit-visible via socket(AF_ALG) (a0=38), setsockopt(XFRM), AF_UNIX setup. Detection rules watch /etc/passwd, /etc/shadow, /etc/sudoers, /usr/bin/su for non-root writes. Family mitigation blacklists algif_aead/esp4/esp6/rxrpc and sets apparmor_restrict_unprivileged_userns=1. Cleanup evicts /etc/passwd from page cache and reverts mitigation conf.", + .arch_support = "x86_64+unverified-arm64", }; /* ----- dirty_frag_esp (CVE-2026-43284 v4) ----- */ @@ -314,6 +316,7 @@ const struct skeletonkey_module dirty_frag_esp_module = { .detect_yara = copy_fail_family_yara, .detect_falco = copy_fail_family_falco, .opsec_notes = "Family-shared infrastructure (copy_fail, copy_fail_gcm, dirty_frag_esp/esp6, dirty_frag_rxrpc): all exploit a page-cache write primitive against /etc/passwd (UID flip to all-zeros) or install a persistent backdoor. Audit-visible via socket(AF_ALG) (a0=38), setsockopt(XFRM), AF_UNIX setup. Detection rules watch /etc/passwd, /etc/shadow, /etc/sudoers, /usr/bin/su for non-root writes. Family mitigation blacklists algif_aead/esp4/esp6/rxrpc and sets apparmor_restrict_unprivileged_userns=1. Cleanup evicts /etc/passwd from page cache and reverts mitigation conf.", + .arch_support = "x86_64+unverified-arm64", }; /* ----- dirty_frag_esp6 (CVE-2026-43284 v6) ----- */ @@ -347,6 +350,7 @@ const struct skeletonkey_module dirty_frag_esp6_module = { .detect_yara = copy_fail_family_yara, .detect_falco = copy_fail_family_falco, .opsec_notes = "Family-shared infrastructure (copy_fail, copy_fail_gcm, dirty_frag_esp/esp6, dirty_frag_rxrpc): all exploit a page-cache write primitive against /etc/passwd (UID flip to all-zeros) or install a persistent backdoor. Audit-visible via socket(AF_ALG) (a0=38), setsockopt(XFRM), AF_UNIX setup. Detection rules watch /etc/passwd, /etc/shadow, /etc/sudoers, /usr/bin/su for non-root writes. Family mitigation blacklists algif_aead/esp4/esp6/rxrpc and sets apparmor_restrict_unprivileged_userns=1. Cleanup evicts /etc/passwd from page cache and reverts mitigation conf.", + .arch_support = "x86_64+unverified-arm64", }; /* ----- dirty_frag_rxrpc (CVE-2026-43500) ----- */ @@ -380,6 +384,7 @@ const struct skeletonkey_module dirty_frag_rxrpc_module = { .detect_yara = copy_fail_family_yara, .detect_falco = copy_fail_family_falco, .opsec_notes = "Family-shared infrastructure (copy_fail, copy_fail_gcm, dirty_frag_esp/esp6, dirty_frag_rxrpc): all exploit a page-cache write primitive against /etc/passwd (UID flip to all-zeros) or install a persistent backdoor. Audit-visible via socket(AF_ALG) (a0=38), setsockopt(XFRM), AF_UNIX setup. Detection rules watch /etc/passwd, /etc/shadow, /etc/sudoers, /usr/bin/su for non-root writes. Family mitigation blacklists algif_aead/esp4/esp6/rxrpc and sets apparmor_restrict_unprivileged_userns=1. Cleanup evicts /etc/passwd from page cache and reverts mitigation conf.", + .arch_support = "x86_64+unverified-arm64", }; /* ----- Family registration ----- */ diff --git a/modules/dirty_cow_cve_2016_5195/skeletonkey_modules.c b/modules/dirty_cow_cve_2016_5195/skeletonkey_modules.c index 1211845..79a7473 100644 --- a/modules/dirty_cow_cve_2016_5195/skeletonkey_modules.c +++ b/modules/dirty_cow_cve_2016_5195/skeletonkey_modules.c @@ -434,6 +434,7 @@ const struct skeletonkey_module dirty_cow_module = { .detect_yara = dirty_cow_yara, .detect_falco = dirty_cow_falco, .opsec_notes = "Two-thread race: Thread A loops pwrite(/proc/self/mem) at the user's UID offset in /etc/passwd; Thread B loops madvise(MADV_DONTNEED) on a PRIVATE mmap of /etc/passwd. Overwrites the UID field with all-zeros, then execlp('su') to claim root. UID offset is parsed from the file, not hardcoded. Audit-visible via open(/proc/self/mem) + write + madvise(MADV_DONTNEED) bursts + /etc/passwd page-cache poisoning. Cleanup callback calls posix_fadvise(POSIX_FADV_DONTNEED) on /etc/passwd and writes 3 to /proc/sys/vm/drop_caches to evict.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_dirty_cow(void) diff --git a/modules/dirty_pipe_cve_2022_0847/skeletonkey_modules.c b/modules/dirty_pipe_cve_2022_0847/skeletonkey_modules.c index f2b0e9e..e9ba4b7 100644 --- a/modules/dirty_pipe_cve_2022_0847/skeletonkey_modules.c +++ b/modules/dirty_pipe_cve_2022_0847/skeletonkey_modules.c @@ -523,6 +523,7 @@ const struct skeletonkey_module dirty_pipe_module = { .detect_yara = dirty_pipe_yara, .detect_falco = dirty_pipe_falco, .opsec_notes = "Creates a pipe, fills+drains to leave PIPE_BUF_FLAG_CAN_MERGE on every slot; finds the UID offset in /etc/passwd by parsing the file; splice(1 byte) from (target_offset-1) to inherit the stale flag, then write(pipe) with the all-zero payload - kernel merges into the file's page cache. Offset must be non-page-aligned and the write must fit in a single page. Audit-visible via splice(fd=/etc/passwd) + write from a non-root process. --active mode writes/reads /tmp/skeletonkey-dirty-pipe-probe-XXXXXX to verify. Cleanup callback evicts /etc/passwd via posix_fadvise + drop_caches.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_dirty_pipe(void) diff --git a/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c b/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c index 31e5fc2..b97a8a8 100644 --- a/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c +++ b/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c @@ -1006,6 +1006,7 @@ const struct skeletonkey_module dirtydecrypt_module = { .detect_yara = dd_yara, .detect_falco = dd_falco, .opsec_notes = "Forked child runs unshare(CLONE_NEWUSER|CLONE_NEWNET); creates AF_RXRPC socket; builds an rxgk XDR token via add_key(SYS_add_key, 'rxrpc'); sets up loopback UDP server + rxrpc client; forges rxrpc DATA packets and fires 10000+ splice-based writes in a sliding window to overwrite a target setuid binary's page cache with a 120-byte ET_DYN ELF (setuid(0) + execve('/bin/sh')). Payload is never written to disk. Audit-visible via socket(AF_RXRPC) (a0=33) + add_key('rxrpc') + splice() bursts. Records target path to /tmp/skeletonkey-dirtydecrypt.target. Cleanup callback evicts candidate targets (/usr/bin/su et al) via drop_caches.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_dirtydecrypt(void) diff --git a/modules/entrybleed_cve_2023_0458/skeletonkey_modules.c b/modules/entrybleed_cve_2023_0458/skeletonkey_modules.c index a1e8ee7..5823d12 100644 --- a/modules/entrybleed_cve_2023_0458/skeletonkey_modules.c +++ b/modules/entrybleed_cve_2023_0458/skeletonkey_modules.c @@ -289,6 +289,7 @@ const struct skeletonkey_module entrybleed_module = { .detect_yara = NULL, .detect_falco = NULL, .opsec_notes = "Pure timing side-channel: rdtsc + prefetchnta sweep across the kernel high-half (~16 MiB) to time which 2 MiB page is mapped (entry_SYSCALL_64) and subtract its known offset from kbase. No syscalls fired, no file artifacts, no network. Classic auditd cannot see it; perf-counter EDR can flag a process spending unusual time in tight prefetchnta loops but classic rules will not. No cleanup needed.", + .arch_support = "x86_64", }; void skeletonkey_register_entrybleed(void) diff --git a/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c b/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c index 18c010f..341be1a 100644 --- a/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c +++ b/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c @@ -1211,6 +1211,7 @@ const struct skeletonkey_module fragnesia_module = { .detect_yara = fg_yara, .detect_falco = fg_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNET) + socket(AF_ALG, SOCK_SEQPACKET) for an AES-GCM keystream table; NETLINK_XFRM setsockopt to install ESP-in-TCP state; TCP_ULP setsockopt on a loopback connection; splice() from a carrier setuid binary (/usr/bin/su or /bin/su) into the TCP socket. Artifacts: /tmp/skeletonkey-fragnesia-probe-XXXXXX (mkstemp, unlinked after probe) and /tmp/skeletonkey-fragnesia.target. Audit-visible via socket(AF_ALG) (38), NETLINK_XFRM (6) writes, TCP_ULP setsockopt, splice() of setuid binary. No external network (loopback). Cleanup callback unlinks /tmp files and evicts the carrier from page cache.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_fragnesia(void) diff --git a/modules/fuse_legacy_cve_2022_0185/skeletonkey_modules.c b/modules/fuse_legacy_cve_2022_0185/skeletonkey_modules.c index 3d8dfe0..fd98378 100644 --- a/modules/fuse_legacy_cve_2022_0185/skeletonkey_modules.c +++ b/modules/fuse_legacy_cve_2022_0185/skeletonkey_modules.c @@ -916,6 +916,7 @@ const struct skeletonkey_module fuse_legacy_module = { .detect_yara = fuse_legacy_yara, .detect_falco = fuse_legacy_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNS) for CAP_SYS_ADMIN; fsopen('cgroup2') + multiple fsconfig(FSCONFIG_SET_STRING, 'source', ...) calls to overflow legacy_parse_param's buffer. OOB write lands in kmalloc-4k adjacent to a msg_msg groom. No persistent files (msg_msg lives in the IPC namespace which disappears with the child). Dmesg silent on success; KASAN would show slab corruption if enabled. Audit-visible via unshare(CLONE_NEWUSER|CLONE_NEWNS) + fsopen + fsconfig pattern in a single process. No cleanup callback - IPC queues auto-drain on namespace exit.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_fuse_legacy(void) diff --git a/modules/netfilter_xtcompat_cve_2021_22555/skeletonkey_modules.c b/modules/netfilter_xtcompat_cve_2021_22555/skeletonkey_modules.c index ddded7e..23ed351 100644 --- a/modules/netfilter_xtcompat_cve_2021_22555/skeletonkey_modules.c +++ b/modules/netfilter_xtcompat_cve_2021_22555/skeletonkey_modules.c @@ -1024,6 +1024,7 @@ const struct skeletonkey_module netfilter_xtcompat_module = { .detect_yara = netfilter_xtcompat_yara, .detect_falco = netfilter_xtcompat_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNET) + setsockopt(SOL_IP, IPT_SO_SET_REPLACE) with a malformed xt_entry_target to trigger xt_compat_target_to_user 4-byte OOB into kmalloc-2k. msg_msg + sk_buff cross-cache groom. Writes /tmp/skeletonkey-xtcompat.log (breadcrumb). Audit-visible via unshare + setsockopt(IPT_SO_SET_REPLACE) + msgsnd/msgrcv + sendmmsg(sk_buff spray). Dmesg silent on success; KASAN oops if the groom misses. Cleanup callback unlinks the log; IPC auto-drains on namespace exit.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_netfilter_xtcompat(void) diff --git a/modules/nf_tables_cve_2024_1086/skeletonkey_modules.c b/modules/nf_tables_cve_2024_1086/skeletonkey_modules.c index 839d0ae..802d797 100644 --- a/modules/nf_tables_cve_2024_1086/skeletonkey_modules.c +++ b/modules/nf_tables_cve_2024_1086/skeletonkey_modules.c @@ -1168,6 +1168,7 @@ const struct skeletonkey_module nf_tables_module = { .detect_yara = nf_tables_yara, .detect_falco = nf_tables_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNET) + nfnetlink batch (NEWTABLE + NEWCHAIN/LOCAL_OUT + NEWSET verdict-key + NEWSETELEM malformed NFT_GOTO) committed twice to trigger the nft_verdict_init double-free. msg_msg cg-96 groom with forged pipapo_elem headers; --full-chain sprays kaddr-tagged forged elems and re-fires. Writes /tmp/skeletonkey-nft_set_uaf.log (conditional). Audit-visible via unshare + socket(NETLINK_NETFILTER) + sendmsg batches + msgget/msgsnd. Dmesg: KASAN double-free panic on vulnerable kernels; silent otherwise. Cleanup is finisher-gated; no persistent files on success.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_nf_tables(void) diff --git a/modules/nft_fwd_dup_cve_2022_25636/skeletonkey_modules.c b/modules/nft_fwd_dup_cve_2022_25636/skeletonkey_modules.c index 7baea84..b89ab6e 100644 --- a/modules/nft_fwd_dup_cve_2022_25636/skeletonkey_modules.c +++ b/modules/nft_fwd_dup_cve_2022_25636/skeletonkey_modules.c @@ -1074,6 +1074,7 @@ const struct skeletonkey_module nft_fwd_dup_module = { .detect_yara = nft_fwd_dup_yara, .detect_falco = nft_fwd_dup_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNET) + nfnetlink batch (NEWTABLE netdev + NEWCHAIN HW_OFFLOAD + NEWRULE with 16 immediate(NF_ACCEPT) + 1 fwd). Offload hook walks the rule advertising num_actions+=16 but allocates only the original-actions size -> OOB write at entries[16] into adjacent kmalloc-512. msg_msg groom tagged 'SKELETONKEY_FWD'. Writes /tmp/skeletonkey-nft_fwd_dup.log. Audit-visible via unshare + socket(NETLINK_NETFILTER) + sendmsg + ioctl(SIOCGIFFLAGS/SIOCSIFFLAGS loopback) + msgsnd. Dmesg: KASAN or silent. Cleanup callback drains IPC queues and unlinks log.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_nft_fwd_dup(void) diff --git a/modules/nft_payload_cve_2023_0179/skeletonkey_modules.c b/modules/nft_payload_cve_2023_0179/skeletonkey_modules.c index 4f49caf..f6a707b 100644 --- a/modules/nft_payload_cve_2023_0179/skeletonkey_modules.c +++ b/modules/nft_payload_cve_2023_0179/skeletonkey_modules.c @@ -1184,6 +1184,7 @@ const struct skeletonkey_module nft_payload_module = { .detect_yara = nft_payload_yara, .detect_falco = nft_payload_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNET) + nfnetlink batch (NEWTABLE + NEWCHAIN/LOCAL_OUT + NEWSET with oversized NFTA_SET_DESC + NEWSETELEM whose NFTA_PAYLOAD_SREG = attacker verdict code). On packet eval, regs->verdict.code is used unchecked as index into regs->data[] -> OOB. Dual-slab groom (kmalloc-1k + kmalloc-cg-96). Trigger via sendto(AF_INET, 127.0.0.1:31337). Writes /tmp/skeletonkey-nft_payload.log. Audit-visible via unshare + socket(NETLINK_NETFILTER) + sendmsg + msgsnd + socket(AF_INET)/sendto. Cleanup callback unlinks log.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_nft_payload(void) diff --git a/modules/nft_set_uaf_cve_2023_32233/skeletonkey_modules.c b/modules/nft_set_uaf_cve_2023_32233/skeletonkey_modules.c index f7dce47..6ddedc3 100644 --- a/modules/nft_set_uaf_cve_2023_32233/skeletonkey_modules.c +++ b/modules/nft_set_uaf_cve_2023_32233/skeletonkey_modules.c @@ -1068,6 +1068,7 @@ const struct skeletonkey_module nft_set_uaf_module = { .detect_yara = nft_set_uaf_yara, .detect_falco = nft_set_uaf_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNET) + single nfnetlink transaction: NEWTABLE + NEWCHAIN + NEWSET (anonymous, ANONYMOUS|CONSTANT|EVAL) + NEWRULE with nft_lookup referencing the anon set + DELSET + DELRULE. Vulnerable kernels do not deactivate the lookup's set ref on commit -> UAF when set frees. msg_msg cg-512 spray (32 queues x 16 msgs, tag 'SKELETONKEY_SET'). --full-chain re-fires with forged headers (data ptr = kaddr) and NEWSETELEM payload. Writes /tmp/skeletonkey-nft_set_uaf.log. Audit-visible via unshare + socket(NETLINK_NETFILTER) + sendmsg + msgsnd. Dmesg: KASAN oops on UAF. Cleanup unlinks log.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_nft_set_uaf(void) diff --git a/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c b/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c index 800f964..8db5a07 100644 --- a/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c +++ b/modules/overlayfs_cve_2021_3493/skeletonkey_modules.c @@ -556,6 +556,7 @@ const struct skeletonkey_module overlayfs_module = { .detect_yara = overlayfs_yara, .detect_falco = overlayfs_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNS) for CAP_SYS_ADMIN; mount('overlay', merged, ...); compile + copy payload into the merged dir (writes upper on host fs); setxattr(upper_payload, 'security.capability', cap_setuid+ep) - the bug is that this xattr persists on the HOST fs despite being set inside userns. Parent then execve's the now-CAP_SETUID payload, calls setuid(0), execs /bin/sh. Artifacts: /tmp/skeletonkey-ovl-XXXXXX/ workdir; cleaned on exit/failure (on success the exec replaces the process so cleanup does not run). Audit-visible via unshare + mount(overlay) + setxattr(security.capability) + execve of attacker-controlled binary. Dmesg silent.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_overlayfs(void) diff --git a/modules/overlayfs_setuid_cve_2023_0386/skeletonkey_modules.c b/modules/overlayfs_setuid_cve_2023_0386/skeletonkey_modules.c index fef4659..1942458 100644 --- a/modules/overlayfs_setuid_cve_2023_0386/skeletonkey_modules.c +++ b/modules/overlayfs_setuid_cve_2023_0386/skeletonkey_modules.c @@ -472,6 +472,7 @@ const struct skeletonkey_module overlayfs_setuid_module = { .detect_yara = overlayfs_setuid_yara, .detect_falco = overlayfs_setuid_falco, .opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNS) + overlayfs mount with a setuid-root binary in lower (e.g. /usr/bin/su); chown on the merged view triggers copy-up that preserves the setuid bit in upper - but upper is owned by the unprivileged user. Overwrites upper-layer contents with attacker payload and execve's for root. Artifacts: /tmp/skeletonkey-ovlsu-XXXXXX/ (workdir with payload.c, binary, overlay mounts); cleanup callback removes these. Audit-visible via unshare(CLONE_NEWUSER|CLONE_NEWNS) + mount(overlay) + chown on the merged view. No network. Dmesg silent on success.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_overlayfs_setuid(void) diff --git a/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c b/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c index 58fd09a..3cce646 100644 --- a/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c +++ b/modules/pack2theroot_cve_2026_41651/skeletonkey_modules.c @@ -791,6 +791,7 @@ const struct skeletonkey_module pack2theroot_module = { .detect_yara = p2tr_yara, .detect_falco = p2tr_falco, .opsec_notes = "TOCTOU race in PackageKit's polkit-auth + D-Bus InstallFiles dispatcher: sends back-to-back async calls (first with SIMULATE to bypass polkit, second with the malicious .deb) so the cached flags are overwritten before the idle callback fires. Builds a minimal .deb ar archive in pure C with a postinst that installs a setuid bash. Writes /tmp/.pk-dummy-.deb, /tmp/.pk-payload-.deb, and /tmp/skeletonkey-pack2theroot.state; via the polkit-bypassed postinst plants /tmp/.suid_bash setuid root. Audit-visible via dpkg execve from packagekitd for a non-root caller, chmod(2) on /tmp/.suid_bash, creat/openat on the .deb files. Cleanup callback unlinks the .debs and best-effort removes /tmp/.suid_bash (which is owned by root).", + .arch_support = "any", }; void skeletonkey_register_pack2theroot(void) diff --git a/modules/ptrace_traceme_cve_2019_13272/skeletonkey_modules.c b/modules/ptrace_traceme_cve_2019_13272/skeletonkey_modules.c index 5487458..ad2c487 100644 --- a/modules/ptrace_traceme_cve_2019_13272/skeletonkey_modules.c +++ b/modules/ptrace_traceme_cve_2019_13272/skeletonkey_modules.c @@ -368,6 +368,7 @@ const struct skeletonkey_module ptrace_traceme_module = { .detect_yara = NULL, .detect_falco = ptrace_traceme_falco, .opsec_notes = "Parent and child cooperate: child calls ptrace(PTRACE_TRACEME) (recording the parent's current credentials), then sleeps; parent execve's a setuid binary (pkexec or su) and elevates. The stale ptrace_link in the child still holds the old (non-root) credentials, so PTRACE_ATTACH succeeds against the now-root parent; the child injects shellcode at the parent's RIP via PTRACE_POKETEXT and detaches. Audit-visible via ptrace with a0=0 (PTRACE_TRACEME) closely followed by execve of a setuid binary in the parent process. No file artifacts; no persistent changes. No cleanup callback - the exploit execs /bin/sh and does not return.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_ptrace_traceme(void) diff --git a/modules/pwnkit_cve_2021_4034/skeletonkey_modules.c b/modules/pwnkit_cve_2021_4034/skeletonkey_modules.c index ca81909..a1b2fe4 100644 --- a/modules/pwnkit_cve_2021_4034/skeletonkey_modules.c +++ b/modules/pwnkit_cve_2021_4034/skeletonkey_modules.c @@ -473,6 +473,7 @@ const struct skeletonkey_module pwnkit_module = { .detect_yara = pwnkit_yara, .detect_falco = pwnkit_falco, .opsec_notes = "Invokes pkexec with argc==0 so the first envp slot is misread as argv[0]; pkexec's iconv-during-decoding loads attacker .so via dlopen by way of crafted GCONV_PATH + CHARSET env vars. Builds a gconv payload .so and gconv-modules cache in /tmp/skeletonkey-pwnkit-XXXXXX (compiles via fork/execl of gcc). Audit-visible via execve(/usr/bin/pkexec) with GCONV_PATH and CHARSET set. No network. Cleanup callback removes /tmp/skeletonkey-pwnkit-* (on failure path; on success the exec replaces the process).", + .arch_support = "any", }; void skeletonkey_register_pwnkit(void) diff --git a/modules/sequoia_cve_2021_33909/skeletonkey_modules.c b/modules/sequoia_cve_2021_33909/skeletonkey_modules.c index 76769ca..3f196e8 100644 --- a/modules/sequoia_cve_2021_33909/skeletonkey_modules.c +++ b/modules/sequoia_cve_2021_33909/skeletonkey_modules.c @@ -752,6 +752,7 @@ const struct skeletonkey_module sequoia_module = { .detect_yara = sequoia_yara, .detect_falco = sequoia_falco, .opsec_notes = "Builds ~5000 nested directories under /tmp/skeletonkey-sequoia (each name 200 'A' chars); enters userns for CAP_SYS_ADMIN; bind-mounts the leaf over itself to amplify the rendered mountinfo string length; reads /proc/self/mountinfo to trigger the int-vs-size_t overflow in seq_buf_alloc(), producing an OOB write of mountinfo bytes off the stack buffer. Artifacts: /tmp/skeletonkey-sequoia/ (deep tree + bind mounts) and /tmp/skeletonkey-sequoia.log (byte count + dmesg sample). Audit-visible via unshare(CLONE_NEWUSER|CLONE_NEWNS) + mount() + burst of ~5000 mkdir/mkdirat. No network. Cleanup callback walks back down the tree, unmounts, removes dirs, unlinks the .log.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_sequoia(void) diff --git a/modules/stackrot_cve_2023_3269/skeletonkey_modules.c b/modules/stackrot_cve_2023_3269/skeletonkey_modules.c index e4f7c4d..c269889 100644 --- a/modules/stackrot_cve_2023_3269/skeletonkey_modules.c +++ b/modules/stackrot_cve_2023_3269/skeletonkey_modules.c @@ -1014,6 +1014,7 @@ const struct skeletonkey_module stackrot_module = { .detect_yara = stackrot_yara, .detect_falco = stackrot_falco, .opsec_notes = "Child forks, enters userns, builds a race region with MAP_GROWSDOWN + anchor VMAs, sprays kmalloc-192 with msg_msg payloads, then spawns Thread A (mremap/munmap of region boundary to rotate maple-tree nodes) + Thread B (fork+fault the growsdown region to deref freed node). UAF in __vma_adjust fires if a sprayed msg_msg reclaims the freed node. Writes /tmp/skeletonkey-stackrot.log (iteration counts + slab delta). Audit-visible via unshare + mremap/munmap bursts on stack regions + msgsnd spray. No network. Cleanup callback unlinks /tmp log.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_stackrot(void) diff --git a/modules/sudo_samedit_cve_2021_3156/skeletonkey_modules.c b/modules/sudo_samedit_cve_2021_3156/skeletonkey_modules.c index 011b878..354d051 100644 --- a/modules/sudo_samedit_cve_2021_3156/skeletonkey_modules.c +++ b/modules/sudo_samedit_cve_2021_3156/skeletonkey_modules.c @@ -506,6 +506,7 @@ const struct skeletonkey_module sudo_samedit_module = { .detect_yara = NULL, .detect_falco = sudo_samedit_falco, .opsec_notes = "Invokes sudoedit with argv = { 'sudoedit', '-s', trailing-backslash, then ~60 padding args each ending in backslash }; the parser's unescape loop in set_cmnd() walks past the end of the argv string for the trailing-backslash argument, copying adjacent stack/env into an undersized heap buffer. Audit-visible via execve(/usr/bin/sudoedit) with -s and a trailing-backslash argv. No persistent file artifacts (only best-effort removal of /tmp/.sudo_edit_*). No network. Dmesg silent unless sudo crashes (SIGSEGV). Per-distro heap layout determines landing; verifies geteuid()==0 afterward.", + .arch_support = "any", }; void skeletonkey_register_sudo_samedit(void) { skeletonkey_register(&sudo_samedit_module); } diff --git a/modules/sudoedit_editor_cve_2023_22809/skeletonkey_modules.c b/modules/sudoedit_editor_cve_2023_22809/skeletonkey_modules.c index f4e7db7..261721e 100644 --- a/modules/sudoedit_editor_cve_2023_22809/skeletonkey_modules.c +++ b/modules/sudoedit_editor_cve_2023_22809/skeletonkey_modules.c @@ -663,6 +663,7 @@ const struct skeletonkey_module sudoedit_editor_module = { .detect_yara = sudoedit_editor_yara, .detect_falco = sudoedit_editor_falco, .opsec_notes = "Sets EDITOR=' -- /etc/passwd' so sudoedit splits on the literal '--' and treats /etc/passwd as an additional editable file. Compiled helper appends 'skel::0:0:skeletonkey:/root:/bin/sh' to the post-'--' target; sudoedit runs the helper as root and copies back. Artifacts: /tmp/skeletonkey-sudoedit-XXXXXX (helper.c, helper binary, optional passwd.before backup); /etc/passwd gets the new 'skel' entry; drops root via 'su skel'. Audit-visible via execve(/usr/bin/sudoedit) with EDITOR/VISUAL/SUDO_EDITOR containing the literal '--' token. No network. Cleanup callback restores /etc/passwd from backup (if root) or removes the 'skel' line, and removes the /tmp dir.", + .arch_support = "any", }; void skeletonkey_register_sudoedit_editor(void) diff --git a/modules/vmwgfx_cve_2023_2008/skeletonkey_modules.c b/modules/vmwgfx_cve_2023_2008/skeletonkey_modules.c index 0f8be3d..2012235 100644 --- a/modules/vmwgfx_cve_2023_2008/skeletonkey_modules.c +++ b/modules/vmwgfx_cve_2023_2008/skeletonkey_modules.c @@ -771,6 +771,7 @@ const struct skeletonkey_module vmwgfx_module = { .detect_yara = vmwgfx_yara, .detect_falco = vmwgfx_falco, .opsec_notes = "Opens /dev/dri/card* (vmwgfx DRM - only reachable on VMware guests); DRM_IOCTL_VMW_CREATE_DMABUF with size=4096+16 lands in the kmalloc-512 page-count bucket but the byte-length overruns during kunmap_atomic copy in ttm_bo_kmap; mmap + write recognizable pattern across page boundary; UNREF commits the OOB into adjacent kmalloc-512. msg_msg spray tagged 'SKVMWGFX'. Writes /tmp/skeletonkey-vmwgfx.log (slab counts pre/post, trigger success). Audit-visible via openat(/dev/dri/card*), ioctl(0xc010644a CREATE / 0x4004644b UNREF), msgsnd spray. No network. Cleanup callback unlinks /tmp log; --full-chain re-seeds spray with kaddr-tagged payloads and the modprobe_path finisher arbitrates via 3s sentinel.", + .arch_support = "x86_64+unverified-arm64", }; void skeletonkey_register_vmwgfx(void) diff --git a/skeletonkey.c b/skeletonkey.c index 5d1aff7..3ffb117 100644 --- a/skeletonkey.c +++ b/skeletonkey.c @@ -35,7 +35,7 @@ #include #include -#define SKELETONKEY_VERSION "0.7.0" +#define SKELETONKEY_VERSION "0.7.1" static const char BANNER[] = "\n" @@ -216,6 +216,13 @@ static void emit_module_json(const struct skeletonkey_module *m, bool include_ru free(op); } + /* Architecture support for the exploit body. */ + if (m->arch_support) { + char *a = json_escape(m->arch_support); + fprintf(stdout, ",\"arch_support\":\"%s\"", a ? a : ""); + free(a); + } + /* Empirical verification records: (distro, kernel, date) tuples * where the module's detect() was confirmed against a real target. */ size_t nv = 0; @@ -272,27 +279,43 @@ static int cmd_list(const struct skeletonkey_ctx *ctx) fprintf(stdout, "]}\n"); return 0; } - fprintf(stdout, "%-20s %-18s %-3s %-3s %-25s %s\n", - "NAME", "CVE", "KEV", "VFY", "FAMILY", "SUMMARY"); - fprintf(stdout, "%-20s %-18s %-3s %-3s %-25s %s\n", - "----", "---", "---", "---", "------", "-------"); - size_t n_kev = 0, n_vfy = 0; + /* The ARCH column shows where exploit() is known/expected to work: + * "any" → userspace or arch-agnostic kernel primitive + * "x64" → x86_64 only (entrybleed) + * "x64?" → x86_64 verified, arm64 untested (the honest default + * for kernel modules that haven't been arm64-confirmed) */ + fprintf(stdout, "%-20s %-18s %-3s %-3s %-5s %-25s %s\n", + "NAME", "CVE", "KEV", "VFY", "ARCH", "FAMILY", "SUMMARY"); + fprintf(stdout, "%-20s %-18s %-3s %-3s %-5s %-25s %s\n", + "----", "---", "---", "---", "----", "------", "-------"); + size_t n_kev = 0, n_vfy = 0, n_any = 0; for (size_t i = 0; i < n; i++) { const struct skeletonkey_module *m = skeletonkey_module_at(i); const struct cve_metadata *md = cve_metadata_lookup(m->cve); bool in_kev = md && md->in_kev; bool verified = verifications_module_has_match(m->name); + const char *arch_abbr = "?"; + if (m->arch_support) { + if (strcmp(m->arch_support, "any") == 0) { arch_abbr = "any"; n_any++; } + else if (strcmp(m->arch_support, "x86_64") == 0) { arch_abbr = "x64"; } + else { arch_abbr = "x64?"; } + } if (in_kev) n_kev++; if (verified) n_vfy++; - fprintf(stdout, "%-20s %-18s %-3s %-3s %-25s %s\n", + fprintf(stdout, "%-20s %-18s %-3s %-3s %-5s %-25s %s\n", m->name, m->cve, in_kev ? "★" : "", verified ? "✓" : "", + arch_abbr, m->family, m->summary); } fprintf(stdout, "\n%zu modules registered · %zu in CISA KEV (★) · " - "%zu empirically verified in real VMs (✓)\n", - n, n_kev, n_vfy); + "%zu empirically verified in real VMs (✓) · " + "%zu arch-independent (any)\n", + n, n_kev, n_vfy, n_any); + fprintf(stdout, "ARCH key: 'any' = userspace or arch-agnostic; " + "'x64' = x86_64 only; 'x64?' = x86_64 verified, " + "arm64 untested\n"); return 0; } @@ -666,6 +689,8 @@ static int cmd_module_info(const char *name, const struct skeletonkey_ctx *ctx) m->exploit ? "exploit " : "", m->mitigate ? "mitigate " : "", m->cleanup ? "cleanup " : ""); + if (m->arch_support) + fprintf(stdout, "arch support: %s\n", m->arch_support); fprintf(stdout, "detect rules: %s%s%s%s\n", m->detect_auditd ? "auditd " : "", m->detect_sigma ? "sigma " : "",