Phoenix JB Questions

Questions and Answers about all things *OS (macOS, iOS, tvOS, watchOS)

Phoenix JB Questions

Postby GoBlueDev » Tue Aug 22, 2017 12:42 pm

Hi all, went through the writeup on Phoenix (very good!) and looking to take a deep dive into learning it by attempting to implement it myself. Had a couple questions for anyone that has looked at it extensively.

1.) For the leak, it makes sense how the kernel text address at index 9 is identified, but I am not sure how the kalloc zone leak at offset 4 is identified as kalloc.384?
2.) Looking at the disassembly, I see the fake ipc_port_t being passed into a the "copyinPort" port functions. I then see a buffer filled with 384 A's and the fake port copied in twice but starting at offset 4 of the buffer holding the 384 A's. Was not sure if I am reading the disassembly right? If I am why begin the fake port copy at that offset instead of offset 0?

Any insight is much appreciated. Thanks!
GoBlueDev
 
Posts: 4
Joined: Tue Aug 22, 2017 12:26 pm

Re: Phoenix JB Questions

Postby morpheus » Tue Aug 22, 2017 10:29 pm

a) kalloc.384 is because that's the size of the Mach msg. The contents of kernel memory you are seeing (and I demonstrated in the output) are the arguments to the iokit call.

b) haven't seen that particular disassembly - Siguza can probably weigh in on this (after all, he wrote it). Remember you have to account for mach_msg headers and such.
morpheus
Site Admin
 
Posts: 532
Joined: Thu Apr 11, 2013 6:24 pm

Re: Phoenix JB Questions

Postby Siguza » Thu Aug 24, 2017 3:33 pm

2) Because we need those 384 bytes for two data structures: Once for the ipc_port_t, and once for the task_t.
We leak kernel memory in the form of (semantically speaking):
Code: Select all
((task_t)(port->ip_kobject))->bsd_info->p_pid
Going backwards from there, our bsd_info isn't even a constructed object, but just points to 8 bytes before the data we intend to read. To get there we need two dereferences though: For one we have a fake port, which we construct in full on those 384 bytes (which is critical, because you get a straight-up panic if the lock isn't valid). But then we also need an address for ip_kobject... and the only controllable kernel addresses we know are on that 384-byte block. But since a task struct is much larger than 384 bytes anyway (somewhere above 1300 IIRC) and since luckily pid_for_task doesn't attempt to lock the task, we only store a single additional pointer in the 384 buffer and point ip_kobject to that address minus the offset that the bsd_info pointer has in task_t.
User avatar
Siguza
Unicorn
 
Posts: 159
Joined: Thu Jan 28, 2016 10:38 am

Re: Phoenix JB Questions

Postby GoBlueDev » Sat Aug 26, 2017 5:57 pm

Thanks for the clarification! I think I got my implementation mostly there, though I am getting a kernel panic when calling pid_for_task. Think I will need to tweak my zone groom. I noticed that your released some portions of Phoenix in your PhoenixNonce repo. Any plans to release more of the Phoenix source in the near term?
GoBlueDev
 
Posts: 4
Joined: Tue Aug 22, 2017 12:26 pm

Re: Phoenix JB Questions

Postby Siguza » Sat Aug 26, 2017 10:38 pm

Nope. Thing is, PhœnixNonce only contains code written by me and tihmstar, plus some open source bits. Phœnix on the other hand contains quite an amount of 3rd party closed-source code...
User avatar
Siguza
Unicorn
 
Posts: 159
Joined: Thu Jan 28, 2016 10:38 am

Re: Phoenix JB Questions

Postby GoBlueDev » Fri Sep 01, 2017 1:48 pm

Having issues with my heap groom and tracked it down to spraying the wrong pointer, but I am curious if you can clarify something for me.

Here are values from my most recent debugging using ios-kern-utils:

My zone leak pointer is 0xc5dc297c and here is the memory for that region
reading 384 bytes from 0xc5dc297c
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 00 80 01 F0 C5 60 85 AF A7 00 69 3A A8

