Blended Cocoa

Adventures in Objective-C, Swift and Cocoa

Failing to Update the `pt_deny_attach` Kernel Module for Mountain Lion

Update 13th April 2013: This now works on Mountain Lion (10.8.3). Thanks to a tip, I have now worked around the problem of write-protected memory pages. See the GitHub repository for the working code.

Note: From the title of this post you will see that I ultimately failed to get the pt_deny_attach kernel module to work in Mountain Lion. This was due to write protected memory in the kernel. However, I did manage to work around the issues presented by Kernel Address Space Layout Randomisation (KASLR). The journey that I took to my ultimate failure may be useful and interesting to others.

I recently answered a question on Stack Overflow that led me to investigate the P_LNOATTACH process flag in Mountain Lion (OS X 10.8).

The P_LNOATTACH flag is a process flag which stops other processes (debuggers, DTrace etc) attaching to the flagged process. The flag is set by calling ptrace(PT_DENY_ATTACH). This feature was almost certainly added to OS X to stop/hinder reverse engineering the DRM used by iTunes. iTunes is certainly the oft-cited example of the use of PT_DENY_ATTACH.

In order to circumvent the protection provided by P_LNOATTACH a kernel module was created which intercepts the call to trace with the PT_DENY_ATTACH parameter. The most recent version of this module is for Lion (OS X 10.7.1) and is available on GitHub. The project page on GitHub says Snow Leopard but the code has been updated to Lion (10.7.1) as can be seen from the following code snippet from pt_deny_attach.c.

pt_deny_attach.cView on GitHub
1
2
3
4
5
6
7
8
9
10
11
/* This value is for OSX 10.7.1.  The exact _nsysent offset can be found
 * via:
 *
 *   nm -g /mach_kernel | grep _nsysent
 *
 * Due to a bug in the kext loading code, it's not currently possible
 * to link against com.apple.kernel to let the linker locate this.
 *
 * http://packetstorm.foofus.com/papers/attack/osx1061sysent.txt
 */
#define _NSYSENT_OSX_10_7_1_  0xffffff8000846eb8

The comment suggests a “bug in the kext loading code” stops you linking against com.apple.kernel. I believe this is intentional in order to make kernel hacks like this one more difficult.

The Lion module was based on an earlier version for Leopard. The above link provides additional background information about the ptrace flag and the kernel module.

The obvious first step to updating this kernel module to Mountain Lion was to download the version for Lion and load it into the kernel. This resulted in the first of many kernel panics. This was unsurprising as the layout of the kernel would almost certainly have changed between 10.7.1 (the version the module was targeted at) and 10.8.2 (the version I am currently running).

How does the Lion version work

Before we can start to update the kernel module to Mountain Lion we should really understand what the module is trying to do. Basically, the module is using pre-discovered offsets to find the sysent data structure which contains the function pointers to all of the syscall functions in the kernel. Once it has found the sysent table it changes the pointer to the ptrace function to point to a function, our_ptrace, supplied by the kernel module which checks to see if the parameter is PT_DENY_ATTACH.

pt_deny_attach.cView on GitHub
1
2
3
4
5
6
7
8
9
10
static int our_ptrace (struct proc *p, struct ptrace_args *uap, int *retval)
{

  if (uap->req == PT_DENY_ATTACH) {
      printf("[ptrace] Blocking PT_DENY_ATTACH for pid %d.\n", uap->pid);
      return (0);
  } else {
      return real_ptrace(p, uap, retval);
  }
}

If the parameter is PT_DENY_ATTACH it does nothing and returns success, return (0), otherwise it calls the original ptrace function passing the supplied parameters. This causes PT_DENY_ATTACH to be ignored but other calls to ptrace continue to function as expected.

In order to make this more difficult in Lion, Apple don’t export the location of the sysent table. In order to find the sysent table the module has the location of the nsysent variable hard-coded.

pt_deny_attach.cView on GitHub
1
#define _NSYSENT_OSX_10_7_1_  0xffffff8000846eb8

nsysent contains the number of entries in the sysent table and is stored close to the sysent table. As the comment above suggests, it is possible to find the memory address of the nsysent variable using the command nm -g /mach_kernel | grep _nsysent. On 10.8.2 this gives:

1
2
3
macbook-air:~ matt$ nm -g /mach_kernel | grep _nsysent
ffffff8000839818 D _nsysent
ffffff80008db150 S _nsysent_size_check

If you try to find the sysent table directly you will get results but not the address of sysent itself:

1
2
3
4
5
6
7
macbook-air:~ matt$ nm -g /mach_kernel | grep _sysent
ffffff80002ce010 T _hi64_sysenter
ffffff80002ce8f0 T _hndl_sysenter
ffffff80002ce010 T _idt64_sysenter
ffffff80008e0688 S _systrace_sysent
ffffff80002b5c30 T _x86_sysenter_arg_store_isvalid
ffffff80002b5c20 T _x86_toggle_sysenter_arg_store

So, having found the 10.8.2 location of nsysent the next obvious step is to update the module with the new location and reload it… kernel panic!

The kernel module makes an assumption about the location of the sysent table. It assumes that the sysent table is immediately before the nsysent variable. It uses the value of nsysent and the sizeof(struct sysent) to calculate the size of the sysent table, table_size, and then subtracts this from the memory address of nsysent to give a pointer to sysent in table.

pt_deny_attach.cView on GitHub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
 * nsysent is placed directly before the hidden sysent, so skip ahead
 * and sanity check that we've found the sysent array.
 *
 * Clearly, this is extremely fragile and not for general consumption.
 */
