Phase 7: nf_tables CVE-2024-1086 + active probe for dirty_pipe
dirty_pipe detect: active sentinel probe (Phase 1.5-ish improvement)
- New dirty_pipe_active_probe(): creates a /tmp probe file with known
sentinel bytes, fires the Dirty Pipe primitive against it, re-reads
via the page cache, returns true if the poisoning landed.
- detect() gated on ctx->active_probe: --scan does version-only check
(fast, no side effects); --scan --active fires the empirical probe
and overrides version inference with the empirical verdict. Catches
silent distro backports that don't bump uname() version.
- Three verdicts now distinguishable:
(a) version says patched, no active probe → 'patched (version-only)'
(b) version says vulnerable, --active fires + probe lands → CONFIRMED
(c) version says vulnerable, --active fires + probe blocked → 'likely
patched via distro backport'
- Probe is safe: only /tmp, no /etc/passwd.
nf_tables CVE-2024-1086 (detect-only, new module):
- Famous Notselwyn UAF in nft_verdict_init. Affects 5.14 ≤ K, fixed
mainline 6.8 with backports landing in 5.4.269 / 5.10.210 / 5.15.149
/ 6.1.74 / 6.6.13 / 6.7.2.
- detect() checks: kernel version range, AND unprivileged user_ns clone
availability (the exploit's reachability gate — kernel-vulnerable
but userns-locked-down hosts report PRECOND_FAIL, signalling that
the kernel still needs patching but unprivileged path is closed).
- Ships auditd + sigma detection rules: unshare(CLONE_NEWUSER) chained
with setresuid(0,0,0) on a previously-non-root process is the
exploit's canonical telltale.
- Full Notselwyn-style exploit (cross-cache UAF → arbitrary R/W → cred
overwrite or modprobe_path hijack) is the next commit.
9 modules total now. CVES.md and ROADMAP.md updated.
This commit is contained in:
@@ -212,10 +212,48 @@ static const struct kernel_range dirty_pipe_range = {
|
||||
sizeof(dirty_pipe_patched_branches[0]),
|
||||
};
|
||||
|
||||
/* Active sentinel probe: write a known byte into a /tmp probe file
|
||||
* via the Dirty Pipe primitive, then re-read to verify the page cache
|
||||
* was actually poisoned. This catches the case where /proc/version
|
||||
* looks vulnerable (e.g. Debian 5.10.0-30 — apparent version 5.10.30)
|
||||
* but the distro silently backported the fix without bumping the
|
||||
* upstream version number visible to uname().
|
||||
*
|
||||
* Side effects: creates and removes a single file under /tmp. No
|
||||
* /etc/passwd writes; safe to run from --scan --active. */
|
||||
static int dirty_pipe_active_probe(void)
|
||||
{
|
||||
char probe_path[] = "/tmp/iamroot-dirty-pipe-probe-XXXXXX";
|
||||
int fd = mkstemp(probe_path);
|
||||
if (fd < 0) return -1;
|
||||
const char seed[16] = "ABCDABCDABCDABCD";
|
||||
if (write(fd, seed, sizeof seed) != sizeof seed) { close(fd); unlink(probe_path); return -1; }
|
||||
fsync(fd);
|
||||
close(fd);
|
||||
|
||||
/* Try writing 'X' at offset 4 — well inside the first page, not
|
||||
* page-aligned (offset 4 → page-relative offset 4, not 0). */
|
||||
int rc = dirty_pipe_write(probe_path, 4, "X", 1);
|
||||
if (rc < 0) {
|
||||
unlink(probe_path);
|
||||
return 0; /* primitive could not even fire — patched or blocked */
|
||||
}
|
||||
|
||||
/* Re-open and read; if the primitive works, byte 4 reads as 'X'.
|
||||
* Use O_RDONLY + read, which goes through the page cache (which
|
||||
* we just poisoned if the bug is live). */
|
||||
fd = open(probe_path, O_RDONLY);
|
||||
if (fd < 0) { unlink(probe_path); return -1; }
|
||||
char readback[16] = {0};
|
||||
ssize_t got = read(fd, readback, sizeof readback);
|
||||
close(fd);
|
||||
unlink(probe_path);
|
||||
if (got < 5) return -1;
|
||||
return readback[4] == 'X' ? 1 : 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");
|
||||
@@ -231,19 +269,51 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
|
||||
return IAMROOT_OK;
|
||||
}
|
||||
|
||||
bool patched = kernel_range_is_patched(&dirty_pipe_range, &v);
|
||||
if (patched) {
|
||||
bool patched_by_version = kernel_range_is_patched(&dirty_pipe_range, &v);
|
||||
|
||||
/* Active probe overrides version-only verdict when requested.
|
||||
* The version check is necessary-but-not-sufficient: distros
|
||||
* silently backport fixes without bumping the upstream version
|
||||
* visible to uname(). The active probe fires the actual primitive
|
||||
* and confirms whether it lands. */
|
||||
if (ctx->active_probe) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] dirty_pipe: kernel %s is patched\n", v.release);
|
||||
fprintf(stderr, "[*] dirty_pipe: running active sentinel probe (safe; /tmp only)\n");
|
||||
}
|
||||
int probe = dirty_pipe_active_probe();
|
||||
if (probe == 1) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] dirty_pipe: ACTIVE PROBE CONFIRMED — primitive lands "
|
||||
"(version %s)\n", v.release);
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
}
|
||||
if (probe == 0) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] dirty_pipe: active probe sentinel did NOT land — "
|
||||
"primitive blocked (likely patched%s)\n",
|
||||
patched_by_version ? "" : ", or distro silently backported");
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
}
|
||||
/* probe < 0: probe machinery failed (mkstemp/open/read) — fall
|
||||
* back to version-only verdict and report TEST_ERROR caveat */
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[?] dirty_pipe: active probe machinery failed; "
|
||||
"falling back to version check\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (patched_by_version) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] dirty_pipe: kernel %s is patched (version-only check; "
|
||||
"use --active to confirm empirically)\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",
|
||||
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n"
|
||||
" Confirm empirically: re-run with --scan --active\n",
|
||||
v.release);
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
|
||||
Reference in New Issue
Block a user