DragonFlyBSD Kernel Audit
DF-0508 / harness.c
← back to finding ↓ download raw
/*
 * DF-0508 harness: ng_l2cap ConfigReq unknown-option echo inflates m_pkthdr.len
 *
 * Source: sys/netgraph7/bluetooth/l2cap/ng_l2cap_evnt.c (DEAD CODE — netgraph7
 * is not compiled on DragonFlyBSD master)
 *
 * The bug:
 *   get_next_l2cap_opt(:1317-1322): default case returns -3 for unknown
 *   non-hint option WITHOUT validating hdr->length vs actual mbuf data.
 *   (MTU/FLUSH/QOS cases DO validate len < hdr->length at :1285,:1294,:1304)
 *
 *   ng_l2cap_process_cfg_req(:599-600):
 *     m_adj(m, off - sizeof(hdr));
 *     m->m_pkthdr.len = sizeof(hdr) + hdr.length;  // OVERWRITES with attacker value
 *
 *   If attacker sends ConfigReq with unknown option type=0x06, length=0xFF,
 *   but only 1 data byte, then m_pkthdr.len is set to sizeof(hdr)+255=257,
 *   while the actual mbuf data is only ~6 bytes.
 *
 *   The inflated mbuf is echoed in ConfigRsp -> ng_l2cap_lp_send uses
 *   pkthdr.len as the on-air L2CAP+ACL length -> ng_ubt usbd_m_copy_in
 *   reads past mbuf chain end -> kernel heap leaked over Bluetooth to attacker.
 *
 * Since netgraph7 is not built on master, this harness demonstrates the
 * pkthdr.len inflation logic in userspace.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

/* Simplified L2CAP config option header */
struct l2cap_opt_hdr {
    uint8_t  type;
    uint8_t  length;    /* attacker-controlled, up to 255 */
} __attribute__((packed));

struct l2cap_cfg_hdr {
    uint16_t dcid;
    uint16_t flags;
} __attribute__((packed));

/* Replicates the vulnerable path from ng_l2cap_evnt.c */
static int process_cfg_req(uint8_t *mbuf_data, int actual_data_len) {
    int pkthdr_len = actual_data_len; /* initial correct value */
    int off = sizeof(struct l2cap_cfg_hdr);

    printf("  Initial pkthdr.len = %d (matches actual data)\n", pkthdr_len);

    /* Parse options */
    while (off + sizeof(struct l2cap_opt_hdr) <= actual_data_len) {
        struct l2cap_opt_hdr *hdr = (struct l2cap_opt_hdr *)(mbuf_data + off);
        int hint = hdr->type & 0x80;
        uint8_t type = hdr->type & 0x7f;

        printf("  Option at off=%d: type=0x%02x length=%d hint=%d\n",
               off, type, hdr->length, hint ? 1 : 0);

        switch (type) {
        case 1: /* MTU */
        case 2: /* FLUSH */
        case 3: /* QOS */
            printf("    -> known option, validates hdr->length <= remaining len\n");
            off += sizeof(*hdr) + hdr->length;
            break;
        default:
            if (hint) {
                printf("    -> unknown hint option, skip\n");
                off += sizeof(*hdr) + hdr->length;
            } else {
                printf("    -> unknown non-hint option, return -3\n");
                /* BUG: no validation of hdr->length vs actual data */
                goto unknown_option;
            }
            break;
        }
    }
    return 0;

unknown_option:
    /* Replicates ng_l2cap_evnt.c:599-600 */
    printf("\n  [BUG] m_adj(m, off - sizeof(hdr))\n");
    printf("  [BUG] m->m_pkthdr.len = sizeof(hdr) + hdr->length\n");
    /* sizeof(l2cap_cfg_hdr) = 4, hdr->length = 255 */
    pkthdr_len = sizeof(struct l2cap_cfg_hdr) + 255;
    printf("  -> pkthdr.len set to %d, but actual data is only %d bytes!\n",
           pkthdr_len, actual_data_len);
    printf("  -> INFLATED by %d bytes\n", pkthdr_len - actual_data_len);
    return pkthdr_len;
}

int main(void) {
    printf("=== DF-0508: ng_l2cap ConfigReq unknown-option pkthdr.len inflation ===\n\n");
    printf("NOTE: netgraph7 is NOT compiled on DragonFlyBSD master.\n");
    printf("      Bug confirmed by source trace; harness demonstrates the logic.\n\n");

    /* Craft a ConfigReq: 4-byte cfg header + unknown option type=0x06, length=0xFF, 1 data byte */
    uint8_t pkt[16];
    memset(pkt, 0, sizeof(pkt));

    struct l2cap_cfg_hdr *cfg = (struct l2cap_cfg_hdr *)pkt;
    cfg->dcid = 0x0040;
    cfg->flags = 0;

    int off = sizeof(struct l2cap_cfg_hdr);
    struct l2cap_opt_hdr *opt = (struct l2cap_opt_hdr *)(pkt + off);
    opt->type = 0x06;     /* unknown non-hint */
    opt->length = 0xFF;   /* inflated — claims 255 bytes of data */
    /* But only 1 byte of actual data follows */
    pkt[off + 2] = 0x41;

    int actual_data_len = off + 3; /* header + opt header + 1 byte */
    printf("Crafted ConfigReq: actual data = %d bytes\n", actual_data_len);
    printf("Unknown option claims length=255 but only 1 byte present\n\n");

    int result = process_cfg_req(pkt, actual_data_len);

    if (result > actual_data_len) {
        printf("\n  BUG CONFIRMED: pkthdr.len (%d) > actual data (%d)\n",
               result, actual_data_len);
        printf("  When this mbuf is echoed in ConfigRsp:\n");
        printf("    -> ng_l2cap_lp_send sets on-air length from pkthdr.len\n");
        printf("    -> usbd_m_copy_in reads %d bytes past mbuf chain end\n",
               result - actual_data_len);
        printf("    -> kernel heap leaked to attacker via Bluetooth\n");
    }

    return 0;
}