static struct sysent *find_sysent () {
  unsigned int table_size;
  struct sysent *table;

  table_size = sizeof(struct sysent) * *(_nsysent);
  table = (struct sysent *) ( ((char *) _nsysent) - table_size );

  printf("[ptrace] Found nsysent at %p (count %d), calculated sysent location %p.\n", _nsysent, *_nsysent, table);

Obviously some things have changed within the kernel. Simply updating the location of nsysent doesn’t fix the problem.

Getting it to work on Mountain Lion

After some experimentation (and many kernel panics) it became obvious that I hadn’t actually found the correct location of nsysent in the kernel address space. Simply trying to read the value of nsysent (which should contain a count of sys calls) was causing a kernel panic.

After some Internet research it became clear that the major difference between the Lion and Mountain Lion kernel was the introduction of Kernel Address Space Layout Randomisation (KASLR). When the kernel is loaded into memory KASLR causes the addresses of all the symbols (variables) to be randomised. The nm command shows the location of the nsysent variable before the randomisation has taken place. Simply changing the value of _NSYSENT_OSX_10_7_1_ is not going to work.

KASLR works by generating a random number and then sliding the symbol addresses in memory by this random value. Basically the kernel slide value is added to all the addresses in the kernel when it is loaded into memory. The value of the kernel slide can be accessed by a privileged process using a new syscall on OS X (this syscall was briefly available in iOS 6 betas but was removed before release). Check out kextstat_kaslr for more details.

The value of the slide is stored in the kernel in a variable called vm_kernel_slide, we can see this in the kernel source code. The kernel source is open source and available at http://opensource.apple.com/source/xnu/xnu-2050.18.24/.

Unfortunately, Apple have not exported this variable for use by kernel modules. We are also in a chicken and egg situation because vm_kernel_slide has also been moved by the kernel slide. Everything I tried failed to give me the value of the kernel slide. It is ironic that a user process running as root can access this value using the kas_info syscall but a kernel module running in the kernel doesn’t seem to be able to. (It is entirely possible that I missed something obvious at this point.)

After pondering the problem it occurred to me that the value of vm_kernel_slide can be inferred by a kernel module. We can use nm to get the address of an exported function in the kernel. Our kernel module can get the function pointer of a function in the running kernel. If we subtract one from the other we get the value of the kernel slide.

I updated the kernel module with code to do this. The module contains the hard-coded location of printf which was found using nm. The module then gets a pointer to the printf function and uses this to calculate the slide.

pt_deny_attach.c
1
#define _PRINTF_OSX_10_8_2_   0xffffff8000229090  // Used to calculate the KASLR slide
pt_deny_attach.c
1
2
3
4
5
6
7
8
9
/*
 * vm_kernel_slide doesn't appear to be available to kexts
 * but we can calculate it by getting the address of a known
 * function, e.g. printf, and then comparing that to the
 * address returned by nm -g /mach_kernel
 */
static vm_offset_t calculate_vm_kernel_slide(void) {
    return (vm_offset_t)&printf - _PRINTF_OSX_10_8_2_;
}

Using the calculated slide value it is now possible to calculate the correct location of nsysent using the following code:

pt_deny_attach.c
1
2
3
4
5
6
7
kern_return_t pt_deny_attach_start (kmod_info_t *ki, void *d) {

    slide = calculate_vm_kernel_slide();
    printf("[ptrace] KASLR kernel slide is 0x%lx\n", slide);

    _nsysent = (int *)(_NSYSENT_OSX_10_8_2_ + slide);

Reading the value of _nsysent at this point gave the correct value for the number of syscalls that Mountain Lion provides. Finally we are getting somewhere.

The next step is to find the sysent table based on the location of nsysent. This took a little bit of hunting as it has moved quite a way in Mountain Lion. I simply searched the memory around nsysent checking it using the sanity checks from the Lion version of the module. I eventually found it after a few more kernel panics with an offset of 0x1c028 from nsysent.

Having found the location of sysent it was easy to confirm that the location of the ptrace function in the syscall table matched the location returned by nm (plus the kernel slide).

Updating the kernel module and loading it into the kernel finally… caused a kernel panic.

This is were I am now stuck and I can’t see an easy way around the problem. As far as I can tell the sysent table is now located in a read-only memory page. Attempting to replace the value of the function pointer to ptrace with a pointer to our_ptrace causes a kernel panic.

The kernel source code declares the sysent table as follows:

init_sysent.c
1
2
3
4
__private_extern__ const struct sysent sysent[] = {
        {0, 0, 0, (sy_call_t *)nosys, NULL, NULL, _SYSCALL_RET_INT_T, 0},                           /* 0 = nosys indirect syscall */
        {AC(exit_args), 0, 0, (sy_call_t *)exit, munge_w, munge_d, _SYSCALL_RET_NONE, 4},           /* 1 = exit */
        {0, 0, 0, (sy_call_t *)fork, NULL, NULL, _SYSCALL_RET_INT_T, 0},                            /* 2 = fork */

I assume the fact it is declared const means that the table has been placed into a protected memory page. Feel free to correct me if I am wrong.

One last try

Given that it doesn’t seem to be possible to modify the sysent table I wondered if it would be possible to hot-patch the ptrace function itself. Unfortunately attempting to write to the memory associated with the function causes…

…yes, you guessed it, a kernel panic!

The source code for a kernel module that works up to the point of actually modifying the sysent table is available on GitHub at https://github.com/mttrb/pt_deny_attach.

Comments