Files
leviathan 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.
2026-05-23 22:15:44 -04:00

222 lines
9.6 KiB
C

/*
* vsock_uaf_cve_2024_50264 — SKELETONKEY module
*
* STATUS: 🟡 PRIMITIVE. Race-driver + msg_msg groom on kmalloc-96
* (the bucket where struct virtio_vsock_sock at 80 bytes lives).
* Full cred-overwrite via the V12 / @v4bel + @qwerty msg_msg path
* from the PT SWARM writeup is documented but not bundled here;
* --full-chain falls through to the shared finisher on x86_64.
*
* The bug (Original bug since Aug 2016; weaponized publicly 2024 →
* Pwn2Own + Pwnie Award 2025 winner):
* AF_VSOCK's `connect()` system call races with a POSIX signal
* that interrupts the connect path. The signal handler tears down
* the virtio_vsock_sock object while connect() still holds a
* reference; subsequent connect-completion writes UAF the freed
* slot. virtio_vsock_sock is 80 bytes → kmalloc-96 slab.
*
* Two known exploitation strategies:
* (a) Original @v4bel + @qwerty kernelCTF path:
* BPF-JIT spray to fill physical memory + SLUBStick →
* page-grained primitive → cred overwrite.
* (b) Alexander Popov (PT SWARM) msg_msg path:
* msg_msg kmalloc-96 groom + UAF write into a forged
* msg_msg header → arb read/write primitive → cred overwrite.
* Doesn't need BPF JIT enabled; works on hardened distros.
*
* Notable: bug is reachable as a PLAIN UNPRIVILEGED USER — no
* userns required. Most kernel-UAF chains need userns for the
* spray, so this is unusually broadly exploitable.
*
* Affects: Linux kernels with CONFIG_VSOCKETS + CONFIG_VIRTIO_VSOCKETS
* below the fix. The bug has existed since the AF_VSOCK signal-
* interrupt code was added in 2016 (commit b91ee4aabbe2). Fix
* commit ad8e1afecc3a (mainline Nov 2024). Stable backports:
* 6.6.x : 6.6.59 (LTS)
* 6.1.x : 6.1.115
* 5.15.x : 5.15.170
* 5.10.x : 5.10.228
*
* Preconditions:
* - socket(AF_VSOCK, ...) must work — requires vsock module
* loaded (autoloaded on KVM/QEMU guests; absent on bare-metal
* hosts without virtualization)
* - msgsnd / SysV IPC for kmalloc-96 spray
* - POSIX timers for the signal-interrupt portion
*
* arch_support: x86_64+unverified-arm64. The bug + race are arch-
* agnostic; the cred-overwrite chains in both published PoCs use
* x86_64-specific kernel offsets.
*/
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/host.h"
#include "../../core/offsets.h"
#include "../../core/finisher.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#ifndef AF_VSOCK
#define AF_VSOCK 40
#endif
/* ---- kernel-range table -------------------------------------------- */
static const struct kernel_patched_from vsock_patched_branches[] = {
{5, 10, 228}, /* 5.10 LTS stable */
{5, 15, 170}, /* 5.15 LTS */
{6, 1, 115}, /* 6.1 LTS */
{6, 6, 59}, /* 6.6 LTS */
{6, 11, 0}, /* mainline fix ad8e1afecc3a */
};
static const struct kernel_range vsock_range = {
.patched_from = vsock_patched_branches,
.n_patched_from = sizeof(vsock_patched_branches) /
sizeof(vsock_patched_branches[0]),
};
/* ---- detect --------------------------------------------------------- */
static bool vsock_reachable(void)
{
int s = socket(AF_VSOCK, SOCK_STREAM, 0);
if (s < 0) return false;
close(s);
return true;
}
static skeletonkey_result_t vsock_uaf_detect(const struct skeletonkey_ctx *ctx)
{
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
if (!v || v->major == 0) {
if (!ctx->json) fprintf(stderr, "[!] vsock_uaf: host fingerprint missing kernel version\n");
return SKELETONKEY_TEST_ERROR;
}
if (kernel_range_is_patched(&vsock_range, v)) {
if (!ctx->json) fprintf(stderr, "[+] vsock_uaf: kernel %s is patched (>= LTS backport / 6.11)\n", v->release);
return SKELETONKEY_OK;
}
if (!vsock_reachable()) {
if (!ctx->json) {
fprintf(stderr, "[+] vsock_uaf: AF_VSOCK socket() unavailable — vsock module not loaded\n");
fprintf(stderr, " (typical on bare-metal hosts without virtualization; module autoloads on KVM/QEMU guests)\n");
}
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[!] vsock_uaf: kernel %s + AF_VSOCK reachable → VULNERABLE\n", v->release);
fprintf(stderr, "[i] vsock_uaf: bug works as plain unprivileged user (no userns required)\n");
fprintf(stderr, "[i] vsock_uaf: Pwnie Award 2025 winner; race + msg_msg groom for chain\n");
}
return SKELETONKEY_VULNERABLE;
}
static skeletonkey_result_t vsock_uaf_exploit(const struct skeletonkey_ctx *ctx)
{
if (!ctx->authorized) {
fprintf(stderr, "[-] vsock_uaf: --i-know required for --exploit\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!vsock_reachable()) {
fprintf(stderr, "[-] vsock_uaf: AF_VSOCK socket() unavailable\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
fprintf(stderr,
"[i] vsock_uaf: race-driver setup. POSIX timer fires SIGUSR1\n"
" mid-connect() on AF_VSOCK; signal handler triggers the\n"
" virtio_vsock_sock teardown that races the connect path.\n"
" msg_msg cross-cache spray (kmalloc-96, tag SKK_VSOCK)\n"
" refills the freed slot. Two published full chains:\n"
" (a) @v4bel + @qwerty kernelCTF (BPF JIT spray + SLUBStick)\n"
" (b) Alexander Popov / PT SWARM (msg_msg arb R/W)\n"
" Neither chain is bundled here (per verified-vs-claimed —\n"
" requires a portable arb-write callback for the finisher).\n"
" Returning EXPLOIT_FAIL honestly.\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
/* ---- detection rules ------------------------------------------------ */
static const char vsock_auditd[] =
"# vsock_uaf CVE-2024-50264 — auditd detection rules\n"
"# AF_VSOCK socket() (a0=40) + SysV IPC msgsnd burst + POSIX timer\n"
"# (timer_create) is the canonical trigger shape.\n"
"-a always,exit -F arch=b64 -S socket -F a0=40 -k skeletonkey-vsock-uaf\n";
static const char vsock_sigma[] =
"title: Possible CVE-2024-50264 AF_VSOCK connect-race UAF\n"
"id: 0c5b1e90-skeletonkey-vsock-uaf\n"
"status: experimental\n"
"description: |\n"
" Detects AF_VSOCK socket creation + msgsnd kmalloc-96 spray\n"
" shape from a non-root process. VSOCK is rare outside\n"
" KVM/QEMU host-guest channels; non-root usage on a bare-metal\n"
" host with msg_msg grooming alongside is the Pwnie-Award\n"
" Pwn2Own exploit trigger.\n"
"logsource: {product: linux, service: auditd}\n"
"detection:\n"
" vs: {type: 'SYSCALL', syscall: 'socket', a0: 40}\n"
" groom: {type: 'SYSCALL', syscall: 'msgsnd'}\n"
" condition: vs and groom\n"
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2024.50264]\n";
static const char vsock_yara[] =
"rule vsock_uaf_cve_2024_50264 : cve_2024_50264 kernel_uaf {\n"
" meta:\n"
" cve = \"CVE-2024-50264\"\n"
" description = \"SKELETONKEY vsock_uaf race-driver tag (Pwnie 2025 winner)\"\n"
" author = \"SKELETONKEY\"\n"
" strings:\n"
" $tag = \"SKK_VSOCK\" ascii\n"
" condition:\n"
" $tag\n"
"}\n";
static const char vsock_falco[] =
"- rule: AF_VSOCK socket() + msgsnd spray (vsock UAF race)\n"
" desc: |\n"
" Non-root process creates an AF_VSOCK socket then drives\n"
" msgsnd burst for kmalloc-96 spray. AF_VSOCK on bare-metal\n"
" Linux is rare; the combination with msgsnd grooming is the\n"
" Pwnie-Award-winning exploit shape.\n"
" condition: >\n"
" evt.type = socket and evt.arg.domain = AF_VSOCK and\n"
" not user.uid = 0\n"
" output: >\n"
" AF_VSOCK socket from non-root (user=%user.name pid=%proc.pid)\n"
" priority: HIGH\n"
" tags: [network, mitre_privilege_escalation, T1068, cve.2024.50264]\n";
const struct skeletonkey_module vsock_uaf_module = {
.name = "vsock_uaf",
.cve = "CVE-2024-50264",
.summary = "AF_VSOCK connect-race UAF (kmalloc-96) — Pwn2Own 2024 / Pwnie 2025",
.family = "vsock",
.kernel_range = "Linux < 6.11 / 6.6.59 / 6.1.115 / 5.15.170 / 5.10.228 with vsock loaded",
.detect = vsock_uaf_detect,
.exploit = vsock_uaf_exploit,
.mitigate = NULL, /* mitigation: upgrade kernel; OR blacklist vsock module */
.cleanup = NULL,
.detect_auditd = vsock_auditd,
.detect_sigma = vsock_sigma,
.detect_yara = vsock_yara,
.detect_falco = vsock_falco,
.opsec_notes = "Opens AF_VSOCK socket (family 40 — unusual on bare-metal Linux; autoloaded on KVM/QEMU guests). Arms a POSIX timer to deliver SIGUSR1 within ~10ms; calls connect() to a bogus VSOCK address (cid=0xdead, port=0xbeef); signal interrupts the connect and tears down virtio_vsock_sock while connect-completion still writes to it → UAF on the kmalloc-96 slab. Sysv msgsnd spray (tag 'SKK_VSOCK') refills the freed slot with attacker-controlled bytes. The bug works as a PLAIN UNPRIVILEGED USER — no userns, no CAP_*, no special groups. dmesg may show 'KASAN: use-after-free in virtio_vsock_'. Audit-visible via socket(AF_VSOCK) + msgsnd + timer_create from a single process — unusual combination outside the exploit. No persistent file artifacts.",
.arch_support = "x86_64+unverified-arm64",
};
void skeletonkey_register_vsock_uaf(void)
{
skeletonkey_register(&vsock_uaf_module);
}