About the “tpwn” Local Privilege Escalation

Note: “Attacking XNU For Fun And Profit” blog posts are suspended indefinitely. I’ll talk about the post-10.11 world in a talk at BlackHat AMS 2015.

This post had to come out two weeks ago, but due to school exams I haven’t had the time to write this down.

So, tpwn gains root on any OS X box running a system <= 10.10.5 by gaining knowledge of the kernel address space layout randomisation slide followed by kernel code execution kickstarting a stack pivot which allows me to control the stack pointer of a kernel thread belonging to a controlled task.

Missing Checks

is_io_service_open_extended does not check whether the task_t* pointer passed by the MIG subsystem is NULL or not.

Our journey begins with IOServiceOpen. This is the definition of this IOKit function that allows you to “connect” to an IOUserClient.

kern_return_t IOServiceOpen ( io_service_t service, task_port_t owningTask, uint32_t type,io_connect_t *connect );

This function will use MIG to tell the kernel to call io_open_service_extended, like this (in osfmk/device/device.defs):

routine io_service_open_extended(
	    service		: io_object_t;
	in  owningTask		: task_t;
	in  connect_type	: uint32_t;
	in  ndr			: NDR_record_t;
	in  properties		: io_buf_ptr_t, physicalcopy;
        out result		: kern_return_t;
	out connection		: io_connect_t
	);

As you can see, from user land you pass in an io_service_t (mach_port_t), a task_port_t (mach_port_t) and an uint32_t, and kernel land gets an io_object_t*, a task_t*, and an uint32_t (along with some other stuff passed in by IOKitLib from userland).

The conversion between a mach port into the desired kernel type is handled by MIG like this (at least for the “task_t” type):

type task_t = mach_port_t
#if	KERNEL_SERVER
		intran: task_t convert_port_to_task(mach_port_t)
		outtran: mach_port_t convert_task_to_port(task_t)
		destructor: task_deallocate(task_t)
#endif	/* KERNEL_SERVER */
		;

 

Mach ports hold a “kobject” value which is a pointer to some data it references in-kernel. This kobject value is typed, which means that technically you cannot pass in a mach port with a certain kobject type to a function which expects a different kobject type.

Of course, convert_port_to_task is a good boy: if the kobject type is not IKOT_TASK, it returns NULL.

task_t
convert_port_to_task(ipc_port_t	port)
{
	task_t		task = TASK_NULL;
[...]
	if (ip_kotype(port) == IKOT_TASK) {
		task = (task_t)port->ip_kobject;
	}
[...]
	return (task);
}

 

So, if you call IOServiceOpen with a mach port whose kobject type is not IKOT_TASK, is_io_service_open_extended is called with a NULL ‘owningTask’ parameter, and before 10.11 “El Captain” is_io_service_open_extended did not perform any check on wether the task is NULL or not, allowing NULL to be passed down to whatever IOUserClient has been opened (this is bug 1, CVE-2015-????).

Mapping NULL

NULL is not a special pointer, although modern OSes will prevent you from mapping memory over there.

Mac OS X does this via a — __PAGEZERO segment in every MH_EXECUTE Mach-O.

This segment “reserves” memory in the NULL page (up to 0x100000000 on x86_64 systems) and makes it impossible to map. The kernel enforces this on OS X, but 32 bit binaries are not required to have a __PAGEZERO segment for compatibility reasons I believe.

Binaries lacking __PAGEZERO are fully free to map the NULL page, allowing us to fully control the data.

Memory Corruption

The following code will run when a new IOHDIXControllerUserClient is created via an is_io_service_open_extended call:

Screen Shot 2015-09-02 at 01.25.52

As you can see, it calls bsd_set_dependency_capable passing in the task value it gets from the caller without checking if it equals to NULL.

void bsd_set_dependency_capable(task_t task)
{
    proc_t p = get_bsdtask_info(task);
    if (p) {
        OSBitOrAtomic(P_DEPENDENCY_CAPABLE, &p->p_flag);
    }
}

