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.
This commit is contained in:
2026-05-16 19:51:47 -04:00
parent 19b9162b1d
commit 1552a3bfcb
12 changed files with 363 additions and 11 deletions
+1 -1
View File
@@ -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. |
+8 -3
View File
@@ -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
+18 -7
View File
@@ -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
+71
View File
@@ -0,0 +1,71 @@
/*
* IAMROOT — kernel_range implementation
*/
#include "kernel_range.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/utsname.h>
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;
}
+59
View File
@@ -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 <stdbool.h>
#include <stddef.h>
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 */
+1
View File
@@ -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 */
+1
View File
@@ -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};
@@ -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
@@ -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
@@ -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 <stdio.h>
#include <string.h>
/* 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);
}
@@ -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