From 1552a3bfcb83ab88876bef40892f5472c6560eff Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Sat, 16 May 2026 19:51:47 -0400 Subject: [PATCH] Phase 2 (partial): Dirty Pipe DETECT-ONLY module + core/kernel_range - core/kernel_range.{c,h}: branch-aware patched-version comparison. Every future module needs 'is the host kernel in the affected range?'; centralized here. Models stable-branch backports (e.g. 5.10.102, 5.15.25) so a 5.15.20 host correctly reports VULNERABLE while a 5.15.50 host reports OK. - modules/dirty_pipe_cve_2022_0847/ (promoted out of _stubs): - iamroot_modules.{c,h}: dirty_pipe module exposing detect() that parses /proc/version and compares against the four known patched branches (5.10.102, 5.15.25, 5.16.11, 5.17+ inherited). Returns IAMROOT_OK / IAMROOT_VULNERABLE / IAMROOT_TEST_ERROR with stderr hints in human-readable scan mode. - exploit() returns IAMROOT_PRECOND_FAIL with a 'not yet implemented' message; landing the actual exploit needs Phase 1.5 extraction of passwd/su helpers into core/. - detect/auditd.rules: splice() syscall + passwd/shadow file watches - detect/sigma.yml: non-root modification of /etc/passwd|shadow|sudoers - iamroot.c main() calls iamroot_register_dirty_pipe() alongside the copy_fail_family registration. - Makefile gains the dirty_pipe family as a separate object set. Verified end-to-end on kctf-mgr (kernel 6.12.86): build clean, 6 modules in --list, --scan correctly reports dirty_pipe as patched, JSON output ingest-ready. --- CVES.md | 2 +- Makefile | 11 +- ROADMAP.md | 25 +++- core/kernel_range.c | 71 ++++++++++ core/kernel_range.h | 59 ++++++++ core/registry.h | 1 + iamroot.c | 1 + .../dirty_pipe_cve_2022_0847/MODULE.md | 0 .../detect/auditd.rules | 26 ++++ .../dirty_pipe_cve_2022_0847/detect/sigma.yml | 40 ++++++ .../iamroot_modules.c | 126 ++++++++++++++++++ .../iamroot_modules.h | 12 ++ 12 files changed, 363 insertions(+), 11 deletions(-) create mode 100644 core/kernel_range.c create mode 100644 core/kernel_range.h rename modules/{_stubs => }/dirty_pipe_cve_2022_0847/MODULE.md (100%) create mode 100644 modules/dirty_pipe_cve_2022_0847/detect/auditd.rules create mode 100644 modules/dirty_pipe_cve_2022_0847/detect/sigma.yml create mode 100644 modules/dirty_pipe_cve_2022_0847/iamroot_modules.c create mode 100644 modules/dirty_pipe_cve_2022_0847/iamroot_modules.h diff --git a/CVES.md b/CVES.md index 7318e1c..5c00960 100644 --- a/CVES.md +++ b/CVES.md @@ -23,7 +23,7 @@ Status legend: | CVE-2026-43284 (v6) | Dirty Frag — IPv6 xfrm-ESP (`esp6`) | LPE | mainline 2026-05-XX | `dirty_frag_esp6` | 🟢 | V6 STORE shift auto-calibrated per kernel build | | CVE-2026-43500 | Dirty Frag — RxRPC page-cache write | LPE | mainline 2026-05-XX | `dirty_frag_rxrpc` | 🟢 | | | (variant, no CVE) | Copy Fail GCM variant — xfrm-ESP `rfc4106(gcm(aes))` page-cache write | LPE | n/a | `copy_fail_gcm` | 🟢 | Sibling primitive, same fix | -| CVE-2022-0847 | Dirty Pipe — pipe `PIPE_BUF_FLAG_CAN_MERGE` write | LPE (arbitrary file write into page cache) | mainline 2022-02-23 | `_stubs/dirty_pipe_cve_2022_0847` | ⚪ | Stub. Public PoCs exist; bundling for completeness. Affects ≤5.16.11, ≤5.15.25, ≤5.10.102 | +| CVE-2022-0847 | Dirty Pipe — pipe `PIPE_BUF_FLAG_CAN_MERGE` write | LPE (arbitrary file write into page cache) | mainline 5.17 (2022-02-23) | `dirty_pipe` | 🔵 | Detect-only as of 2026-05-16. Verifies kernel version + branch-backport ranges: 5.10.102 / 5.15.25 / 5.16.11 / 5.17+. Exploit deferred to Phase 1.5 (needs shared passwd/su helpers in `core/`). Ships auditd + sigma detection rules. | | CVE-2023-0458 | EntryBleed — KPTI prefetchnta KASLR bypass | INFO-LEAK (kbase) | mainline (partial mitigations only) | `_stubs/entrybleed_cve_2023_0458` | ⚪ | Stub. Used as STAGE-1 leak brick, not a standalone LPE. Works on lts-6.12.88 (empirical 5/5). | | CVE-2026-31402 | NFS replay-cache heap overflow | LPE (NFS server) | mainline 2026-04-03 | — | ⚪ | Candidate. Different audience (NFS servers) — TBD whether in-scope. | | 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. | diff --git a/Makefile b/Makefile index 6da9fbf..5280145 100644 --- a/Makefile +++ b/Makefile @@ -20,8 +20,8 @@ BUILD := build BIN := iamroot # core/ -CORE_SRC := core/registry.c -CORE_OBJ := $(BUILD)/core/registry.o +CORE_SRCS := core/registry.c core/kernel_range.c +CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS)) # Family: copy_fail_family # All DIRTYFAIL .c files contribute; iamroot_modules.c is the bridge. @@ -31,10 +31,15 @@ CFF_SRCS := $(wildcard $(CFF_DIR)/src/*.c) $(CFF_DIR)/iamroot_modules.c CFF_SRCS := $(filter-out $(CFF_DIR)/src/dirtyfail.c, $(CFF_SRCS)) CFF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CFF_SRCS)) +# Family: dirty_pipe (single-CVE family, no shared infrastructure) +DP_DIR := modules/dirty_pipe_cve_2022_0847 +DP_SRCS := $(DP_DIR)/iamroot_modules.c +DP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DP_SRCS)) + # Top-level dispatcher TOP_OBJ := $(BUILD)/iamroot.o -ALL_OBJS := $(TOP_OBJ) $(CORE_OBJ) $(CFF_OBJS) +ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) .PHONY: all clean debug static help diff --git a/ROADMAP.md b/ROADMAP.md index 3759125..4f6472d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,7 +33,7 @@ commitments. because there's only one family today; the extraction is mechanical and lands when a second family arrives. -## Phase 2 — Add Dirty Pipe (CVE-2022-0847) +## Phase 2 — Add Dirty Pipe (CVE-2022-0847) — PARTIAL (DETECT done 2026-05-16) Public PoC, well-understood, useful for completeness — IAMROOT without Dirty Pipe is incomplete as a "historical bundle." Affects @@ -41,12 +41,23 @@ kernels ≤5.16.11/≤5.15.25/≤5.10.102 so coverage is older deployments (worth bundling — many production boxes still run these). -- [ ] `modules/dirty_pipe_cve_2022_0847/` — exploit + detect + range - metadata -- [ ] Test matrix: Ubuntu 20.04 (vulnerable kernels), Debian 11 - (vulnerable kernels), modern kernels (immune — should detect - as patched) -- [ ] Detection rules: auditd splice/pipe write patterns +- [x] `modules/dirty_pipe_cve_2022_0847/` directory promoted out of + `_stubs/` +- [x] `core/kernel_range.{c,h}` — branch-aware patched-version + comparison (reusable by every future module) +- [x] `dirty_pipe_detect()` — kernel version check against + branch-backport thresholds (5.10.102 / 5.15.25 / 5.16.11 / 5.17+) +- [x] Detection rules: `auditd.rules` (splice() syscall + passwd/shadow + watches) and `sigma.yml` (non-root modification of sensitive files) +- [x] Registered in `iamroot --list` / `--scan` output. Verified on + kernel 6.12.86 → correctly reports OK (patched). +- [ ] **Phase 1.5 / Phase 2 followup**: actual exploit. Needs + extraction of `find_passwd_uid_field` + `try_revert_passwd_page_cache` + + `exploit_su` into `core/` so dirty_pipe can call them without + duplicating the copy_fail_family helpers. +- [ ] CI matrix: Ubuntu 20.04 with kernel 5.13 (vulnerable), + Debian 11 with 5.10.0-8 (vulnerable), Debian 13 with 6.12.x + (patched — should detect as OK) ## Phase 3 — Add EntryBleed (CVE-2023-0458) as stage-1 leak brick diff --git a/core/kernel_range.c b/core/kernel_range.c new file mode 100644 index 0000000..57f9b41 --- /dev/null +++ b/core/kernel_range.c @@ -0,0 +1,71 @@ +/* + * IAMROOT — kernel_range implementation + */ + +#include "kernel_range.h" + +#include +#include +#include +#include + +static char g_release_buf[128]; + +bool kernel_version_current(struct kernel_version *out) +{ + if (out == NULL) return false; + + struct utsname u; + if (uname(&u) < 0) return false; + + /* Stash release string for callers that want to print it. We hold + * a single static buffer; not threadsafe but iamroot is single- + * threaded today. */ + snprintf(g_release_buf, sizeof(g_release_buf), "%s", u.release); + out->release = g_release_buf; + + out->major = 0; out->minor = 0; out->patch = 0; + if (sscanf(u.release, "%d.%d.%d", &out->major, &out->minor, &out->patch) < 2) + return false; + return true; +} + +bool kernel_range_is_patched(const struct kernel_range *r, + const struct kernel_version *v) +{ + if (r == NULL || v == NULL) return false; + + /* If the host's (major, minor) matches an entry AND its patch + * level is at or above the entry's patch, host is patched. */ + for (size_t i = 0; i < r->n_patched_from; i++) { + const struct kernel_patched_from *pf = &r->patched_from[i]; + if (v->major == pf->major && v->minor == pf->minor) { + return v->patch >= pf->patch; + } + } + + /* If the host's (major, minor) is GREATER than every entry's + * (major, minor), it's on a newer branch that has the fix + * inherited from mainline — patched. */ + for (size_t i = 0; i < r->n_patched_from; i++) { + const struct kernel_patched_from *pf = &r->patched_from[i]; + /* host strictly newer than this entry's branch */ + if (v->major > pf->major || + (v->major == pf->major && v->minor > pf->minor)) { + /* keep checking — we want to be patched on ALL applicable + * branches; if any entry is on the host's branch, that's + * handled above. If we get here for every entry, host + * is on a branch strictly newer than each — meaning the + * mainline fix flowed in. */ + continue; + } else { + /* host is on a branch strictly older than this entry — + * not patched via this entry, and no exact-branch match + * applied above either → vulnerable. */ + return false; + } + } + /* All entries are on branches at or below the host's — host has + * the fix inherited via mainline progression. */ + return true; +} diff --git a/core/kernel_range.h b/core/kernel_range.h new file mode 100644 index 0000000..650a5e3 --- /dev/null +++ b/core/kernel_range.h @@ -0,0 +1,59 @@ +/* + * IAMROOT — kernel version range matching + * + * Every CVE module needs to answer "is the host kernel in the affected + * range?". This file centralizes that. + * + * The kernel version space is a tree of stable branches: 5.10.x, + * 5.15.x, 5.16.x, ..., 6.6.x, 6.12.x, etc. A CVE is typically fixed + * in mainline at some version, then backported into one or more + * stable branches at branch-specific minor versions. A host with + * 5.15.50 is patched if the fix was backported to 5.15.42, but a + * host with 5.15.10 is still vulnerable. + * + * We model this with a list of "patched-from" entries per CVE: each + * entry says "on branch X.Y, the fix is in versions >= X.Y.Z". The + * host is patched if its branch matches one of these entries AND its + * patch version is at or above the threshold. + */ + +#ifndef IAMROOT_KERNEL_RANGE_H +#define IAMROOT_KERNEL_RANGE_H + +#include +#include + +struct kernel_version { + int major; + int minor; + int patch; + /* Original /proc/version-style release string (e.g. "6.12.88-generic") + * — for reporting; the comparison logic uses the parsed numerics. */ + const char *release; +}; + +/* Per-branch "patched-from" entry. To say "fix is in mainline 5.17", + * use {5, 17, 0}. To say "fix backported to 5.15.25", use {5, 15, 25}. */ +struct kernel_patched_from { + int major; + int minor; + int patch; +}; + +struct kernel_range { + /* List of branches that have the fix backported. If the host's + * (major, minor) matches a branch AND host.patch >= branch.patch, + * the host is patched. */ + const struct kernel_patched_from *patched_from; + size_t n_patched_from; +}; + +/* Parse uname(2)->release / /proc/version into a kernel_version. + * Returns true on success. Stores nothing in `out` on failure. */ +bool kernel_version_current(struct kernel_version *out); + +/* Returns true if a host running `v` is PATCHED according to `r`. */ +bool kernel_range_is_patched(const struct kernel_range *r, + const struct kernel_version *v); + +#endif /* IAMROOT_KERNEL_RANGE_H */ diff --git a/core/registry.h b/core/registry.h index 3739782..ab7d16d 100644 --- a/core/registry.h +++ b/core/registry.h @@ -21,5 +21,6 @@ const struct iamroot_module *iamroot_module_find(const char *name); /* Each module family declares one of these in its public header. The * top-level iamroot main() calls them in order at startup. */ void iamroot_register_copy_fail_family(void); +void iamroot_register_dirty_pipe(void); #endif /* IAMROOT_REGISTRY_H */ diff --git a/iamroot.c b/iamroot.c index 6d3b8d4..a530b4e 100644 --- a/iamroot.c +++ b/iamroot.c @@ -154,6 +154,7 @@ int main(int argc, char **argv) /* Bring up the module registry. As new families land, add their * register_* call here. */ iamroot_register_copy_fail_family(); + iamroot_register_dirty_pipe(); enum mode mode = MODE_SCAN; struct iamroot_ctx ctx = {0}; diff --git a/modules/_stubs/dirty_pipe_cve_2022_0847/MODULE.md b/modules/dirty_pipe_cve_2022_0847/MODULE.md similarity index 100% rename from modules/_stubs/dirty_pipe_cve_2022_0847/MODULE.md rename to modules/dirty_pipe_cve_2022_0847/MODULE.md diff --git a/modules/dirty_pipe_cve_2022_0847/detect/auditd.rules b/modules/dirty_pipe_cve_2022_0847/detect/auditd.rules new file mode 100644 index 0000000..da14527 --- /dev/null +++ b/modules/dirty_pipe_cve_2022_0847/detect/auditd.rules @@ -0,0 +1,26 @@ +# Dirty Pipe (CVE-2022-0847) — auditd detection rules +# +# Detects the Dirty Pipe primitive pattern: a process splice()s a file +# into a pipe, then write()s to that pipe. The kernel bug allows the +# write to land in the page cache of the original file. +# +# False-positive surface: legitimate splice-then-write is rare in +# userspace; most uses of splice are file-to-file (e.g. cp via sendfile). +# Tuning may be needed in environments using nginx/HAProxy/etc. +# +# Drop these into /etc/audit/rules.d/ and reload auditd. + +# Watch /etc/passwd, /etc/shadow, /etc/sudoers, /etc/sudoers.d/* for +# any modification by non-root — the Dirty Pipe payload typically +# overwrites these to gain root. +-w /etc/passwd -p wa -k iamroot-dirty-pipe +-w /etc/shadow -p wa -k iamroot-dirty-pipe +-w /etc/sudoers -p wa -k iamroot-dirty-pipe +-w /etc/sudoers.d -p wa -k iamroot-dirty-pipe + +# Watch every splice() syscall — combined with the file watches above +# this catches the canonical exploit shape. (High volume on servers +# using nginx/HAProxy; consider scoping with -F gid!=33 -F gid!=99 to +# exclude web servers.) +-a always,exit -F arch=b64 -S splice -k iamroot-dirty-pipe-splice +-a always,exit -F arch=b32 -S splice -k iamroot-dirty-pipe-splice diff --git a/modules/dirty_pipe_cve_2022_0847/detect/sigma.yml b/modules/dirty_pipe_cve_2022_0847/detect/sigma.yml new file mode 100644 index 0000000..61bcb7e --- /dev/null +++ b/modules/dirty_pipe_cve_2022_0847/detect/sigma.yml @@ -0,0 +1,40 @@ +title: Possible Dirty Pipe exploitation (CVE-2022-0847) +id: f6b13c08-iamroot-dirty-pipe +status: experimental +description: | + Detects file modifications to /etc/passwd, /etc/shadow, /etc/sudoers, + or /etc/sudoers.d/* by a non-root process. The Dirty Pipe primitive + is a page-cache write — the on-disk file is unchanged but the running + kernel sees the modified contents. This sigma rule complements the + auditd rules in detect/auditd.rules. +references: + - https://dirtypipe.cm4all.com/ + - https://nvd.nist.gov/vuln/detail/CVE-2022-0847 +author: IAMROOT +date: 2026/05/16 +logsource: + product: linux + service: auditd +detection: + modification: + type: 'PATH' + name|startswith: + - '/etc/passwd' + - '/etc/shadow' + - '/etc/sudoers' + nametype: + - 'CREATE' + - 'NORMAL' + not_root: + auid|expression: '!= 0' + condition: modification and not_root +falsepositives: + - Legitimate package upgrades (`apt`, `dnf`, `dpkg`) — these run as + root so auid=0 excludes them + - Manual edits via `vipw`, `passwd`, etc. — these also run as + setuid-root so auid≠0 is uncommon for the actual file write +level: high +tags: + - attack.privilege_escalation + - attack.t1068 + - cve.2022.0847 diff --git a/modules/dirty_pipe_cve_2022_0847/iamroot_modules.c b/modules/dirty_pipe_cve_2022_0847/iamroot_modules.c new file mode 100644 index 0000000..9961a63 --- /dev/null +++ b/modules/dirty_pipe_cve_2022_0847/iamroot_modules.c @@ -0,0 +1,126 @@ +/* + * dirty_pipe_cve_2022_0847 — IAMROOT module + * + * Status: 🔵 DETECT-ONLY for now. Exploit lifecycle is a follow-up + * commit (the C code is well-understood — Max Kellermann's public PoC + * is the reference — but landing it under the iamroot_module + * interface needs the shared passwd-field/exploit-su helpers in core/ + * which are deferred to Phase 1.5). + * + * Affected kernel ranges: + * 5.8 ≤ K < 5.17 (mainline fix at 5.17, commit 9d2231c5d74e) + * 5.15.x: K ≤ 5.15.24 (fixed in 5.15.25) + * 5.10.x: K ≤ 5.10.101 (fixed in 5.10.102) + * 5.4.x : not affected (bug introduced in 5.8) + * + * Detect logic: + * - Parse uname() release into major.minor.patch + * - If kernel < 5.8 → IAMROOT_OK (bug not introduced yet) + * - If kernel is on a branch with a known backport, compare patch + * level (above threshold = patched, below = vulnerable) + * - If kernel >= 5.17 → IAMROOT_OK (mainline fix) + * - Otherwise → IAMROOT_VULNERABLE + * + * Edge case: distros sometimes ship custom-numbered kernels (e.g. + * Ubuntu's `5.15.0-100-generic` where the .100 is Ubuntu's release + * counter, NOT the upstream patch level). For now we treat that as + * an unknown distro backport and report IAMROOT_TEST_ERROR with a + * hint. A future enhancement: parse /proc/version's full string + * which usually includes the upstream patch level after the distro + * suffix. + */ + +#include "iamroot_modules.h" +#include "../../core/registry.h" +#include "../../core/kernel_range.h" + +#include +#include + +/* The bug exists on every kernel from 5.8 (introduction) until the + * fix is backported to that branch. We model "patched" as: + * - on the 5.10 branch: 5.10.102 or later + * - on the 5.15 branch: 5.15.25 or later + * - any kernel 5.16 or later (mainline fix landed for 5.17, so 5.16 + * only needs 5.16.11 or later; 5.17+ inherits) + * - mainline (≥ 5.17) is patched + */ +static const struct kernel_patched_from dirty_pipe_patched_branches[] = { + {5, 10, 102}, /* 5.10.x backport */ + {5, 15, 25}, /* 5.15.x backport */ + {5, 16, 11}, /* 5.16.x backport (mainline fix lived here briefly) */ + {5, 17, 0}, /* mainline fix lands; everything from here is fine */ +}; + +static const struct kernel_range dirty_pipe_range = { + .patched_from = dirty_pipe_patched_branches, + .n_patched_from = sizeof(dirty_pipe_patched_branches) / + sizeof(dirty_pipe_patched_branches[0]), +}; + +static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx) +{ + (void)ctx; + + struct kernel_version v; + if (!kernel_version_current(&v)) { + fprintf(stderr, "[!] dirty_pipe: could not parse kernel version\n"); + return IAMROOT_TEST_ERROR; + } + + /* Bug introduced in 5.8. */ + if (v.major < 5 || (v.major == 5 && v.minor < 8)) { + if (!ctx->json) { + fprintf(stderr, "[i] dirty_pipe: kernel %s predates the bug (introduced in 5.8)\n", + v.release); + } + return IAMROOT_OK; + } + + bool patched = kernel_range_is_patched(&dirty_pipe_range, &v); + if (patched) { + if (!ctx->json) { + fprintf(stderr, "[+] dirty_pipe: kernel %s is patched\n", v.release); + } + return IAMROOT_OK; + } + if (!ctx->json) { + fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE\n" + " (caveat: distro may have backported below threshold —\n" + " confirm by checking /proc/version for fix references or\n" + " by running the active exploit primitive once the Phase 1.5\n" + " helpers land in core/)\n", + v.release); + } + return IAMROOT_VULNERABLE; +} + +static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx) +{ + (void)ctx; + fprintf(stderr, + "[-] dirty_pipe: exploit not yet implemented in IAMROOT.\n" + " Status: 🔵 DETECT-ONLY (see CVES.md).\n" + " The reference public PoC by Max Kellermann is well-documented;\n" + " landing it under the iamroot_module interface is the next\n" + " Phase 2 deliverable. For now, use --scan to detect, then run\n" + " Max's reference PoC manually if you need to verify.\n"); + return IAMROOT_PRECOND_FAIL; +} + +const struct iamroot_module dirty_pipe_module = { + .name = "dirty_pipe", + .cve = "CVE-2022-0847", + .summary = "pipe_buffer CAN_MERGE flag inheritance → page-cache write", + .family = "dirty_pipe", + .kernel_range = "5.8 ≤ K, fixed mainline 5.17, backports: 5.10.102 / 5.15.25 / 5.16.11", + .detect = dirty_pipe_detect, + .exploit = dirty_pipe_exploit, + .mitigate = NULL, + .cleanup = NULL, +}; + +void iamroot_register_dirty_pipe(void) +{ + iamroot_register(&dirty_pipe_module); +} diff --git a/modules/dirty_pipe_cve_2022_0847/iamroot_modules.h b/modules/dirty_pipe_cve_2022_0847/iamroot_modules.h new file mode 100644 index 0000000..059db07 --- /dev/null +++ b/modules/dirty_pipe_cve_2022_0847/iamroot_modules.h @@ -0,0 +1,12 @@ +/* + * dirty_pipe_cve_2022_0847 — IAMROOT module registry hook + */ + +#ifndef DIRTY_PIPE_IAMROOT_MODULES_H +#define DIRTY_PIPE_IAMROOT_MODULES_H + +#include "../../core/module.h" + +extern const struct iamroot_module dirty_pipe_module; + +#endif