Mac OS X El Capitan (10.11) and task_for_pid()

Overview

I might be partially illiterate, but documentation regarding OS X and memory manipulation of other processes is terrible.

The official documentation (there is none):
screenshot_doc
10/10 Documentation

So if you are an idiot, and can't understand the official documentation, here are some options:

  1. A blog post for 10.5 (Leopard) from 2010, which I used a fair amount.
  2. A blog post from 2016 which basically parrots the aforementioned post with some refinements.
  3. The Bit Slicer source code.
  4. An article from 2006 about ptrace.

And then there are a lot of links that express some painful ideas which are, at best, wrong, and, at worst, dangerous.

Myths

A caveat up-front: all this work is for hooking user-mode applications. If you are trying to hook the kernel, disregard these.

  1. If you disabled SIP, reenable it. You do not need to disable this for user-land hooking. To check, use csrutil status.
  2. Depending on your set-up, you do not necessarily need to sign your code, but it doesn't hurt anything.
    codesign not required

Target

First, we need an application to hook. A simple one for tests:

/*!
*    game.c: A game that detracts one and prints the score every time we hit enter.
*/
#include <stdio.h>

int main( int argc, char** argv )  
{
    int score = 9000;

    while( 1 )
    {
        getchar( );
        printf( "score: %d", --score );
    }

    return 0;
}

Compile with gcc game.c -o game.

Hook

task_for_pid's definition is straightforward:

kern_return_t task_for_pid(  
                mach_port_t parent, 
                int pid,
                mach_port_t *task_out ); 
  • parent should be the calling task and can be sastified by mach_task_self().
  • pid is the task you want to attach to.
  • task_out is a pointer to a mach_port struct that will hold the task port id.
  • task_for_pid returns a kernel code. The two you will generally see are KERN_SUCCESS( 0 ) and (os/kern) failure( 5 ).

An example call:

kern_return_t kern_return = 0;

mach_port_t task = 0;

long int pid = _some_pid;

kern_return = task_for_pid( mach_task_self(), pid, &task );  
if( kern_return != KERN_SUCCESS )  
{
    printf( "task_for_pid failed: %s\n", mach_error_string( kern_return ) );
    return 0;
}

Given this boiler, it's pretty easy to build a simple program that will read a pid from an argument and attach to that process:

/*!
*   task_for_pid.c: Given a pid in argv[ 1 ], return the mach task port.
*/
#include <stdio.h>
#include <stdlib.h>
#include <mach/mach.h>

int main( int argc, char** argv )  
{
    kern_return_t kern_return = 0;

    mach_port_t task = 0;

    long int pid = 0;

    char *endptr = NULL;

    if( argc < 2 ) 
    {
        return 0;
    }

    pid = strtol( argv[ 1 ], &endptr, 10 );

    kern_return = task_for_pid( mach_task_self(), pid, &task );
    if( kern_return != KERN_SUCCESS ) 
    {
        printf( "task_for_pid failed: %s\n", mach_error_string( kern_return ) );
        return 0;
    }

    printf( "%u\n", task );

    return 0;
}

The only difference between this and the previous example is that we use strtol to parse the first argument passed.

Compiling

For the kernel to allow us to use task_for_pid, we need to give our application the SecTaskAccess permission. To do this, create a file called Info.plist with the following content:

<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">  
<plist version="1.0">  
<dict>  
    <key>SecTaskAccess</key>
    <array>
        <string>allowed</string>
    </array>
</dict>  
</plist>  

When compiling, use -sectcreate to create a section for the plist:
gcc task_for_pid.c -sectcreate __TEXT __info_plist ./Info.plist -o task_for_pid

That's all that is required. Execute our target, find it's pid using ps and then run sudo ./task_for_pid _some_pid

example

task_for_pid failed: (os/kern) failure

So what happens if it fails? A couple things to try:

  1. Make sure you are running with root permissions.
  2. Some setups will require you to code-sign ./task_for_pid. If that is the case, create a new system certificate in the keychain and use it to sign via codesign -s hook-cert ./task_for_pid.

What's next?

With a mach task, you can do pretty much anything, including reading memory (mach_vm_read), writing memory (mach_vm_write), mapping memory (mach_vm_map), and scanning memory regions (mach_vm_region and mach_vm_region_recurse). As the kids say, your imagination is the limit.

Or your terrible coding skills, whichever you hit first.

A public gist containing the code is available here.

Update 2017/08/22

Due to security features in Mac OS X El Capitan, task_for_pid will correctly fill in task but will always output 3331.