At BlackHat Asia 2014, Ming-chieh Pan and Sung-ting Tsai presented about Mac OS X Rootkits (paper and slides). They describe some very cool techniques to access kernel memory in different ways than the usual ones. The slides and paper aren’t very descriptive about all the techniques so this weekend I decided to give it a try and replicate the described vulnerability to access kernel memory.
The access to kernel task (process 0) was possible before Leopard (or was it fixed in Snow Leopard? too lazy to check it now!), by using the function task_for_pid(0). This would retrieve the task port for the kernel and then we could use the mach_vm_read/write functions to fool around with kernel memory. It was pretty cool but a giant hole, even if it required root access to be used. The task_for_pid() function now has the following code to deny access to the kernel task (from 10.9.0 XNU source code):
/*
* Routine: task_for_pid
* Purpose:
* Get the task port for another "process", named by its
* process ID on the same host as "target_task".
*
* Only permitted to privileged processes, or processes
* with the same user ID.
*
* Note: if pid == 0, an error is return no matter who is calling.
*
* XXX This should be a BSD system call, not a Mach trap!!!
*/
kern_return_t
task_for_pid(
struct task_for_pid_args *args)
{
...
/* Always check if pid == 0 */
if (pid == 0) {
(void ) copyout((char *)&t1, task_addr, sizeof(mach_port_name_t));
AUDIT_MACH_SYSCALL_EXIT(KERN_FAILURE);
return(KERN_FAILURE);
}
...
}
So root or not, we can’t use this trick anymore to get the kernel task port. But Apple was so kind to leave a similar hole in other functions as the mentioned presentation shows. The function processor_set_tasks() lists all the tasks in the processor set. What is a processor set? Mac OS X and iOS Internals book describes it as “A processor set is a logically coupled group of processors and allows Mach to efficiently scale to SMP architectures by using the set as a container for related processors”. Essentially a XNU abstraction to scale to multiprocessors/multicores architectures. The interesting bit out of this function is that it returns the task port for all the tasks in the processor set, which in practice should mean all processes running in the system. This includes the kernel task due to XNU design, where the kernel is just another task in the system.
The vulnerability is very easy to use! We just set all the necessary ports to use processor_set_tasks, calls this function, and get the kernel task port in the element zero of the returned task_array_t of processor_set_tasks. After having the task port we can use the mach_vm_read and mach_vm_write functions to read and write from kernel memory, like it’s done for userland processes. The first argument for those functions is the task port, so as long we have a valid port we can do whatever we want with the kernel memory or any other process in the system (technically all the processes in the task list but there’s a one to one mapping between tasks and BSD processes in OS X).
Also a very fun detail is that this same vulnerability was perfectly described in Mac OS X and iOS Internals book for a long time on page 387. A screenshot of that page follows:
I totally missed the clue when I read it although my silly brain still remembered I read something about the processor sets in the book. The other funny detail about this is at the bottom of that page, where it says the vulnerability was fixed in iOS but left all this time in OS X (still unfixed in latest Mavericks update).
If you want to see it working you can check the checkidt util in Github repo. This is an updated version of an old port I did from a Phrack article three years ago. It tries to use this vulnerability to read from kernel memory before trying to use the /dev/kmem device (that needs to be manually configured in OS X). It is a very useful vulnerability to this kind of tools.
What’s the catch about all this? It still needs root access to work, which is not perfect from a rootkit point of view but also not a big obstacle (how many installers ask for admin privileges? too many!). task_for_pid(0) was fixed and it also required root privileges to work.
This is/was a nice bug, let it rest in peace and be useful while it lasts.
Have fun,
fG!