Files
leviathan a26f471ecf dirtydecrypt + fragnesia: pin CVE fix commits, version-based detect()
Both modules' detect() was precondition-only because we didn't know the
mainline fix commits at port time. Debian's security tracker now
provides them — pinning here turns detect() into a proper version-
based verdict (still with --active for empirical override).

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

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

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

Both detect()s smoke-tested in docker gcc:latest on kernel 6.12.76-
linuxkit: dirtydecrypt correctly reports OK ('predates the rxgk code
added in 7.0'); fragnesia + pack2theroot correctly report
PRECOND_FAIL (no userns / no D-Bus in container). Local macOS + Linux
builds both clean.
2026-05-22 23:06:15 -04:00

3.7 KiB

dirtydecrypt — CVE-2026-31635

🟡 PRIMITIVE / ported. Faithful port of the public V12 PoC into the skeletonkey_module interface. Not yet validated end-to-end on a vulnerable-kernel VM — see Verification status below.

Summary

DirtyDecrypt (a.k.a. DirtyCBC) is a missing copy-on-write guard in rxgk_decrypt_skb() (net/rxrpc/rxgk_common.h). The function decrypts incoming rxgk socket buffers in place before the HMAC is verified. When the skb fragment pages are page-cache pages — spliced in via MSG_SPLICE_PAGES over loopback — the in-place AES decrypt corrupts the page cache of a read-only file.

It is a sibling of Copy Fail (CVE-2026-31431) and Dirty Frag (CVE-2026-43284 / 43500): same bug class, different kernel subsystem (rxgk / AFS-style rxrpc encryption rather than algif_aead or xfrm-ESP).

Primitive

Each fire():

  1. Adds an rxrpc security key holding a crafted rxgk XDR token.
  2. Opens an AF_RXRPC client + a fake UDP server on loopback and completes the rxgk handshake.
  3. Forges a DATA packet whose wire header comes from userspace and whose payload pages come from the target file's page cache (splice + vmsplice).
  4. The kernel decrypts the spliced page-cache pages in place — the HMAC check then fails (expected), but the page cache is already mutated.

pagecache_write() drives a sliding-window technique: byte[0] of each corrupted 16-byte AES block is uniformly random (≈1/256 chance of the wanted value), and round i+1 at offset S+i+1 overwrites the 15-byte collateral of round i without disturbing the byte round i fixed. Net cost ≈ 256 fires per byte.

The exploit rewrites the first 120 bytes of a setuid-root binary (/usr/bin/su and friends) with a tiny ET_DYN ELF that calls setuid(0) + execve("/bin/sh").

Operations

Op Behaviour
--scan Checks AF_RXRPC reachability + a readable setuid carrier. With --active, fires the primitive against a disposable /tmp file and reports VULNERABLE/OK empirically.
--exploit … --i-know Forks a child that corrupts the carrier's page cache and execs it for a root shell. --no-shell stops after the page-cache write.
--cleanup Evicts the carrier from the page cache (POSIX_FADV_DONTNEED + drop_caches). The on-disk binary is never written.
--detect-rules Emits embedded auditd + sigma rules.

Preconditions

  • AF_RXRPC reachable (the rxrpc module loadable / built in).
  • A readable setuid-root binary to use as the payload carrier.
  • x86_64 (the embedded ELF payload is x86_64 shellcode).

Verification status

This module is a faithful port of https://github.com/v12-security/pocs/tree/main/dirtydecrypt, compiled into the SKELETONKEY module interface. The exploit body has not been validated end-to-end against a known-vulnerable kernel inside the SKELETONKEY CI matrix.

detect() is now version-pinned against the mainline fix commit a2567217ade970ecc458144b6be469bc015b23e5 (Linux 7.0): kernels < 7.0 predate the vulnerable rxgk RESPONSE-handling code (Debian tracker confirms older stable branches as <not-affected, vulnerable code not present>), kernels ≥ 7.0 have the fix. With --active, the detector runs the rxgk primitive against a /tmp sentinel and reports empirically — catches pre-fix 7.0-rc kernels and any distro rebuilds the version check misses.

Before promoting to 🟢: validate the exploit end-to-end on a 7.0-rc kernel that pre-dates commit a2567217ade…. The Debian tracker entry for CVE-2026-31635 is the source of truth for branch-backport thresholds; extend the kernel_range table when distros publish stable backports.