It translates (on 10.10 XNU at least), into:

task->bsd_info->p_flag |= P_DEPENDENCY_CAPABLE;

Since “task” equals NULL, by mapping NULL in userland, this function will read from userland memory(!) a pointer (task->bsd_info), dereferences it (->p_flag), treating the resulting pointer as an integer which will be ORed ( |= ) to 0x00100000 (P_DEPENDENCY_CAPABLE). Since we have full control of the pointer, this can be considered an OR 0x10 primitive. This is done in kernel, which means you can potentially corrupt kernel memory!

You’re starting to get the picture?

Meaningful Corruption

But what do we corrupt, exactly? First of all, there’s kASLR. Then kernel __TEXT, which is R-X. Kernel __DATA is RW-, but e.g. syscalls are in the ‘const’ section. Furthermore, our corruption allows us to |= 0x10; any byte in kernel memory. Not exactly a write-what-where. Thankfully, if you paid attention to my last two blog posts attacking the heap is where all the fun is at these days. We’ll need just a couple things first: a pointer to something in the heap, and that something must be something that gets us some sort of meaningful undefined behaviour thanks to our corruption.

P.S. In “tpwn” I decided to perform multiple corruptions for the exploit, but this is un-needed, and it was done just because in this case it provided mathematically provable 100% reliability.

IOAudioEngineUserClient to the rescue!

Thankfully, IOAudioEngineUserClient has a solution for us! It exposes a very handy method to retrieve a “connection ID”.

IOReturn IOAudioEngineUserClient::getConnectionID(UInt32 *connectionID)
{
    audioDebugIOLog(3, "+-IOAudioEngineUserClient[%p]::getConnectionID(%p)\n", this, connectionID);

    *connectionID = (UInt32) (((UInt64)this >> 8) & 0x00000000FFFFFFFFLLU) ;
    return kIOReturnSuccess;
}

This ID is derived by the “this” pointer, but it is not sufficiently obfuscated. In fact, thanks to canonical addressing and memory alignment, it is not obfuscated at all.

Any kernel-land pointer is negative, and due to canonical addressing this means that the first 24 bits are known to be “0xFFFFFF”.

Any memory allocation in a zone bigger than 0x100 will have the last 8 bits known to be “0x00”.
IOAudioEngineUserClient fits in an allocation from the kalloc.1024 zone, so 0x400 bytes in size.

Interestingly enough, all other missing bits are our very own connection ID! Thanks IOAudioEngine! (this is bug 2, CVE-2015-????).

    // from tpwn/main.m's leak_heap_ptr
    uint64_t    scalarO_64=0;
    uint32_t    outputCount = 1;
    IOConnectCallScalarMethod(*co, 2, NULL, 0, &scalarO_64, &outputCount);
    if (!scalarO_64) {
        puts("failed infoleaking");
        exit(-20);
    }
    scalarO_64 <<= 8;
    scalarO_64 |=  0xffffff0000000000;

This is a C++ object residing in a RW- page, in an allocation from the kalloc.1024 zone! We can create allocations, free them and leak their pointers at will, which allows us to keep trying until we get two adjacent allocations. By freeing the first allocation and re-allocating a vm_map_copy structure into the newly poked hole, we now have a vm_map_copy adjacent to a C++ object, or more precisely, an IOAudioEngineUserClient object.

Give my “Attacking XNU For Fun And Profit Part 2” post a read.

Since we can control the size of our kernel allocation from userland, we can make sure that the |= 0x10 really means += 0x10 by making sure size AND 0x10 == 0, in turn allowing us to read the first 16 bytes of the adjacent allocation. The adjacent allocation, again, is a C++ object. First 8 bytes of said allocation is a pointer to the vtable of our object, which resides in a kernel extension’s __TEXT/const __DATA segment.

