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.