โฌข DragonFlyBSD Kernel Audit
DF-0393 / harness_fixed.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 PATCHED ieee80211_scan_sta.c:310-322 ----
     * (copied character-for-character from the fixed /usr/src after
     *  applying fix.diff; the only change vs the unfixed harness.c is
     *  the added bounds clamp on meshidlen) */
#ifdef IEEE80211_SUPPORT_MESH
    if (sp->meshid != NULL && sp->meshid[1] != 0) {
        uint8_t meshidlen = sp->meshid[1];
        if (meshidlen > IEEE80211_MESHID_LEN)
            meshidlen = IEEE80211_MESHID_LEN;
        memcpy(ise->se_meshid, sp->meshid, 2 + meshidlen);
    }
#endif
    /* ---- END verbatim (patched) ---- */
}

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. With the fix in effect, the clamp limits the
     * actual memcpy to 2+min(meshid[1],32) bytes, so se_meshid[34] is
     * never exceeded. We report based on OBSERVED memory corruption
     * (se_ies / se_age / canary), not the raw input length. */
    uint8_t meshid_clamped = ie[1] > IEEE80211_MESHID_LEN ? IEEE80211_MESHID_LEN : ie[1];
    int actual_copy = 2 + meshid_clamped;

    /* Detect real corruption: se_ies was poisoned to 0xAA, se_age to 0xAABBCCDD */
    int ies_corrupted = memcmp(&a->se.se_ies, "\xAA", 1) != 0 ? 0 :
                        (memcmp(((uint8_t *)&a->se.se_ies)+1, "\xAA", 1) != 0); /* any byte changed? */
    /* simpler: check if wpa_ie ptr changed from 0xAAAAAAAAAAAAAAAA */
    uint64_t poison_ies = 0xAAAAAAAAAAAAAAAAULL;
    ies_corrupted = (memcmp(&a->se.se_ies.wpa_ie, &poison_ies, 8) != 0);
    int age_corrupted = (a->se.se_age != 0xAABBCCDD);
    int first_bad = -1;
    int canary_bad = canary_corrupted(a, &first_bad);
    int any_corruption = ies_corrupted || age_corrupted || canary_bad;

    printf("---- Corruption report (PATCHED harness) ----\n");
    printf("  input meshid[1]            = %u\n", ie[1]);
    printf("  clamped meshidlen          = %u (IEEE80211_MESHID_LEN=%d)\n",
           meshid_clamped, IEEE80211_MESHID_LEN);
    printf("  actual memcpy size         = %d bytes (<= se_meshid width %zu)\n",
           actual_copy, sizeof(a->se.se_meshid));
    hexdump(a->se.se_meshid, sizeof(a->se.se_meshid), "se_meshid[34] (full field)");
    printf("  se_ies.wpa_ie ptr          = %p  %s\n", (void *)a->se.se_ies.wpa_ie,
           ies_corrupted ? "(CORRUPTED!)" : "(intact)");
    printf("  se_age                     = 0x%08X  %s\n", a->se.se_age,
           age_corrupted ? "(CORRUPTED!)" : "(intact)");
    printf("  canary                     = %s\n", canary_bad ? "CORRUPTED!" : "intact");

    printf("\nVERDICT: ");
    if (!any_corruption) {
        printf("NO OVERFLOW โ€” fix in effect: meshid[1]=%u clamped to %d, "
               "actual copy %d bytes fits se_meshid[%zu]; se_ies/se_age/canary all intact.\n",
               ie[1], meshid_clamped, actual_copy, sizeof(a->se.se_meshid));
        free(ie); free(a);
        return 1;  /* no overflow = fix works */
    } else {
        printf("OVERFLOW STILL PRESENT โ€” se_ies%s%s corrupted; fix is insufficient!\n",
               ies_corrupted ? " (ptrs)" : "", age_corrupted ? " +se_age" : "");
        free(ie); free(a);
        return 0;
    }
}