We can analyse the kernel extension’s symbol table to find out the pointer to the IOAudioEngineUserClient vtable, find out the unslid address of said kernel extension in kernel address space, add the two to find out the unslid address of said vtable. Since we also know the slid address (thanks to our vm_map_copy corruption), by subtracting the two we can find out the kaslr slide which allows us to know the address space layout and allows us to re-use code inside the kernel’s __TEXT, to bypass SMEP.

Gaining RIP Control

There are many ways to do this, but I decided to use a specific execution vector which gives us provable 100% reliability, and is specific to IOAudioEngineUserClient. Since we can corrupt memory at will and we know a pointer to an IOAudioEngineUserClient, by changing a NULL pointer to a C++ object into a 0x10 pointer to a C++ object, said object can be controlled. In our case, a static method will be called on said object, so hijacking the vtable is useless. However, said static method will release a reference to an object, and does so by calling object->release();, which is a vcall. By controlling the pointer to said object, you can make sure the call reads the vtable pointer from userland memory, allowing you to control two registers. One is the pointer to our fake vtable, usually stored in RAX, the other is RIP, which can just point to a stack pivot allowing you to set RSP = RAX, and run a small chain which transfers control to a full chain. My chain sets current task’s uid = 0, which gives the tpwn process root privileges. system(“/bin/sh”) spawns a shell.

Profit!

— Luca ‘qwertyoruiop’ Todesco for Kim Jong Cracks Research.

Shout out @jk9357 for the new coming soon KJC track!

12 thoughts on “About the “tpwn” Local Privilege Escalation

  1. I found your gwegpegmn weblog web site on google and examine just a few of your early posts. Continue to keep up the excellent operate. I simply additional up your RSS feed to my MSN Information Reader. Looking for ahead to reading extra from you in a while!…

  2. you are in reality a good webmaster. The site loading speed is amazing. It seems that you are doing any unique trick. Moreover, The contents are masterpiece. you’ve done a fantastic job on this matter!

  3. Saya tidak bahkan memahami bagaimana akhirnya aku up di
    sini, tapi saya pikir ini mengirimkan adalah baik.

    Saya tidak mengenali yang Anda tapi pasti Anda akan terkenal
    terkenal blogger ketika Anda tidak sudah. Cheers!

  4. Apakah Anda pernah berpikir tentang dianggap penerbitan sebuah ebook atau authoring
    tamu di lain situs? Aku punya blog berdasarkan pada yang sama informasi Anda membahas dan akan benar-benar seperti untuk memiliki
    Anda berbagi beberapa cerita / informasi. Saya tahu saya pemirsa akan menghargai pekerjaan Anda.
    Jika Anda bahkan jauh tertarik, jangan ragu untuk menembak saya sebuah e mail.

  5. We specialize in large trays for ot tomans.

    Our trays are handmade with care
    for the highest quality possible! We offer ottoman trays in a
    number of styles and finishes.
    We can also do any custom stain or any custom color you need.
    Square Trays – Any Size

    Octagon Trays – Great for round ottomans
    Rectangular Trays – Any Dimension for a perfect fit on your ottoman

    round ottoman tray,large round ottoman tray,extra large trays for ottomans,
    wood tray for ottoman tray,large ottoman tray,
    round tray for ottoman tray,ottoman coffee table tray,table tray ottoman,coffee table tray,
    ottoman trays large,large tray to put on ottoman,trays for ottomans,
    decorative trays for ottomans,tray for ottoman coffee table,coffee
    table tray ottoman
    ,black ottoman tray,large round tray,custom ottoman tray,round ottoman trays,
    tray for ottoman,extra large trays,ottoman tray,large trays,ottoman tray table,
    wooden tray for ottoman,large wooden ottoman tray,tray table for
    ottoman,
    large trays for ottomans,extra large tray for ottoman,large ottoman serving
    tray,
    large round tray for ottoman,trays for ottoman,large tray for ottoman,big tray for ottoman,
    large serving trays for ottomans,large decorative
    trayfor ottoman,large tray for coffee table,
    cocktail ottoman tray

Leave a Reply

Your email address will not be published. Required fields are marked *