v0.10.0
81 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
73c7d09445 |
overlayfs: direct uid=0 witness; broaden false-OK audit (all OK sites clean)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
overlayfs previously claimed EXPLOIT_OK from a getxattr proxy (the persisted security.capability xattr is the bug's signature) then exec-transferred the cap'd payload. Tightened to a DIRECT witness: the payload now takes a proof-path arg and, running as root (setuid(0) works because of the persisted cap), drops a root-owned proof + setuid bash; the module forks it, then reports OK only after stat() confirms the proof is root-owned. Re-verified on focal 5.4.0-26 (uid 1000 -> root-owned proof + -rwsr-xr-x root:root bash). Broadened the false-EXPLOIT_OK audit from exec-transfer callers to EVERY EXPLOIT_OK return site. The copy_fail_family root-pop (exploit_su.c) runs verify_plant() — confirming the shellcode actually landed in the setuid binary's page cache, reverting otherwise — before exec'ing it, so it's proxy-verified like dirtydecrypt/fragnesia, not a blind exec-transfer. Conclusion recorded in docs/EXPLOITED.md: no false-OKs remain beyond the four already fixed; every OK is backed by a real out-of-band check (root-owned artifact stat, getxattr, passwd grep, setuid(0) gate, or page-cache verify_plant). Host build + unit harness green (33 + 115). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y |
||
|
|
65588599ff |
dirty_cow: verify end-to-end on 4.8.0; robust su helper (dirty_pipe too)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Verified the dirty_cow fix end-to-end on a genuinely Dirty-COW-vulnerable mainline 4.8.0 kernel (installed the kernel.ubuntu.com 4.8.0 deb on a 16.04 image + virtio-rng for entropy). A standalone built verbatim from the module's primitive + escalation raced root's password field, authenticated as root over a pty, planted a root-owned setuid bash, and left /etc/passwd byte-identical. (The full skeletonkey binary won't compile on xenial's 4.4-era uapi headers — unrelated nft_* modules use newer kernel constants — so the verbatim standalone stands in for --exploit dirty_cow there.) Robustness fix (both dirty_cow AND dirty_pipe): the su-over-pty step now POLLS for the password prompt and hard-caps at 20s. The previous fixed-delay write raced su's prompt setup and HUNG on xenial; without a cap that would block the revert and leave /etc/passwd poisoned. Now su can never hang the module, and the revert always runs. Also: netfilter_xtcompat now includes <linux/if.h> for IFNAMSIZ (ip_tables.h doesn't pull it transitively on older kernel headers, e.g. Ubuntu 16.04) — a real portability fix surfaced by building on xenial. Host build + unit harness green (33 + 115). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y |
||
|
|
68dac6c063 |
dirty_cow: fix false-EXPLOIT_OK (same 3 bugs as dirty_pipe) + audit the corpus
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
False-EXPLOIT_OK audit: the dispatcher's run_callback_isolated reports OK when the
exploit execve()s (FD_CLOEXEC closes the result pipe -> parent reads EOF), so any
exploit whose main path execs a not-guaranteed-root target lies. Audited every
exec*-calling module (results in docs/EXPLOITED.md).
dirty_cow had the identical 3-bug pattern to the pre-fix dirty_pipe:
1. raced the CALLER's UID field to "0000" then ran `su self` -> still needs the
caller's password, never rooted anything;
2. execlp'd su -> dispatcher's exec-transfer path reported a FALSE EXPLOIT_OK;
3. reverted via drop_caches (needs root) -> left the running /etc/passwd
corrupted when run unprivileged.
Plus a latent overflow: the success-check readback[16] was too small for a
payload > 16 bytes.
Fix (byte-for-byte the verified dirty_pipe technique): race ROOT's password field
to a known $6$ hash -> authenticate as root over a pty -> plant a root-owned proof
+ setuid bash -> revert by racing the original bytes back through the Dirty COW
primitive (no root / no drop_caches). Success judged only by the OOB artifact;
readback buffer bumped to 512. cleanup() re-reverts idempotently.
The escalation half is identical to dirty_pipe (verified end-to-end); the COW
primitive itself needs a pre-4.8.3 kernel to land, which no cached VM has, so on
patched hosts it now fails HONESTLY with no corruption. Unit harness green.
Other exec-transfer modules audited clean or already-fixed; two (overlayfs proxy,
sudoedit_editor unverified reporting) noted for tightening.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
|
||
|
|
678a37b2f5 |
dirty_pipe: fix 3 bugs — working CVE-2022-0847 exploit that lands real root
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Verified out-of-band on a genuinely pre-fix mainline 5.16.0 kernel (installed
from kernel.ubuntu.com onto the jammy image; every cached cloud image was
pre-5.8 or backport-patched). `skeletonkey --exploit dirty_pipe` (uid 1000) ->
EXPLOIT_OK + a -rwsr-xr-x root:root bash, and /etc/passwd is byte-identical
afterward.
Three real bugs in the shipped module:
1. Wrong escalation: it flipped the CALLER's UID field to "0000" and ran
`su <self>`, which still demands the caller's password -> never rooted
anything.
2. False EXPLOIT_OK: it execlp'd su, so the dispatcher's "exec transferred ->
clean exit = OK" path reported success even on su's auth failure.
3. Dangerous revert: revert_passwd_page_cache() used drop_caches, which needs
root -> as an unprivileged caller it left the running system's /etc/passwd
page cache corrupted (broke sshd user resolution in testing).
Rewrite (AabyssZG-style, self-contained + verifying):
- overwrite ROOT's password field with a known $6$ crypt hash via the Dirty
Pipe primitive (the longer hash clobbers into following lines, transiently);
- authenticate as root over a pty with the matching password (su reads the
password from the controlling tty, not stdin);
- plant a root-owned proof + setuid bash; judge success ONLY by stat()'ing the
root-owned artifact;
- revert the page cache by writing the saved original bytes back through the
Dirty Pipe primitive itself — no root, no drop_caches, nothing persists.
cleanup() re-reverts idempotently. Unit harness green (33 + 115).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
|
||
|
|
82ba6e0d08 |
sudo_samedit: working CVE-2021-3156 (Baron Samedit) exploit — lands real root
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The shipped module drove a structural sudoedit trigger with no heap-grooming offsets and honestly reported EXPLOIT_FAIL. Ported blasty's proven technique: the `sudoedit -s` set_cmnd() unescape overflow overwrites a glibc NSS service_user, so the subsequent NSS lookup dlopen's an attacker-planted libnss_X/'P0P_SH3LLZ_ .so.2' from CWD, whose constructor runs while sudo is still root. Implementation follows the corpus's runtime-compile pattern: build the NSS payload on the target (unique -DSK_PROOF/-DSK_ROOTBASH), lay out the libnss_X/ dir, exec sudoedit with the crafted argv (AAAA..\ / \ / BBBB..\) and env (null_stomp × "\\", "X/P0P_SH3LLZ_", padded LC_ALL) from that CWD, and verify root by stat()'ing the root-owned artifacts — never self-report. Grooming lengths are libc-family specific (Ubuntu 56/54/63/212, Debian 64/49/60/214); a null_stomp_len ±8 sweep (blasty brute.sh's axis) is the fallback for drift. Verified out-of-band on Ubuntu 18.04.0 / sudo 1.8.21p2 / libc-2.27, as uid 1000 (non-sudoer — the overflow precedes the sudoers/password check): `skeletonkey --exploit sudo_samedit` -> EXPLOIT_OK + a -rwsr-xr-x root:root bash, primary lengths landing first try. Gated on cc + sudoedit; cleanup() removes artifacts and the scratch build dir. Unit harness green (33 + 115). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y |
||
|
|
635f7d2d24 |
ptrace_traceme: working CVE-2019-13272 exploit — lands real root (x86_64)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The bundled sequence had the mechanism backwards (it PTRACE_ATTACHed the now-root parent) and never rooted anything. Replaced it with the proven Jann Horn (Project Zero #1903) / bcoles technique: - a "middle" process execs setuid pkexec (euid 0 for a window); - its child spins until it sees middle is euid 0, calls PTRACE_TRACEME (recording middle's ROOT creds as ptracer_cred — the bug), then execs pkexec itself. The traced setuid exec is NOT degraded because ptracer_cred is root, so the child becomes real root, still traced; - staged execveat() self-re-exec injects the payload as root. The staged self-re-exec needs the exploit to exist as its own binary with an argv[0] stage dispatcher, so the proven PoC is embedded verbatim (ptrace_helper_src.h — only spawn_shell() changed, to plant a root-owned proof + setuid bash instead of only an interactive shell), compiled on the target at runtime with unique -DSK_PROOF/-DSK_ROOTBASH paths, run, and verified by stat()'ing the root-owned artifacts (never self-report). Verified out-of-band on Ubuntu 18.04.0 / 4.15.0-50: `skeletonkey --exploit ptrace_traceme` as uid 1000 -> EXPLOIT_OK + a -rwsr-xr-x root:root bash. Real-world precondition, honestly reported: pkexec must authorize an auto-discovered implicit-active=yes helper, which needs an active local session (desktop) or a permissive polkit policy; over inactive ssh it returns "Not authorized" and the module reports EXPLOIT_FAIL with that diagnosis. Gated on a C compiler + pkexec; x86_64 only (register-level injection). cleanup() removes the artifacts. Unit harness green (33 + 115). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y |
||
|
|
70972e0c9d |
sudoedit_editor: fix CVE-2023-22809 exploit to actually land root
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The module built the right EDITOR="helper -- /etc/passwd" injection but failed
two ways, both now fixed (VM-verified root on Ubuntu 22.04.0 / sudo
1.9.9-1ubuntu2):
1. Writable-CWD guard: sudoedit refuses to edit any file whose parent dir is
user-writable. The injected "--" is resolved relative to CWD, so running
from home/tmp made sudoedit abort "--: editing files in a writable
directory" before the editor ran. Fix: chdir("/") in the sudoedit child.
2. Wrong tmp copy: sudoedit hands the editor one tmp copy per file, named
<basename>.XXXXXX. The helper wrote argv[argc-1] — the sudoers-authorized
COVER file (e.g. motd), not the target. Fix: the helper now matches the
target's basename prefix (passed via SKEL_TARGET) to pick the right copy.
Result: /etc/passwd gains a skel::0:0 row; `su skel` -> uid=0(root).
docs/EXPLOITED.md + VERIFICATIONS.jsonl updated. Unit harness green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
|
||
|
|
6edf78f765 |
overlayfs_setuid: working CVE-2023-0386 exploit (libfuse) — lands real root
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The shipped module used a bogus chown-the-merged-view technique that never
worked. Rewrote it as a faithful port of the public PoC (xkaneiki): a libfuse
filesystem exports a setuid-root /file (st_uid=0, mode 04777), mounted in the
init ns via the setuid fusermount helper; then unshare(USER|NS) + overlay with
that FUSE mount as lowerdir; open(merged/file, O_WRONLY) triggers copy-up which
materialises upper/file as a genuine setuid-root binary; the unprivileged parent
execs it for real root.
VM-verified landing real root on Ubuntu 22.04.0 / 5.15.0-25 (uid=0 witnessed
out-of-band: /tmp/skeletonkey-ovlsu-pwned shows uid=0(root), and the dropped
setuid /tmp/.suid_bash runs with euid=0).
Four things were each required and took isolation to find:
1. Overlay refuses a userns-mounted FUSE lowerdir (ENOSYS) -> FUSE must be
mounted in the init ns via fusermount (libfuse). A raw /dev/fuse server was
tried and abandoned (fragile; destabilised the kernel on malformed INIT).
2. fuse2 low-level API (fuse_mount/fuse_new/fuse_loop_mt, empty args); fuse_main
advertises copy_file_range caps that ENOSYS at copy-up with no fallback.
3. read_buf callback (copy-up splice read path).
4. ioctl callback (copy-up's FS_IOC_GETFLAGS) — the last missing piece.
libfuse is linked conditionally via pkg-config (fuse/fuse3), matching the
pack2theroot+libglib precedent; without it the module stubs to PRECOND_FAIL.
Makefile detects it and adds $(OSU_LIBS) to the link. Module header + opsec
notes rewritten to the real technique; docs/EXPLOITED.md updated. 148-test unit
harness green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
|
||
|
|
e01aa99ec6 |
af_packet2: fix uid_map (read uid/gid before unshare) — same bug as cgroup
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
af_packet2 read getuid()/getgid() AFTER unshare(CLONE_NEWUSER), so it wrote
"0 65534 1" to uid_map/gid_map (65534 = nobody, the initial unmapped id) and
the write was rejected EPERM — the userns-root mapping silently failed and the
CAP_NET_RAW primitive could not fire. Capture the outer uid/gid before unshare.
Audited every userns module for the pattern; only cgroup_release_agent (fixed
in
|
||
|
|
8c45b2beb8 |
cgroup_release_agent: fix uid_map (read uid pre-unshare) + add CLONE_NEWCGROUP
Two real bugs found via VM verification (Ubuntu 20.04.0 / 5.4.0-26): 1. uid_map EPERM: the child captured getuid()/getgid() AFTER unshare(CLONE_NEWUSER), where they return 65534 (nobody, the initial unmapped id), so it wrote "0 65534 1" to uid_map — rejected with EPERM. Now the outer uid/gid are read BEFORE unshare, yielding the correct "0 <uid> 1" single-uid self-map. (overlayfs got this right, which is why it worked; this module didn't.) 2. mount EPERM: the unprivileged cgroup-v1 mount needs a private cgroup namespace. Added CLONE_NEWCGROUP (with a fallback) — mounting an unused v1 controller then succeeds. With both fixed the userns+cgroupns+mount setup is correct. Note: on a stock systemd host every v1 controller is already mounted (its release_agent is owned by init-root and unwritable from the userns) and creating a fresh named hierarchy is refused, so a bare unprivileged user cannot own release_agent — CVE-2022-0492 is reachable in a container context (CAP_SYS_ADMIN / an ownable cgroup), consistent with the module's "host root from rootless container" framing. Verified out-of-band; see docs/EXPLOITED.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y |
||
|
|
59cc2be065 |
ptrace_traceme: stop the false EXPLOIT_OK; add exploit-verification ledger
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
ptrace_traceme reported EXPLOIT_OK while obtaining no root on a genuinely vulnerable host (Ubuntu 18.04.2 / 4.15.0-50). The bundled ptrace sequence is a non-working placeholder — it PTRACE_ATTACHes to the tracer, which by then has reparented to init, so the attach fails EPERM; the parent then execve'd the setuid trigger and the dispatcher's exec-transfer path reported a false OK. Now the parent fires the trigger in a grandchild, stays alive, and verifies euid==0 before claiming success — otherwise honest EXPLOIT_FAIL with a pointer to the real CVE-2019-13272 technique (Jann Horn / bcoles) that still needs porting. Adds docs/EXPLOITED.md: the exploit-verification ledger (root witnessed OUT OF BAND, not from the module's self-report). Confirmed landing root: refluxfs, overlayfs, pwnkit (after its gconv fix), sudo_runas_neg1. Needs PoC ports: ptrace_traceme, overlayfs_setuid (CVE-2023-0386 FUSE copy-up), sudo_samedit (heap), cgroup_release_agent. ~30 modules still exploit-untested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y |
||
|
|
24b839eccf |
pwnkit: fix the gconv re-injection layout so the exploit actually lands root
The exploit reported EXPLOIT_OK but did NOT get root on a genuinely vulnerable
target (Ubuntu 20.04.0, polkit 0.105-26ubuntu1). pkexec printed "Cannot run
program pwnkit" + glibc "Could not open converter from UTF-8 to PWNKIT" and no
root shell was obtained — a false positive from the dispatcher's "execve
transferred → clean exit = OK" path.
Root cause: the module built workdir/pwnkit/{gconv-modules,PWNKIT.so} correctly
but omitted the two pieces that make the GCONV_PATH trick fire:
1. a directory literally named "GCONV_PATH=." containing an executable
"pwnkit", so pkexec's g_find_program_in_path() resolves argv[0] and
RE-INJECTS GCONV_PATH=./pwnkit into the sanitised environment; and
2. chdir(workdir) so the re-injected relative "./pwnkit" resolves to the
gconv dir.
It also set an absolute GCONV_PATH=<workdir>/pwnkit directly in envp, which
pkexec strips (GCONV_PATH is on polkit's blacklist) — removed.
VM-verified after the fix on Ubuntu 20.04.0 / 5.4.0-26 (polkit 0.105-26ubuntu1):
unprivileged sk -> uid=0(root), confirmed out-of-band (wrote /root/, read
/etc/shadow). overlayfs (CVE-2021-3493) also confirmed landing root on the same
host; sudo_samedit returns an honest EXPLOIT_FAIL there (heap not landed).
Found via the new qemu exploit-verification sweep (root confirmed out-of-band,
not from the module's self-report).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
|
||
|
|
c55adc1840 |
refluxfs: drop the redundant standalone verify harness
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The own-files reachability harness tools/verify-vm/refluxfs_verify.c is superseded by the module itself: the safe default --exploit does the same own-files reachability confirmation, and --full-chain provides the stronger end-to-end proof (actual root). Remove the standalone and repoint the three doc references (MODULE.md, RELEASE_NOTES.md, targets.yaml) at the module's full-chain verification. The 4/4 own-files measurement it produced is kept as a historical data point, just without the now-deleted reproducer. No code depended on it; 148-test unit harness still green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y |
||
|
|
5c18b678a5 |
refluxfs: wire the --full-chain /etc/passwd root pop (🟡 trigger -> 🟢 full chain)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Promotes refluxfs (CVE-2026-64600) to a 🟢 full-chain module. VM-verified end-to-end on Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1.x86_64 under qemu/KVM, unprivileged uid=1000, SELinux Enforcing: skeletonkey --exploit refluxfs --i-know --full-chain reflink-clones /etc/passwd, races the CoW window (32 writers / 8 helpers), strips root's password field on-disk (root❌ -> root::, the public PoC's technique), evicts the stale page cache, verifies via O_DIRECT and returns EXPLOIT_OK. `su root` (empty password) then gives uid 0. 3/3 wins on a private-extent target (1244/3716/7913 rounds, 4-30s). Safety properties (this bug rewrites the block device permanently, unlike the page-cache 🟢 modules): - Crafts the payload FIRST and refuses unless it can preserve both root and the invoking user's line; every other passwd line is kept byte-for-byte. A tail-truncating port drops sshd/nobody/the caller and bricks login (hit exactly this during development). - Backs /etc/passwd up before the race; restores on failure; cleanup() restores it (run as root after the pop). - Destructive path gated behind --full-chain. Plain --exploit / --auto run only the safe own-files trigger (EXPLOIT_FAIL), unchanged. Exploitability constraint discovered during verification (NOT in the Qualys writeup): the race only fires when the target's extent is PRIVATE going in. The block starts at refcount 2 (target + attacker clone), the concurrent CoW drops it to 1, and the stale writer reads "1 -> private". A file already reflink-shared with a third file keeps a post-CoW refcount > 1 and is NOT attackable via that target. Stock Rocky 9 ships /etc/passwd pre-shared and was unattackable across ~41,000 rounds; rewriting it to a private extent (byte-identical content, as any useradd/passwd/vipw does) made it fall in ~2,000 rounds. So the exploitable state is the normal administered state. Also fixed a page-cache staleness bug in the win path: the overwrite bypasses the target inode, so its clean cached pages are never invalidated and a buffered read (getpwnam in `su`, or the module's own verify) would see the OLD passwd. The module now issues POSIX_FADV_DONTNEED on a win and verifies via O_DIRECT. detect() --active gains a per-target extent-privacy check: it reports whether /etc/passwd is private (attackable) or already-shared (not). 88-test unit harness still green (148 total). Docs updated: MODULE.md (full-chain flow, private-extent precondition, verification tables), NOTICE.md, CVES.md (🟢 + tier/ops tables), README (15 full-chain / 13 primitive; lands-root list), RELEASE_NOTES, targets.yaml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y |
||
|
|
e46a32f11e |
modules: add refluxfs (CVE-2026-64600, "RefluXFS" XFS reflink CoW ILOCK race)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Adds the corpus's first XFS module and its first data-oriented kernel bug —
every other kernel entry corrupts memory; this one corrupts file contents.
xfs_direct_write_iomap_begin() reads the data-fork extent map under ILOCK,
then xfs_reflink_fill_cow_hole() drops ILOCK to wait for transaction log
space. On reacquiring it, the code re-queries the refcount btree at the
ORIGINAL imap->br_startblock and never re-reads the data fork. A second
O_DIRECT writer holding only IOLOCK completes a whole CoW cycle in that
window, so the first writer's stale mapping sees refcount 1, treats a
still-shared block as private, and writes to it in place — landing its data
on the reflink source file's on-disk blocks.
The primitive is an arbitrary overwrite of the on-disk contents of any
readable file, which has three consequences that drive the design:
- No offsets, no ROP, no KASLR/SMEP/SMAP; SELinux, containers and seccomp
are all irrelevant.
- The victim's inode is never written, so mtime/ctime/size never change
and nothing is logged — FIM and `-w /etc/passwd -p wa` cannot see it.
- The change persists across reboots.
Introduced 4.11 (3c68d44a2b49); fixed 2f4acd0fcd86 (mainline 7.2-rc4,
merged 2026-07-16), stable backports 7.1.4 / 6.18.39 / 6.12.96. Exposure is
distro-shaped: RHEL/CentOS/Rocky/Alma/Oracle/CloudLinux 8-10, Fedora Server
>= 31 and Amazon Linux 2023 ship XFS+reflink by default.
detect() is not a pure version gate — reachability here is safely
observable, so it pairs the backport table with a real storage precondition
(writable XFS via statfs XFS_SUPER_MAGIC, deliberately not via a successful
FICLONE since btrfs implements that too and is unaffected). --active
confirms reflink via FICLONE; SKELETONKEY_XFS_ASSUME_REFLINK=1/0 overrides.
On rpm-family hosts it warns that vendors backport without bumping the
upstream version, so the verdict speaks only to the upstream base.
exploit() forks a child that works only in a private mkdtemp scratch dir on
two files it owns: it establishes a shared extent (FICLONE, corroborated by
FIEMAP_EXTENT_SHARED) plus an O_DIRECT gate, then races a hard-bounded
8 writers / 2 helpers / 16 rounds / 2s and stops, reading the donor back
with O_DIRECT. Deliberately under-driven, and it never clones or targets a
file it does not own — the /etc/passwd overwrite -> su -> root step is
documented but NOT bundled. Always returns EXPLOIT_FAIL.
Safety rank 55, far above bad_epoll (12) and ghostlock (11): a won race
corrupts 4 KiB of our own scratch file and cannot touch kernel memory, so
there is no oops/KASAN/panic path.
Detection inverts the usual advice. auditd/sigma anchor on ioctl request
0x40049409 (FICLONE, matched exactly) and openat O_DIRECT; falco adds the
cross-uid reflink condition; and the yara rule is genuinely the right tool
here, matching the on-disk artifact because FIM is structurally blind.
VM-VERIFIED 2026-07-23 — the corpus's first rpm-family verification, taking
the empirical count to 29 of 41 CVEs. Rocky Linux 9.8 /
5.14.0-687.10.1.el9_8.0.1.x86_64 under qemu/KVM, stock GenericCloud layout
with no provisioner changes (root is XFS with reflink=1 out of the box).
detect() -> VULNERABLE, --active FICLONE witness confirmed reflink, phase A
observed FIEMAP_EXTENT_SHARED on a real shared extent, scratch self-cleaned,
clean build on el9 gcc. The underlying bug was separately confirmed winnable
on that kernel via tools/verify-vm/refluxfs_verify.c at the public PoC's
parameters (32 writers / 8 helpers, 60s): 4/4 runs won, first divergence
after 69/114/170/494 rounds. The shipped under-driven trigger did NOT win in
its 2s budget on that same vulnerable kernel — intended behaviour, and
exactly why a non-win must never be read as "patched".
14 new detect() unit rows (148 tests total, 0 failures). Bumps to v0.9.14.
Credit: Qualys Threat Research Unit (blog by Saeed Abbasi; the technical
advisory credits model-assisted kernel analysis performed with Anthropic),
and the upstream XFS maintainers who fixed it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
|
||
|
|
03324c8542 |
modules: add ghostlock (CVE-2026-43499, "GhostLock" rtmutex/futex requeue-PI stack UAF)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
Adds the ghostlock module for CVE-2026-43499 ("GhostLock", VEGA / Nebula
Security's "IonStack part II") — a ~15-year race UAF on kernel STACK memory in
the rtmutex/futex requeue-PI path (kernel/locking/rtmutex.c). On the -EDEADLK
deadlock-rollback, remove_waiter() runs against `current` instead of the waiter
task, so a concurrent sched_setattr()-driven PI-chain priority walk on another
CPU clears pi_blocked_on on the wrong task and leaves an on-stack rt_mutex_waiter
dangling. Reachable by any unprivileged user (CVSS 7.8, PR:L) — plain
futex(2) + sched_setattr(2), no userns/CONFIG beyond CONFIG_FUTEX_PI. The
corpus's first rtmutex/futex-PI module and first kernel-stack UAF (all others
are heap/slab).
Introduced 2.6.39; fixed 3bfdc63936dd (7.1-rc1), stable backports
7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175; 5.15/5.10/5.4/4.19 affected with
no upstream fix. CWE-416; not in KEV.
detect() is a pure version gate over the five-branch backport table. exploit()
forks an isolated child that (A) deterministically confirms the -EDEADLK
remove_waiter() rollback path is reachable (safe — no concurrent walk means no
dangling pointer; validated on real hardware) then (B) exercises the actual race
a hard-bounded 24 iters / 2s with a sibling-CPU sched_setattr(SCHED_BATCH) storm,
and stops. Deliberately under-driven: no copy_from_user widening, no stack-frame
spray/reoccupation, and the KernelSnitch leak -> forged-waiter ->
fops/ashmem/pipe R/W -> cred-patch chain (Android/Pixel-specific, per-build
offsets) is NOT bundled. Returns EXPLOIT_FAIL. Lowest --auto safety rank (11) —
a won race corrupts the kernel stack. Unlike most races it ships a real
detection signature (futex requeue-PI returning EDEADLK + sibling sched_setattr);
auditd/sigma anchor on sched_setattr, falco/eBPF on the EDEADLK tell; no yara.
Wired: registry, Makefile, safety rank 11, version 0.9.13, 9 detect() test rows
(incl. 6.13.0 -> VULNERABLE, the multi-branch "newer than all" case), CVE
metadata (CWE-416 / T1068 / not-KEV; cve_metadata.c + KEV_CROSSREF.md regenerated
-> 13 of 40), README + CVES.md + website counts (45 modules / 40 CVEs),
RELEASE_NOTES v0.9.13, verify-vm target. Also corrects pre-existing
docs/index.html drift left by v0.9.12 (body counts stuck at 43/38 and a missing
bad_epoll corpus pill). Tests: 33 kernel_range + 101 detect, all pass.
Credit: VEGA / Nebula Security (nebusec.ai, NebuSec/CyberMeowfia, Apache-2.0);
upstream fix 3bfdc63936dd (Keenan Dong / Thomas Gleixner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUq4DGXSPBmPkAyJ9WnM9n
|
||
|
|
95589e26cb |
modules: add bad_epoll (CVE-2026-46242, "Bad Epoll" epoll teardown race UAF)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
Jaeyoung Chung's kernelCTF "Bad Epoll": a race-condition use-after-free in fs/eventpoll.c. ep_remove() clears file->f_ep under f_lock but keeps using the file (hlist_del_rcu + unlock) while a concurrent __fput() frees the still-referenced struct eventpoll. Reachable by any unprivileged user with no userns / CONFIG / capability; weaponised via cross-cache to a struct file, /proc/self/fdinfo arb-read, and ROP. Introduced 58c9b016e128 (6.4), fixed a6dc643c6931 (7.1-rc1), stable backport 7.0.13. CWE-416; not in KEV. The corpus's first epoll / VFS-teardown module. Shipped as a reconstructed, deliberately under-driven trigger on the stackrot / nft_catchall contract: detect() is a pure version gate (>= 6.4 and below the on-branch fix; no userns precondition -- epoll needs none); exploit() forks a CPU-pinned child that exercises the ep_remove-vs-__fput close window a hard-bounded 48 attempts / 2s and returns EXPLOIT_FAIL. It does not grind the race to a win, does not do the cross-cache reclaim, and does not bundle the fdinfo R/W + ROP (a won race frees a live struct file and rarely trips KASAN -> silent-corruption risk). Wired: registry, Makefile, safety rank (12 -- lowest in the corpus; a kernel race is the least predictable class), 5 detect() test rows (version gating), CVE metadata (sorted insert, CWE-416 / T1068 / not-KEV), README + CVES.md + website counts (44/39), RELEASE_NOTES v0.9.12, and a verify-vm target (sweep pending). Detection rules are intentionally weak/structural (epoll ubiquitous, rarely KASAN) -- post-exploitation euid-0 transition, no yara. Also corrects pre-existing README drift in the "not yet verified" count (8 -> 11). Not VM-verified, so the verified count stays 28 of 39. Version 0.9.12. Credit: Jaeyoung Chung (J-jaeyoung). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUq4DGXSPBmPkAyJ9WnM9n |
||
|
|
4d0a0e2443 |
modules: add nft_catchall (CVE-2026-23111, nf_tables catch-all abort UAF)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
The newest nftables LPE: a use-after-free in the nf_tables transaction-
abort path. An inverted condition (a stray '!') in
nft_map_catchall_activate() makes the abort path process active catch-all
map elements instead of skipping them; a catch-all GOTO element drives a
chain's use-count to zero so a following DELCHAIN frees it while the
catch-all verdict still references it -> UAF, escalatable from an
unprivileged user (userns + nftables) via modprobe_path/selinux_state ROP.
Fixed upstream by f41c5d1; CWE-416, CVSS 7.8; not in CISA KEV. Public
reproduction by FuzzingLabs.
Takes the corpus to 43 modules / 38 CVEs.
🟡 reconstructed trigger, primitive-only, NOT VM-verified — same contract
as nf_tables (CVE-2024-1086). detect() version-gates (catch-all elems
~5.13; Debian backports 6.1.164/6.12.73/6.18.10, 7.0+ inherits) AND
requires unprivileged user_ns clone (else PRECOND_FAIL). exploit() forks
an isolated child, builds a verdict map with a catch-all GOTO element,
provokes an aborting batch to drive the abort-path UAF, observes slabinfo,
and returns EXPLOIT_FAIL — the per-kernel leak + R/W + modprobe_path ROP
is not bundled. kernel_range table verified drift-clean against the live
Debian tracker (the 6.18 branch / 6.18.10 fix is the real forky/sid
backport; the earlier 7.0.10 figure was wrong).
Wired: registry, Makefile, safety rank (35), 6 detect() test rows (version
+ userns gating), CVE_METADATA.json + cve_metadata.c + KEV_CROSSREF.md
(sorted insert, CWE-416/T1068/not-KEV), README + CVES.md + website counts
(43/38) + yellow pill, RELEASE_NOTES v0.9.11, verify-vm target. Credit:
FuzzingLabs + upstream fix f41c5d1. Version 0.9.11.
|
||
|
|
050731396d |
docs: record partial VM verification of cifswitch (CVE-2026-46243)
Verified 2026-06-08 on Ubuntu 24.04.4 / kernel 6.8.0-117-generic under
QEMU/HVF (offline: cloud image + payload iso, no guest networking):
- modprobe cifs registers the cifs.spnego key type (cifs-utils not needed
to reach the primitive).
- Independent python3 ctypes add_key('cifs.spnego', forged
uid/creduid/upcall_target) ACCEPTED (user-key control also accepted);
module exploit() independently reported 'primitive CONFIRMED' then the
honest EXPLOIT_FAIL.
- detect() returned PRECOND_FAIL without cifs-utils and VULNERABLE under
SKELETONKEY_CIFS_ASSUME_PRESENT=1.
Still pending (so cifswitch stays 🟡 and is NOT counted as a verified
end-to-end CVE; verified count stays 28 of 37): a patched kernel
(>=6.12.90/7.0.10) to prove add_key is REJECTED there (probe discriminates
fixed-from-vulnerable), and the full namespace+NSS root-pop. Recorded in
NOTICE.md, CVES.md, RELEASE_NOTES.md, and tools/verify-vm/targets.yaml.
|
||
|
|
ada56b0db3 |
modules: add cifswitch (CVE-2026-46243, Asim Manizada's CIFSwitch)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
CIFSwitch is the newest kernel-7-era LPE not already in the corpus: a
~19-year-old logic flaw in fs/smb/client/cifs_spnego.c where the
cifs.spnego request-key type accepts key descriptions created by
userspace (add_key(2)/request_key(2)) without verifying the request came
from the in-kernel CIFS client. The description's authority-bearing
fields (pid/uid/creduid/upcall_target) are trusted by the root cifs.upcall
helper; with user+mount namespace tricks an unprivileged user coerces
cifs.upcall into loading an attacker NSS module as root. Fixed upstream by
3da1fdf4efbc (merged 7.1-rc5); CWE-20; not in CISA KEV.
Takes the corpus to 42 modules / 37 CVEs.
🟡 honest port — full chain not VM-verified. detect() gates on the kernel
version (Debian backports 5.10.257/6.1.174/6.12.90/7.0.10) AND on the
cifs userspace path (cifs.upcall / cifs.spnego request-key rule), so a
vulnerable kernel without cifs-utils is PRECOND_FAIL not a false positive
(override via SKELETONKEY_CIFS_ASSUME_PRESENT=1/0). exploit() fires only
the non-destructive add_key(2) cifs.spnego probe (no upcall, loads
nothing, revoked immediately) and returns EXPLOIT_FAIL without a euid-0
witness — the namespace+NSS root-pop is not bundled until VM-verified.
--mitigate blocklists the cifs module; --cleanup reverts.
Wired everywhere: registry, Makefile, safety rank (86), 6 detect() test
rows (env-driven precondition override), CVE_METADATA.json + cve_metadata.c
+ KEV_CROSSREF.md (sorted insert, CWE-20/T1068/not-KEV), README + CVES.md
+ website counts (42/37) and a yellow module pill, RELEASE_NOTES v0.9.10,
verify-vm target (sweep pending). Credit: Asim Manizada. Version 0.9.10.
|
||
|
|
dd5f4fa06d |
modules: add sudo_host (CVE-2025-32462, Stratascale sudo --host policy bypass)
Second new module this cycle; sibling of sudo_chwoot (CVE-2025-32463, same Stratascale/Rich Mirch disclosure). sudo's -h/--host option — meant only to pair with -l/--list — was honored when running a command, so a sudoers rule scoped to a host other than the current machine (and not ALL) is usable via 'sudo -h <host> <cmd>' for local root. Affects sudo 1.8.8 -> 1.9.17p0; fixed 1.9.17p1. CWE-863, CVSS 8.8 (not in KEV). detect(): version-gate [1.8.8, 1.9.17p0] via ctx->host->sudo_version (the host-restricted rule itself isn't probeable unprivileged, so VULNERABLE means 'vulnerable sudo present'). exploit(): discovers an abusable host-restricted rule from readable sudoers (or SKELETONKEY_SUDO_HOST), witnesses with 'sudo -n -h <host> id -u', pops 'sudo -h <host> /bin/bash' (SKELETONKEY_SUDO_CMD) only on a uid-0 witness; honest EXPLOIT_FAIL + operator guidance otherwise. Shared 'sudo' family; structural, arch=any; safety rank 96. auditd/sigma/falco rules, NOTICE.md (Rich Mirch / Stratascale) + MODULE.md, 4 detect() test rows. Wiring: registry, Makefile, cve_metadata (+JSON), verify-vm/targets.yaml (ubuntu1804 sudo 1.8.21p2 target, sweep pending). Docs: README + CVES.md + docs/index.html counts 40->41 modules / 35->36 CVEs; not-yet-verified lists + corpus pill. |
||
|
|
bd63aabd64 |
modules: add ptrace_pidfd (CVE-2026-46333, Qualys ptrace/pidfd_getfd cred-steal)
New module for Qualys's 2026-05-20 disclosure: a __ptrace_may_access logic flaw leaves a process dropping privileges briefly reachable past its dumpable boundary; pidfd_getfd(2) steals root-opened fds / authenticated channels from it. Default-distro, no userns, arch-agnostic (fd-steal, no shellcode). detect(): version-pinned, predates-gate at pidfd_getfd's 5.6 introduction; kernel_range from Debian backports (5.10.251/6.1.172/6.12.88/7.0.7), drift-check clean. exploit(): spawns a setuid victim, pidfd_open()s it, sweeps pidfd_getfd() over its fd table during the cred-drop window, reports any uid-0-owned fd captured from a non-root context. Honest EXPLOIT_FAIL without a euid-0 witness; not yet VM-verified. mitigate(): yama ptrace_scope=2; cleanup() reverts. auditd/sigma/falco rules, NOTICE.md (Qualys TRU credit) + MODULE.md, safety rank 84. Wiring: registry, Makefile, cve_metadata (+JSON source), verify-vm/targets.yaml (ubuntu2204 + mainline 5.15.5 target, sweep pending). Docs: README + CVES.md + docs/index.html counts 39->40 modules / 34->35 CVEs; added to not-yet-verified lists + corpus pill. |
||
|
|
35c33df16f |
fragnesia: add 5.10.257 kernel_range entry (Debian bullseye backport)
Weekly drift-check (build.yml schedule cron) went red 2026-06-01: Debian's security tracker now lists CVE-2026-46300 as fixed on the 5.10 branch (bullseye 5.10.257), a branch fragnesia's kernel_patched_from table didn't model. detect() would false-positive VULNERABLE on a patched bullseye 5.10.257+ host.
Adding {5,10,257} clears the only MISSING finding; refresh-kernel-ranges.py now exits 0. The 10 remaining drifted modules are INFO-only 'more permissive' entries the check tolerates.
|
||
|
|
25c2afc3e9 |
release v0.9.6: --auto no longer prompts for sudo password
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
sudo_runas_neg1 and sudoedit_editor's detect() bodies invoked 'sudo -ln' (intending list + non-interactive). Some sudoers / PAM configurations have been observed prompting for a password anyway when the flags are bundled. That meant skeletonkey --auto --i-know could hang on a sudo password prompt during the corpus scan — bad ergonomics for an LPE tool whose whole point is to get root without already having it. Fix: write '-n -l' as separate flags, redirect stdin from /dev/null so sudo cannot fall back to reading the tty even if PAM tries to coerce one. Belt-and-suspenders against any tty prompt during --auto. |
||
|
|
13fbbce618 |
release v0.9.5: kernel_range drift cleanup (12 modules)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
v0.9.4 fixed cve_metadata drift but exposed kernel_range drift which
had been hidden behind it. Applied refresh-kernel-ranges.py --patch
recommendations: 11 TOO_TIGHT findings (false-positive risk — our
threshold later than Debian's earliest known fix) + 8 MISSING (Debian
has fixes for branches we didn't model) across 12 modules.
All changes are strictly correctness-improving: detect() now correctly
returns OK on kernels Debian has on record as patched, instead of
false-positiving VULNERABLE.
The biggest single fix is reverting fragnesia from {7,0,10} (NVD) to
{7,0,9} (Debian's backported fix). I introduced that off-by-one in
v0.9.4 from misreading NVD's versionEndExcluding semantics.
Build's kernel_range drift step now exits 0 with 0 TOO_TIGHT + 0 MISSING.
|
||
|
|
4454d8148e |
release v0.9.4: drift unblock, fragnesia range fix, infra docs
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
- Sync docs/CVE_METADATA.json + KEV_CROSSREF.md to match the
hand-applied core/cve_metadata.c entries from v0.9.3. Nightly
drift-check (red since 2026-05-25) now passes. Pintheft's CWE
landed as CWE-787 from NVD (was NULL in the hand-applied entry).
- Fix fragnesia (CVE-2026-46300) range table. Per NVD: bug entered
at 5.11 SKBFL_SHARED_FRAG, vulnerable through 5.15.207 / 6.1.173 /
6.6.140 / 6.12.90 / 6.18.32 / 7.0.9, fixed at .208/.174/.141/
.91/.33/.10. Prior table had one entry {7,0,9} — off-by-one and
missing every other backport. Added predates-5.11 introduction gate
+ test row.
- Update tools/verify-vm/README.md to document the v0.9.x infra:
mainline kernel pinning via kernel.ubuntu.com, per-module
provisioner hooks, two-phase prep→reboot→verify with post-reboot
kernel confirmation, GRUB_DEFAULT pinning.
- Add curl fallback for NVD lookups in refresh-cve-metadata.py.
Mirrors the CISA path's existing fallback. Prevents the silent
Python urlopen hang seen during v0.9.3 prep (55-min stuck on
CLOSE_WAIT socket; 30s timeout never fired).
|
||
|
|
fa0228df9b |
release v0.9.3: CVE metadata refresh (KEV 10→12) + dirtydecrypt bug fix
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
CVE metadata refresh:
- Added 8 entries to core/cve_metadata.c for the v0.8.0 + v0.9.0 module
CVEs. Two are CISA-KEV-listed:
- CVE-2018-14634 mutagen_astronomy (2026-01-26, CWE-190)
- CVE-2025-32463 sudo_chwoot (2025-09-29, CWE-829)
- Populated via direct curl when refresh-cve-metadata.py's Python urlopen
hung on CISA's HTTP/2 endpoint for ~55 min — same data, different
transport.
dirtydecrypt module bug fix:
- dd_detect() was wrongly gating 'predates the bug' on kernel < 7.0
- Per NVD CVE-2026-31635: bug entered at 6.16.1 stable; vulnerable
through 6.18.22 / 6.19.12 / 7.0-rc7; fixed at 6.18.23 / 6.19.13 / 7.0
- Fix: predates-gate now uses 6.16.1; patched_branches[] adds {6,18,23}
- Re-verified: dirtydecrypt now correctly returns VULNERABLE on mainline
6.19.7 instead of OK. Previously a false negative on real vulnerable
kernels.
Footer goes from '10 in CISA KEV' to '12 in CISA KEV'. Verified count
stays at 28 but dirtydecrypt's record is now a TRUE VULNERABLE match
(was OK match).
|
||
|
|
7f4a6e1c7c |
pintheft: drop --full-chain stub (calls undefined finisher symbol)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
The x86_64 path called finisher_modprobe_path_overwrite() which doesn't exist — the real API is skeletonkey_finisher_modprobe_path() with a callback signature. arm64 builds dodged it via the #if guard; x86_64 linker rightly choked. Same fix as tioscpgrp/vsock_uaf/nft_pipapo: primitive-only modules return EXPLOIT_FAIL honestly per verified-vs- claimed. |
||
|
|
f41eed834e |
pintheft: add missing <sys/mman.h> for mmap/mprotect/PROT_*
v0.9.0 release builds all 4 failed because pintheft module used mmap/ mprotect/PROT_READ/MAP_PRIVATE without including sys/mman.h. Worked on the dev host because some indirect include pulled it in; CI's stricter glibc/musl headers don't. |
||
|
|
d84b3b0033 |
release v0.9.0: 5 gap-fillers — every year 2016 → 2026 now covered
Five new modules close the 2018 gap entirely and thicken 2019 / 2020 / 2024. All five carry the full 4-format detection-rule corpus + opsec_notes + arch_support + register helpers. CVE-2018-14634 — mutagen_astronomy (Qualys, closes 2018) create_elf_tables() int wrap → SUID-execve stack corruption. CISA KEV-listed Jan 2026 despite the bug's age; legacy RHEL 7 / CentOS 7 / Debian 8 fleets still affected. 🟡 PRIMITIVE. arch_support: x86_64+unverified-arm64. CVE-2019-14287 — sudo_runas_neg1 (Joe Vennix) sudo -u#-1 → uid_t underflow → root despite (ALL,!root) blacklist. Pure userspace logic bug; the famous Apple Information Security finding. detect() looks for a (ALL,!root) grant in sudo -ln output; PRECOND_FAIL when no such grant exists for the invoking user. arch_support: any (4 -> 5 userspace 'any' modules). CVE-2020-29661 — tioscpgrp (Jann Horn / Project Zero) TTY TIOCSPGRP ioctl race on PTY pairs → struct pid UAF in kmalloc-256. Affects everything through Linux 5.9.13. 🟡 PRIMITIVE (race-driver + msg_msg groom). Public PoCs from grsecurity / spender + Maxime Peterlin. CVE-2024-50264 — vsock_uaf (a13xp0p0v / Pwnie Award 2025 winner) AF_VSOCK connect-race UAF in kmalloc-96. Pwn2Own 2024 + Pwnie 2025 winner. Reachable as plain unprivileged user (no userns required — unusual). Two public exploit paths: @v4bel+@qwerty kernelCTF (BPF JIT spray + SLUBStick) and Alexander Popov / PT SWARM (msg_msg). 🟡 PRIMITIVE. CVE-2024-26581 — nft_pipapo (Notselwyn II, 'Flipping Pages') nft_set_pipapo destroy-race UAF. Sibling to nf_tables (CVE-2024-1086) from the same Notselwyn paper. Distinct bug in the pipapo set substrate. Same family signature. 🟡 PRIMITIVE. Plumbing changes: core/registry.h + registry_all.c — 5 new register declarations + calls. Makefile — 5 new MUT/SRN/TIO/VSK/PIP module groups in MODULE_OBJS. tests/test_detect.c — 7 new test rows covering the new modules (above-fix OK, predates-the-bug OK, sudo-no-grant PRECOND_FAIL). tools/verify-vm/targets.yaml — verifier entries for all 5 with honest 'expect_detect' values based on what Vagrant boxes can realistically reach (mutagen_astronomy gets OK on stock 18.04 since 4.15.0-213 is post-fix; sudo_runas_neg1 gets PRECOND_FAIL because no (ALL,!root) grant on default vagrant user; tioscpgrp + nft_pipapo VULNERABLE with kernel pins; vsock_uaf flagged manual because vsock module rarely available on CI runners). tools/refresh-cve-metadata.py — added curl fallback for the CISA KEV CSV fetch (urlopen times out intermittently against CISA's HTTP/2 endpoint). Corpus growth across v0.8.0 + v0.9.0: v0.7.1 v0.8.0 v0.9.0 Modules 31 34 39 Distinct CVEs 26 29 34 KEV-listed 10 10 11 (mutagen_astronomy) arch 'any' 4 6 7 (sudo_runas_neg1) Years 2016-2026: 10/11 10/11 **11/11** Year-by-year coverage: 2016: 1 2017: 1 2018: 1 2019: 2 2020: 2 2021: 5 2022: 5 2023: 8 2024: 3 2025: 2 2026: 4 CVE-2018 gap → CLOSED. Every year from 2016 through 2026 now has at least one module. Surfaces updated: - README.md: badge → 22 VM-verified / 34, Status section refreshed - docs/index.html: hero eyebrow + footer → v0.9.0, hero tagline 'every year 2016 → 2026', stats chips → 39 / 22 / 11 / 151 - docs/RELEASE_NOTES.md: v0.9.0 entry added on top with year coverage matrix + per-module breakdown; v0.8.0 + v0.7.1 entries preserved below - docs/og.svg + og.png: regenerated with new numbers + 'Every year 2016 → 2026' tagline CVE metadata refresh (tools/refresh-cve-metadata.py) deferred to follow-up — CISA KEV CSV + NVD CVE API were timing out during the v0.9.0 push window. The 5 new CVEs will return NULL from cve_metadata_lookup() until the refresh runs (—module-info simply skips the WEAKNESS/THREAT INTEL header for them; no functional impact). Re-run 'tools/refresh-cve-metadata.py' when network cooperates. Tests: macOS local 33/33 kernel_range pass; detect-test stubs (88 total) build clean; ASan/UBSan + clang-tidy CI jobs still green from the v0.7.x setup. |
||
|
|
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.
|
||
|
|
f792a3c4a6 |
verify-vm: close the loop — first successful end-to-end VM verification
Five fixes that landed us at a working 'verify.sh <module> -> JSON
verification record' loop. Tested with pwnkit on
generic/ubuntu2004 / Ubuntu 20.04.6 LTS / 5.4.0-169-generic.
1. core/nft_compat.h — shim header that conditionally defines newer-
kernel nft uapi constants that aren't in older distro headers:
NFT_CHAIN_HW_OFFLOAD kernel 5.5
NFT_CHAIN_BINDING kernel 5.9
NFTA_VERDICT_CHAIN_ID kernel 5.14
NFTA_SET_DESC_CONCAT kernel 5.6
NFTA_SET_EXPR kernel 5.12
NFTA_SET_EXPRESSIONS kernel 5.16
NFTA_SET_ELEM_KEY_END kernel 5.6
NFTA_SET_ELEM_EXPRESSIONS kernel 5.16
Numeric values are stable kernel ABI; the target vulnerable kernel
understands them at runtime regardless of the build host's headers.
Without this, nf_tables / nft_fwd_dup / nft_payload / nft_set_uaf
modules fail to compile on Ubuntu 20.04's libc-dev (5.4 uapi).
2. modules/{nf_tables, nft_fwd_dup, nft_payload, nft_set_uaf}/
skeletonkey_modules.c — each #includes the new compat shim after
<linux/netfilter/nf_tables.h>.
3. tools/verify-vm/Vagrantfile — wrap config in 'c.vm.define host do
|m| ... end' block so 'vagrant up <skk-MODULE>' finds the machine.
(Earlier without define block, vagrant always treated the Vagrantfile
as a single anonymous machine.) Also disable Parallels Tools auto-
install — it fails on Ubuntu 20.04's 5.4 kernel ('current Linux
kernel version is outdated and not supported by latest tools'); we
use rsync sync_folder over plain SSH which doesn't need the tools.
4. tools/verify-vm/verify.sh — explicit 'vagrant rsync' before
'vagrant provision build-and-verify' so the source tree gets synced
even on already-running VMs (vagrant up runs rsync automatically;
vagrant provision does not).
5. tools/verify-vm/verify.sh — fix verdict parser. Vagrant prefixes
provisioner stdout with the VM name (' skk-pwnkit: VERDICT:
VULNERABLE'), so the previous '^VERDICT: ' regex never matched.
New grep allows the prefix; added '|| true' so a grep miss doesn't
trigger set-e+pipefail and silently exit the script before the JSON
verification record gets emitted.
First successful verification record:
{
"module": "pwnkit",
"verified_at": "2026-05-23T19:26:02Z",
"host_kernel": "5.4.0-169-generic",
"host_distro": "Ubuntu 20.04.6 LTS",
"vm_box": "generic/ubuntu2004",
"expect_detect": "VULNERABLE",
"actual_detect": "VULNERABLE",
"status": "match"
}
SKELETONKEY correctly identifies polkit 0.105 on Ubuntu 20.04 as
vulnerable to CVE-2021-4034. The verifier pipeline is now ready for
sweep across the rest of the corpus.
|
||
|
|
8ab49f36f6 |
detection rules: complete sigma/yara/falco coverage across the corpus
Three parallel research agents drafted 49 detection rules grounded in
each module's source + existing .opsec_notes string + existing .detect_auditd
counterpart. A one-shot tools/inject_rules.py wrote them into the
right files and replaced the .detect_<format> = NULL placeholders.
Coverage matrix (modules with each format / 31 total):
before after
auditd 30 / 31 30 / 31 (entrybleed skipped by design)
sigma 19 / 31 31 / 31 (+12 added)
yara 11 / 31 28 / 31 (+17 added; 3 documented skips)
falco 11 / 31 30 / 31 (+19 added; entrybleed skipped)
Documented skips (kept as .detect_<format> = NULL with comment):
- entrybleed: yara + falco + auditd. Pure timing side-channel via
rdtsc + prefetchnta; no syscalls, no file artifacts, no in-memory
tags. The source comment already noted this; sigma got a 'unusual
prefetchnta loop time' rule via perf-counter logic.
- ptrace_traceme: yara. Pure in-memory race; no on-disk artifacts
or persistent strings to match. Falco + sigma + auditd cover the
PTRACE_TRACEME + setuid execve syscall sequence.
- sudo_samedit: yara. Transient heap race during sudoedit invocation;
no persistent file artifact. Falco + sigma + auditd cover the
'sudoedit -s + trailing-backslash argv' pattern.
Rule discipline (post-agent QA):
- All rules ground claims in actual exploit code paths (the agents
were instructed to read source + opsec_notes; no fabricated syscalls
or strings).
- Two falco rules were narrowed by the agent to fire only when
proc.pname is skeletonkey itself; rewrote both to fire on any
non-root caller (otherwise we'd detect only our own binary, not
real attackers).
- Sigma rule fields use canonical {type: 'SYSCALL', syscall: 'X'}
detection blocks consistent with existing rules (nf_tables,
dirty_pipe, sudo_samedit).
- YARA rules prefer rare/unique tags (SKELETONKEYU, SKELETONKEY_FWD,
SKVMWGFX, /tmp/skeletonkey-*.log) over common bytes — minimizes
false positives.
- Every rule tagged with attack.privilege_escalation + cve.YYYY.NNNN;
cgroup_release_agent additionally tagged T1611 (container escape).
skeletonkey.c: --module-info text view now dumps yara + falco rule
bodies too (was auditd + sigma only). All 4 formats visible per module.
Verification:
- macOS local: clean build, 33 kernel_range tests pass.
- Linux (docker gcc:latest): 33 + 54 = 87 passes, 0 fails.
- --module-info nf_tables / af_unix_gc / etc.: 'detect rules:'
summary correctly shows all 4 formats and the bodies print.
|
||
|
|
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).
|
||
|
|
60d22eb4f6 |
core/host: add meltdown_mitigation passthrough + migrate entrybleed
The kpti_enabled bool in struct skeletonkey_host flattens three
distinct sysfs states into one bit:
/sys/devices/system/cpu/vulnerabilities/meltdown content:
- 'Not affected' → CPU is Meltdown-immune; KPTI off; EntryBleed
doesn't apply (verdict: OK)
- 'Mitigation: PTI' → KPTI on (verdict: VULNERABLE)
- 'Vulnerable' → KPTI off but CPU not hardened (rare;
verdict: VULNERABLE conservatively)
- file unreadable → unknown (verdict: VULNERABLE conservatively)
kpti_enabled=true only captures 'Mitigation: PTI'; kpti_enabled=false
collapses 'Not affected', 'Vulnerable', and 'unreadable' into one
indistinguishable case. That meant entrybleed_detect() had to
re-open the sysfs file to recover the raw string.
Fix by also stashing the raw first line in
ctx->host->meltdown_mitigation[64]. kpti_enabled stays for callers
that only need the simple bool; new code that needs the nuance reads
the string. populate happens once at startup, like every other host
field.
entrybleed migration:
- reads ctx->host->meltdown_mitigation instead of opening sysfs
- removes the file-local read_first_line() helper (now dead code)
- same three-way verdict logic, but driven by a const char *
instead of a fresh fopen() each detect()
Test coverage:
- 3 new test rows on x86_64 fingerprints:
empty mitigation → VULNERABLE (conservative)
'Not affected' → OK
'Mitigation: PTI' → VULNERABLE
- 1 stub-path test row on non-x86_64 fingerprints (PRECOND_FAIL)
- registry coverage report: 30/31 modules now have direct tests
(up from 29/31; copy_fail is the only remaining untested module)
Verification:
- macOS: 33 kernel_range + 1 entrybleed-stub = 34 passes, 0 fails
- Linux (docker gcc:latest): 33 kernel_range + 54 detect = 87
passes, 0 fails. Up from 83 last commit.
|
||
|
|
8de46e212e |
kernel_range: refresh tables from Debian tracker — 5 MISSING adds + 4 off-by-one harmonisations
First batch of fixes surfaced by tools/refresh-kernel-ranges.py.
Drift drops from 18 actionable findings (5 MISSING + 13 TOO_TIGHT)
to 13 (now only 1 MISSING + 12 TOO_TIGHT). The remaining
TOO_TIGHT findings all involve threshold-version drops of 2+
patch versions; those need per-commit verification against
git.kernel.org/linus before applying (saving for a follow-up).
MISSING adds — branches Debian has fixed that we had no entry for:
af_unix_gc (CVE-2023-4622):
+ {6, 4, 13} stable 6.4.x (forky/sid/trixie all at this version)
dirtydecrypt (CVE-2026-31635):
+ {6, 19, 13} stable 6.19.x (forky/sid) — our previous table
only listed mainline 7.0.0; Debian is shipping
the fix on the 6.19 branch ahead of 7.0 release.
overlayfs_setuid (CVE-2023-0386):
+ {5, 10, 179} stable 5.10.x (bullseye)
vmwgfx (CVE-2023-2008):
+ {5, 10, 127} stable 5.10.x (bullseye)
+ {5, 18, 14} stable 5.18.x (bookworm/forky/sid/trixie)
TOO_TIGHT harmonisations — single-patch-version differences,
almost certainly off-by-one curation errors on our side:
nf_tables (CVE-2024-1086):
{5, 10, 210} -> {5, 10, 209} (Debian bullseye)
nft_payload (CVE-2023-0179):
{5, 10, 163} -> {5, 10, 162} (Debian bullseye)
nft_set_uaf (CVE-2023-32233):
{5, 10, 180} -> {5, 10, 179} (Debian bullseye)
{6, 1, 28} -> {6, 1, 27} (Debian bookworm)
Larger TOO_TIGHT diffs deferred:
- cgroup_release_agent (5.16.9 -> 5.16.7, diff 2)
- cls_route4 (5.18.18 -> 5.18.16, diff 2; 5.10.143 -> 5.10.136, diff 7)
- dirty_cow (4.7.10 -> 4.7.8, diff 2)
- dirty_pipe (5.10.102 -> 5.10.92, diff 10)
- netfilter_xtcompat (5.10.46 -> 5.10.38, diff 8)
- overlayfs_setuid (6.1.27 -> 6.1.11, diff 16)
- ptrace_traceme (4.19.58 -> 4.19.37, diff 21)
- sequoia (5.10.52 -> 5.10.46, diff 6)
These need per-commit confirmation against the upstream-stable
kernel changelog before lowering our threshold. Conservatively
keeping the current (more strict) values until each is verified.
Verification:
- Linux (docker gcc:latest + libglib2.0-dev + sudo): 44/44 tests
pass, full build clean.
- macOS (local): 31-module build clean.
- tools/refresh-kernel-ranges.py rerun: drift reduced 18 -> 13.
|
||
|
|
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. |
||
|
|
150f16bc97 |
pwnkit + sudoedit_editor: ctx->host migration + 4 more tests (39 total)
pwnkit: migrate detect() to consult ctx->host->polkit_version with the same graceful-fallback pattern as the sudo modules. The version is populated once at startup by core/host.c (via pkexec --version); detect() skips the per-scan popen when the host fingerprint has the version. Falls back to the inline popen path when ctx->host is missing the version (degenerate test contexts). sudoedit_editor: already migrated; this commit adds direct test coverage. tests/test_detect.c expansion (35 → 39): - pwnkit: polkit_version='0.105' -> VULNERABLE (pre-0.121 fix) - pwnkit: polkit_version='0.121' -> OK (fix release) - sudoedit_editor: vuln sudo + no sudoers grant -> PRECOND_FAIL (documented behaviour: vulnerable version, but the dispatcher has no usable sudoedit grant on the host) - sudoedit_editor: fixed sudo (1.9.13p1) -> OK The sudoedit_editor 'vuln + no grant' case is the first test to exercise the second-level precondition gate AFTER the version check passes — proves the version-pinned detect logic AND the sudo -ln target-discovery short-circuit both work as intended. The h_vuln_sudo / h_fixed_sudo synthetic fingerprints gained the .polkit_version field alongside .sudo_version so a single fingerprint exercises both pwnkit and the sudo modules. Verification: 39/39 pass on Linux (docker gcc:latest + libglib2.0-dev + sudo, non-root user skeletonkeyci). macOS dev box still reports 'skipped — Linux-only' as designed. |
||
|
|
86812b043d |
core/host: userspace version fingerprint (sudo, polkit)
The host fingerprint now captures sudo + polkit versions at startup
so userspace-LPE modules can consult a single source of truth
instead of each popen-ing the relevant binary themselves on every
scan. Pack2theroot already queries PackageKit version via D-Bus
in-module, so PackageKit stays there for now.
core/host.h:
- new fields: char sudo_version[64], char polkit_version[64].
Empty string when the tool isn't installed or version parse fails;
modules should treat that as PRECOND_FAIL.
- documented next to has_systemd / has_dbus_system in the struct.
core/host.c:
- new populate_userspace_versions(h) called from
skeletonkey_host_get() after the other populators.
- capture_first_line() helper runs a command via popen, grabs first
stdout line, strips newline. Best-effort: failure leaves dst empty.
- extract_version_after_prefix() pulls the version token after a
fixed prefix string ('Sudo version', 'pkexec version'), handling
the colon/space variants.
- skeletonkey_host_print_banner() gained a third line when either
version is non-empty:
[*] userspace: sudo=1.9.17p2 polkit=-
Module migration (graceful fallback pattern — modules still work
without ctx->host populated):
- sudo_samedit detect: if ctx->host->sudo_version is set, skip the
popen and synthesize a 'Sudo version <X>' line for the existing
parser. Falls back to the original find_sudo + popen path if the
host fingerprint didn't capture a version.
- sudoedit_editor detect: same pattern — host fingerprint sudo_version
takes precedence over the local get_sudo_version popen.
tests/test_detect.c additions (2 new cases, 33 → 35):
- h_vuln_sudo fingerprint (sudo_version='1.8.31', kernel 5.15) —
asserts sudo_samedit reports VULNERABLE via the host-provided
version string.
- h_fixed_sudo fingerprint (sudo_version='1.9.13p1', kernel 6.12) —
asserts sudo_samedit reports OK on a patched sudo.
This is the first test pair to cover the *vulnerable* path of a
module rather than just precondition gates — proves the
version-parsing logic itself, not only the short-circuits.
Verification: 35/35 pass on Linux. macOS banner shows
'userspace: sudo=1.9.17p2 polkit=-' as the dev box has Homebrew
sudo but no polkit.
|
||
|
|
0d87cbc71c |
copy_fail_family: bridge-level userns gate + 4 new tests (33 total)
The 4 dirty_frag siblings + the GCM variant all gate on unprivileged user-namespace creation (the XFRM-ESP / AF_RXRPC paths are unreachable without it). The inner DIRTYFAIL detect functions already check this, but the check happened deep inside the legacy code — invisible to the test harness, and the bridge wrappers would delegate first and only short-circuit afterwards. Move the check up to the bridge: a single cff_check_userns() helper inspects ctx->host->unprivileged_userns_allowed and returns PRECOND_FAIL (with a host-fingerprint-annotated message) BEFORE calling the inner detect. The inner check stays in place as belt- and-suspenders. copy_fail itself uses AF_ALG (no userns needed) and bypasses the gate — its inner detect still confirms the primitive empirically via the active probe. modules/copy_fail_family/skeletonkey_modules.c: - #include "../../core/host.h" alongside the existing includes. - new static cff_check_userns(modname, ctx) helper. - copy_fail_gcm_detect_wrap, dirty_frag_esp_detect_wrap, dirty_frag_esp6_detect_wrap, dirty_frag_rxrpc_detect_wrap all call cff_check_userns before delegating. - copy_fail_detect_wrap is intentionally untouched. tests/test_detect.c: 4 new EXPECT_DETECT cases assert that all 4 gated bridge wrappers return PRECOND_FAIL when unprivileged_userns_allowed=false, using the existing h_kernel_5_14_no_userns fingerprint. 29 → 33 tests, all pass on Linux. |
||
|
|
2b1e96336e |
core/host: in_range helper + 13-module migration + 12 more tests (29 total)
Three coordinated changes that build on the host_kernel_at_least
landed in
|
||
|
|
1571b88725 |
core/host: skeletonkey_host_kernel_at_least + 9 new detect() tests
core/host helper:
- Adds bool skeletonkey_host_kernel_at_least(h, M, m, p) — the
canonical 'kernel >= X.Y.Z' check. Replaces the manual
'v->major < X || (v->major == X && v->minor < Y)' pattern that
many modules use for their 'predates the bug' pre-check. Returns
false when h is NULL or h->kernel.major == 0 (degenerate cases),
true otherwise iff the host kernel sorts at or above the supplied
version.
- dirtydecrypt migrated as the demo: the 'kernel < 7.0 → predates'
pre-check now reads 'if (!host_kernel_at_least(ctx->host, 7, 0, 0))'.
Other modules still using the manual pattern continue to work
unchanged; migrating them is incremental polish.
tests/test_detect.c expansion (8 → 17 cases):
New fingerprints:
- h_kernel_4_4 — ancient (Linux 4.4 LTS); used for 'predates the
bug' on dirty_pipe.
- h_kernel_6_12 — recent (Linux 6.12 LTS); above every backport
threshold in the corpus — modules report OK via
the 'patched by mainline inheritance' branch of
kernel_range_is_patched.
- h_kernel_5_14_no_userns — vulnerable-era kernel (5.14.0, past
every relevant predates check while below every
backport entry) with unprivileged_userns_allowed
deliberately false; lets the userns gate fire
after the version check confirms vulnerable.
New tests (9):
- dirty_pipe + kernel 4.4 → OK (predates 5.8 introduction)
- dirty_pipe + kernel 6.12 → OK (above every backport)
- dirty_cow + kernel 6.12 → OK (above 4.9 fix)
- ptrace_traceme + kernel 6.12 → OK (above 5.1.17 fix)
- cgroup_release_agent + kernel 6.12 → OK (above 5.17 fix)
- nf_tables + vuln kernel + userns=false → PRECOND_FAIL
- fuse_legacy + vuln kernel + userns=false → PRECOND_FAIL
- cls_route4 + vuln kernel + userns=false → PRECOND_FAIL
- overlayfs_setuid + vuln kernel + userns=false → PRECOND_FAIL
Process note: initial 8th and 9th userns tests failed because the
chosen test kernel (5.10.0) tripped each module's predates check
(nf_tables bug introduced 5.14; overlayfs_setuid 5.11). Switched to
5.14.0, which is past every predates threshold AND below every
backport entry in this batch — the version verdict is now genuinely
'vulnerable' and the userns gate fires next. The bug-finding tests
caught a real-but-narrow modeling gap in the original picks.
Verification:
- Linux (docker gcc:latest, non-root user): 17/17 pass.
- macOS (local): builds clean, suite reports 'skipped — Linux-only'
as designed.
|
||
|
|
36814f272d |
modules: migrate remaining 22 modules to ctx->host fingerprint
Completes the host-fingerprint refactor that started in
|
||
|
|
ea1744e6f0 |
tests: detect() unit harness with mocked ctx->host
Adds tests/test_detect.c — a standalone harness that constructs synthetic struct skeletonkey_host fingerprints (vulnerable / patched / specific-gate-closed) and asserts each migrated module's detect() returns the expected verdict. First real test coverage for the corpus; catches regressions in the host-fingerprint-consuming logic. Initial coverage — 8 deterministic cases across the 4 modules that already consume ctx->host: - dirtydecrypt: 3 cases verifying 'kernel < 7.0 -> predates the bug' short-circuit on synthetic 6.12 / 6.14 / 6.8 hosts. - fragnesia: unprivileged_userns_allowed=false -> PRECOND_FAIL. - pack2theroot: is_debian_family=false -> PRECOND_FAIL. - pack2theroot: has_dbus_system=false -> PRECOND_FAIL. - overlayfs: distro=debian / distro=fedora -> 'not Ubuntu' -> OK. Coverage grows automatically as more modules migrate to ctx->host (task #12 below adds them). Each new module that consults the host fingerprint can have its precondition gates tested with a one-line EXPECT_DETECT call against a pre-built fingerprint. Wiring: - Makefile: new MODULE_OBJS var consolidates the module .o list so both the main binary and the test binary can share it without duplication. New TEST_BIN := skeletonkey-test target. 'make test' builds and runs the suite. - .github/workflows/build.yml: install libglib2.0-dev + pkg-config so pack2theroot builds with GLib in CI (was previously stub-compiling). New 'tests — detect() unit suite' step runs 'make test' as a non-root user so modules' 'already root' gates don't short-circuit before the synthetic host checks fire. - Test harness compiles cross-platform but assertions are #ifdef __linux__ guarded (on non-Linux all module detect() bodies stub-out to PRECOND_FAIL, making assertions tautological); macOS dev build reports 'skipped'. Module change: - pack2theroot p2tr_detect now consults ctx->host->is_root (with a geteuid() fallback when ctx->host is null) instead of calling geteuid() directly. Production behaviour is identical (host->is_root is populated from geteuid() at startup); tests can now construct non-root fingerprints regardless of the test process's actual euid. Exposed a real consistency issue worth fixing. Verified in docker as non-root: 8/8 pass on Linux. macOS reports 'skipped' as designed. |
||
|
|
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)'.
|
||
|
|
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.
|
||
|
|
cdb8f5e8f9 |
all modules: wrap Linux-only code in #ifdef __linux__ — full macOS build works
Every kernel-LPE module that uses Linux-only headers (splice, posix_fadvise,
linux/netlink.h, sys/ptrace.h, etc.) now follows the same #ifdef __linux__
pattern the new modules already used: Linux body in the ifdef, stub
detect/exploit/cleanup returning SKELETONKEY_PRECOND_FAIL on non-Linux,
platform-neutral rule strings + module struct + register fn left outside.
14 modules wrapped:
dirty_pipe (already done above), af_packet, af_packet2,
cgroup_release_agent, cls_route4, dirty_cow, fuse_legacy,
netfilter_xtcompat, nf_tables, nft_fwd_dup, nft_payload,
overlayfs, overlayfs_setuid, ptrace_traceme.
Several modules previously had ad-hoc partial stubs (af_packet2 faked
SIOCSIFFLAGS/MAP_LOCKED, netfilter_xtcompat faked sysv-msg syscalls,
the nft_* modules had 3 partial __linux__ islands each, fuse_legacy /
nf_tables had inner-only ifdef blocks) — all replaced with the uniform
outer-wrap shape from dirty_pipe / dirtydecrypt / fragnesia / pack2theroot.
Where a module includes core/kernel_range.h, core/finisher.h, or
core/offsets.h, those are now inside the ifdef block as well — silences
clangd's "unused-includes" LSP warning on macOS while keeping them
present for the real Linux build.
No exploit logic, constant, struct, shellcode byte, or rule string was
modified — only include placement and ifdef markers.
Build verification:
macOS (local): make clean && make → Mach-O x86_64, 31 modules
registered, --scan reports each Linux-only module as
"Linux-only module — not applicable here".
Linux (docker gcc:latest + libglib2.0-dev): make clean && make →
ELF 64-bit, 31 modules. Exploit code paths unchanged.
|
||
|
|
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).
|
||
|
|
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`).
|
||
|
|
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. |