In January, Barak Gabai of the X-Force Application Security Research Team discovered an interesting information leak vulnerability in iOS IOKit IOMobileFramebuffer (CVE-2015-1097), which can be used to defeat the kernel address obfuscation mechanism available since iOS 6. The vulnerability was disclosed to Apple and has been fixed in iOS 8.3.
In this blog post, I will provide a brief overview of the kernel address obfuscation technique employed by iOS and show how Gabai’s vulnerability renders it ineffective, as is the case with similar memory disclosures. I will also briefly touch on what I believe makes this particular vulnerability uniquely interesting.
The iOS kernel address obfuscation is excellently described in full in the “mach_port_kobject() and the kernel address obfuscation” blog post by Stefan Esser. Readers unfamiliar with this security mitigation mechanism and techniques for defeating it are encouraged to first read Esser’s post.
Background
On iOS, the kernel address space is randomized. C++ objects on the kernel heap are juicy targets for exploiters, and they can often be used for arbitrary code execution.
In some circumstances, the kernel — such as via mach_port_kobject() prior to iOS 8.1.3 — can provide an address of a kernel object to a user-space caller. Should the address of these objects be provided directly to user-space, exploitation would be assisted because an attacker — such as a malware author or jailbreak developer — could know where exploitable heap objects were situated in the kernel heap. Therefore, in recent versions of the kernel, these addresses are not passed to user-space in the clear and are instead obfuscated.
In order to perform this obfuscation of kernel-space addresses prior to being exposed to user-space, the addresses are passed through the VM_KERNEL_ADDRPERM(addr) macro, which is defined as follows:
#define VM_KERNEL_ADDRPERM(_v) \
(((vm_offset_t)(_v) == 0) ? \
(vm_offset_t)(0) : \
(vm_offset_t)(_v) + vm_kernel_addrperm)
As we can see, the code takes the kernel-space address and adds a vm_kernel_addrperm value to it. This permutation constant is a random value that is generated on boot. Prior work has been published on the strength of this permutation constant, and I’m not going to go into how the vm_kernel_addrperm value is generated in this blog post. However, for the purposes of this discussion, let’s assume Apple has managed to generate a permutation constant of sufficient entropy.
If all the above worked as it should, it would be difficult to attack the interesting objects situated on the kernel heap. However, in order to defeat this mechanism, we can make use of the invertible nature of the obfuscation function in order to deobfuscate the kernel address. The kernel is able to do so because it already knows vm_kernel_addrperm. So if we would like to do the same, we would ideally like to discover vm_kernel_addrperm.
For the function itself, we can see the following:
vm_kernel_addrperm = addr_obfuscated - addr_clear
So if we manage to find a leak of addr_clear (or any address from which we can reliably calculate the offset), we can then render the entire obfuscation mechanism useless. Such leaks have been demonstrated in the past.
The Vulnerability
Gabai discovered that the (unobfuscated) address of the IOMobileFramebuffer object on the kernel heap is inadvertently being leaked to user-space by a member function. In my opinion, what makes this particular leak interesting is the mechanism of the leak itself.
The vulnerability can basically be summarized as follows: The ‘get_framebuffer_id’ method (selector 7) of IOMobileFramebufferUserClient calls a member method of IOMobileFramebuffer and passes the return value of the call to user-space.
In the standard ARM calling convention, the R0 register is used both to pass the first argument to the function and to hold the return value on the function return.
In our case, the first argument (placed in R0) is the pointer of the IOMobileFramebuffer object itself. In this instance, the called method is, in fact, ’empty’ — a nullsub. Therefore, as the caller expects the return value to be in R0 — and the nullsub simply doesn’t touch R0 — the return value is the persisted value of R0, the object pointer. This is then further persisted to user-space, and hence, we have our information leak.
How Can These Nullsubs Occur?
The most likely reason in the case of C++ is that an empty virtual function of a base class has been called instead of the derived class function. In such a case, the return register, as defined by the calling convention, will persist in its value. In C++, the first argument is the pointer of the object itself (the ‘this’ pointer). Depending on the calling convention, such as in the case of the standard ARM calling convention, it’s even possible that this object pointer is what will be present in the return register, then the function returns.
Another reason could be that code may have been #if{,def}’d out. This pattern is sometimes seen with debug code, which is to be removed in release builds. In such a case, it is possible that compiler optimizations could remove the function altogether. Based on some minor experimentation I performed with both GCC and Clang at varying optimization levels, it’s entirely possible nullsubs could be left in the code. Also, by default, GCC doesn’t warn about control reaching the end of a non-void function, whereas Clang does.
Any instance where control from a non-void function can return without a return value is potentially dangerous, and developers should take care to eliminate such instances from their code.
While developers need to be aware of how the code looks in a high-level language, they should think about what’s happening under the hood, as well.
Security Researcher, X-Force Application Security Research Team, IBM Security Systems