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.
1 2 3 4 5 6 7 8 9 10 11 | |
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.
1 2 3 4 5 6 7 8 9 10 | |
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.
1
| |
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 | |
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 | |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
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.
1
| |
1 2 3 4 5 6 7 8 9 | |
Using the calculated slide value it is now possible to calculate the correct location of nsysent using the following code:
1 2 3 4 5 6 7 | |
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:
1 2 3 4 | |
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.