8de46e212e15a7487be0821fcf869994b9d7767e
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |