ASLR & the iOS Kernel — How virtual address spaces are randomised
By: Billy Ellis
Tags: Aslr, Reverse Engineering, iOS, Security, Hacking

The iOS kernel is based on XNU, and XNU is open source. This makes it fairly easy to look into how parts of the iOS kernel work as we can refer directly to the source code.

The iOS kernel isn’t a direct result of compiling XNU however. Apple adds new, iOS-specific, code to XNU and these parts are kept closed source, although referring to the XNU source code is generally still very useful for getting a high-level understanding of parts of the iOS kernel code base.

The function load_machfile() in bsd/kern/mach_loader.c in XNU is responsible for parsing a given Mach-O file (the executable format used on iOS) and setting up a new task, loading it into memory etc. Every time you open an app or run a binary on your iPhone, this function is called in the kernel.

It’s in this function that the ASLR slide for the new process is generated.

In load_machfile() , after the initial setup of creating a new pmap and vm_map for the to-be process, we reach the code responsible for generating the ASLR slide.

There’s actually two ASLR slide values being generated here — one for the new process and one for dyld. Both are generated in the same way.

Firstly, a random 32-bit value is generated by the call to random() .

Secondly, this random value is ‘trimmed’ down so that it does not exceed the maximum slide value. On 32-bit, this value gets trimmed down to just one byte. On 64-bit it’s two.

Thirdly, the trimmed value is shifted left by an amount depending on the host’s page size.

The resulting value is the ASLR slide.

For example:

random() returns 0x3910fb29

2. Value 0x3910fb29 is trimmed down to 0x29 (assuming 32-bit OS)

3. Byte 0x29 is shifted left by 0x12 (the page shift amount)

The resulting 0x29000 is the ASLR slide value that will be used for this new process.

Shortly after the slide values are generated, load_machfile() calls into another function — parse_machfile() . This function is much larger and proceeds to load each Mach-O segment into the virtual memory space, perform code-signing-related tasks and ultimately launch the new process.

To see ASLR in effect, compile the following program on your iOS device.

#include <stdio.h>

#include <string.h> char *str = "HELLO ASLR"; int main() { printf("ptr %p

", str); return 0;

}

This code prints a pointer (using the %p format specifier) to a static char array stored in the binary.

Compile this code using the -fpie flag. This flag enables the code to be ‘position independent’ — essentially meaning it supports address space randomisation.

This should actually be enabled automatically and, in fact, 64-bit iOS enforces it. If you’ve ever dealt with ASLR on 64-bit iOS, you may have noticed that compiling a binary with -fno-pie (to disable position independent code) has no effect — all processes are launched with ASLR regardless.

If you run the above program a few times, you’ll notice that the pointer to the static char array changes on each new execution. This is due to a new slide value being generated by the kernel each time.

You may also notice that the three right-most hexadecimal digits remain the same — this makes sense as we know from the kernel code that the format of the slide value will always be 0xXX000 .

Patching the iOS kernel

A nice experiment you can try given the knowledge above is to actually patch the iOS kernel so that ASLR is disabled system wide.

This is fairly straightforward to do — only a single instruction needs to be changed in the kernel in order for the ASLR slide to always be set to 0x0.

With a patch like this applied, all apps and processes executed (even those compiled with -fpie ) will be mapped into memory at their static binary mapping.

This can even be potentially useful for other debugging/reversing tasks on iOS — having ASLR disabled at the kernel level can save you time having to recompile programs with the -fno-pie flag, or having to modify AppStore apps in order to run them without ASLR.

Unfortunately, due to KPP/KTRR on 64-bit iOS preventing us from writing to the __TEXT segment (the code section) of the kernel, we’ll be limited to using a 32-bit jailbroken device for this exercise.

You could apply the same patch in a static 64-bit kernel cache and boot the custom kernel on a device using an exploit like checkm8. But in this blog I’ll stick to patching it dynamically on 32-bit iOS 9.0 on my iPhone 5C.

Yeah, I guess 32-bit iOS is a bit redundant these days, but oh well. It’s still a cool little experiment to try out.