This is somewhere near the middle of the 384 byte block
I then noticed that the leaked pointer returned by copyinPort() has 0x78 subtracted which is the size of the fake port struct

dumping memory at 0xc5dc297c - 0x78 shows
02 00 00 80 64 00 00 00 00 00 00 00 00 00 00 00
11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 09 03 00 00 00 00 00 00 00 00 78 56 34 12
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
63 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 00 80 01 F0 C5

This lines up to the correct spray data. And 0xc5dc297c - 0x78 - 0x4 is the buffer that is being sprayed in.

So, I assumed the zone leaked pointer would point to the beginning of a 384 byte block which is where the spray would land. How did you know that you would have to subtract the size of the fake port from this address? Would not have been able to figure this out if I could not dump these sections of kernel memory...
GoBlueDev
 
Posts: 4
Joined: Tue Aug 22, 2017 12:26 pm

Re: Phoenix JB Questions

Postby Siguza » Fri Sep 01, 2017 4:36 pm

Oh, that's an obscure story. The relevant bits are in the functions ipc_kmsg_alloc, ipc_kmsg_copyin_body and ipc_kmsg_copyout_body, and the macro ikm_set_header.

Starting from the beginning:
All data belonging to one mach message is store in one single block of memory (that kalloc.384 allocation in this case). First you have a kmsg header, which serves mostly as bookkeeping data to the kernel. Consumers of mach messages never see those bits. Then after that you have the actual mach message with its header, body and trailer. So far so good.
Where it gets obscure is with the feature of out-of-line descriptors. Those are embedded in the mach message itself so they directly affect its size, however they contain a pointer, which means their size is not the same for 32- and 64-bit. Now, if the source task is 32-bit and either the kernel or the target task (if the message isn't targeted at the kernel) is 64-bit, then the message will come in with a 32-bit pointer and will have to be expanded for a 64-bit one to fit in. The code allocating the kmsg has no idea what it will contain though, so it simply makes the most protective assumption, namely that the message will come from a 32-bit task and will have to be expanded for 64-bits. (Note that it does that even if the kernel is 32-bit. On ARM, 64-bit userland isn't a thing if EL1 is 32-bit, but some older versions of macOS actually had a 64-bit userland with a 32-bit kernel, so that isn't entirely uncalled for.) This is ipc_kmsg_alloc, in its entirety - the LP64 comment is rather informative:

Code: Select all
ipc_kmsg_t ipc_kmsg_alloc(mach_msg_size_t msg_and_trailer_size)
{
   mach_msg_size_t max_expanded_size;
   ipc_kmsg_t kmsg;

   /*
    * LP64support -
    * Pad the allocation in case we need to expand the
    * message descrptors for user spaces with pointers larger than
    * the kernel's own, or vice versa.  We don't know how many descriptors
    * there are yet, so just assume the whole body could be
    * descriptors (if there could be any at all).
    *
    * The expansion space is left in front of the header,
    * because it is easier to pull the header and descriptors
    * forward as we process them than it is to push all the
    * data backwards.
    */
   mach_msg_size_t size = msg_and_trailer_size - MAX_TRAILER_SIZE;

   /* compare against implementation upper limit for the body */
   if (size > ipc_kmsg_max_body_space)
      return IKM_NULL;

   if (size > sizeof(mach_msg_base_t)) {
      mach_msg_size_t max_desc = (mach_msg_size_t)(((size - sizeof(mach_msg_base_t)) /
                       sizeof(mach_msg_ool_descriptor32_t)) *
                       DESC_SIZE_ADJUSTMENT);

      /* make sure expansion won't cause wrap */
      if (msg_and_trailer_size > MACH_MSG_SIZE_MAX - max_desc)
         return IKM_NULL;

      max_expanded_size = msg_and_trailer_size + max_desc;
   } else
     max_expanded_size = msg_and_trailer_size;

   if (max_expanded_size < IKM_SAVED_MSG_SIZE)
      max_expanded_size = IKM_SAVED_MSG_SIZE;    /* round up for ikm_cache */

   if (max_expanded_size == IKM_SAVED_MSG_SIZE) {
      struct ikm_cache   *cache;
      unsigned int      i;

      disable_preemption();
      cache = &PROCESSOR_DATA(current_processor(), ikm_cache);
      if ((i = cache->avail) > 0) {
         assert(i <= IKM_STASH);
         kmsg = cache->entries[--i];
         cache->avail = i;
         enable_preemption();
         ikm_check_init(kmsg, max_expanded_size);
         ikm_set_header(kmsg, msg_and_trailer_size);
         return (kmsg);
      }
      enable_preemption();
      kmsg = (ipc_kmsg_t)zalloc(ipc_kmsg_zone);
   } else {
      kmsg = (ipc_kmsg_t)kalloc(ikm_plus_overhead(max_expanded_size));
   }

   if (kmsg != IKM_NULL) {
      ikm_init(kmsg, max_expanded_size);
      ikm_set_header(kmsg, msg_and_trailer_size);
   }

   return(kmsg);
}

Close to the bottom you see a call to ikm_set_header, which is a macro defined as:

Code: Select all
#define ikm_set_header(kmsg, mtsize)               \
MACRO_BEGIN                        \
   (kmsg)->ikm_header = (mach_msg_header_t *)          \
   ((vm_offset_t)((kmsg) + 1) + (kmsg)->ikm_size - (mtsize));   \
MACRO_END

So inside the kmsg, the mach message is aligned to the end rather than the beginning, and initially has only the size that was specified from userland.
Now in ipc_kmsg_copyin_body you see:

Code: Select all
dsc_count = body->msgh_descriptor_count;
if (dsc_count == 0)
    return MACH_MSG_SUCCESS;

// ...

/* Shift the mach_msg_base_t down to make room for dsc_count*16bytes of descriptors */
if(descriptor_size != 16*dsc_count) {
    vm_offset_t dsc_adjust = 16*dsc_count - descriptor_size;

    memmove((char *)(((vm_offset_t)kmsg->ikm_header) - dsc_adjust), kmsg->ikm_header, sizeof(mach_msg_base_t));
    kmsg->ikm_header = (mach_msg_header_t *)((vm_offset_t)kmsg->ikm_header - dsc_adjust);

    /* Update the message size for the larger in-kernel representation */
    kmsg->ikm_header->msgh_size += (mach_msg_size_t)dsc_adjust;
}

And in ipc_kmsg_copyout_body you see:

Code: Select all
if(user_dsc != kern_dsc) {
    vm_offset_t dsc_adjust = (vm_offset_t)user_dsc - (vm_offset_t)kern_dsc;
    memmove((char *)((vm_offset_t)kmsg->ikm_header + dsc_adjust), kmsg->ikm_header, sizeof(mach_msg_base_t));
    kmsg->ikm_header = (mach_msg_header_t *)((vm_offset_t)kmsg->ikm_header + dsc_adjust);
    /* Update the message size for the smaller user representation */
    kmsg->ikm_header->msgh_size -= (mach_msg_size_t)dsc_adjust;
}

So the mach message header is pulled back if needed, and otherwise it is just left where it is.

Now, I don't feel like breaking up the math for you, but you get the idea. The allocation is made protectively large, the message is placed at the end by default, and unless it's crammed full with ool descriptors, it stays that way. And since the pointer we leak is to the buffer containing the property name, which lives in the mach message body, we have to subtract from it the size of the mach message header, the size of the kmsg header and the amount of memory that would've been used if the message consisted only of ool descriptors that had to be expanded from 32- to 64-bit.

Also since iOS 11 is putting the nail in the coffin for 32-bit, I wonder if we'll see this get "optimised away" on mobile some day. :P
User avatar
Siguza
Unicorn
 
Posts: 159
Joined: Thu Jan 28, 2016 10:38 am


Return to Questions and Answers

Who is online

Users browsing this forum: No registered users and 1 guest

cron