In our iOS Kernel Internals for Security Researchers training at offensive_con we let our trainees look at some code that Apple introduced to the kernel in iOS 10. This code implements a new sysctl handler for the kernel.backtrace sysctl. This sysctl is meant to retrieve the current thread's user level backtrace. The idea behind this exercise is to see if the trainees can spot a 0-day information leak vulnerability in the iOS kernel if they are already pointed into the right direction.
The kernel.backtrace is a relatively new addition to the iOS kernel that let's the current process retrieve its own user level backtrace. While the logic of determining the user level's backtrace is somewhere buried in the Mach part of the kernel source code the sysctl handler itself is implemented in the file /bsd/kern/kern_backtrace.c. The code for the handler is shown below.
48 static int 49 backtrace_sysctl SYSCTL_HANDLER_ARGS 50 { 51 #pragma unused(oidp, arg2) 52 uintptr_t *bt; 53 uint32_t bt_len, bt_filled; 54 uintptr_t type = (uintptr_t)arg1; 55 bool user_64; 56 int err = 0; 57 58 if (type != BACKTRACE_USER) { 59 return EINVAL; 60 } 61 62 if (req->oldptr == USER_ADDR_NULL || req->oldlen == 0) { 63 return EFAULT; 64 } 65 66 bt_len = req->oldlen > MAX_BACKTRACE ? MAX_BACKTRACE : req->oldlen; 67 bt = kalloc(sizeof(uintptr_t) * bt_len); 68 if (!bt) { 69 return ENOBUFS; 70 } 71 72 err = backtrace_user(bt, bt_len, &bt_filled, &user_64); 73 if (err) { 74 goto out; 75 } 76 77 err = copyout(bt, req->oldptr, bt_filled * sizeof(uint64_t)); 78 if (err) { 79 goto out; 80 } 81 req->oldidx = bt_filled; 82 83 out: 84 kfree(bt, sizeof(uintptr_t) * bt_len); 85 return err; 86 }
The code above will first validated the incoming arguments and limit the depth of the backtrace that can be retrieved (lines 58-66). It will then allocated a heap buffer to store a backtrace of the user selected depth in line 67 and use an external helper function to fill the buffer with the user level backtrace (line 72). The actually retrieved backtrace is then copied to user land (line 77) and the heap buffer is released (line 84).
Before reading on further I suggest that you take a look at the code above again and try to spot the vulnerability yourself without help. Only one hint should be given: the vulnerability can only be exploited in older iOS/watchOS/tvOS devices.
Please do not read further before you have given yourself a chance to spot the vulnerability.
I am serious! Please try to first spot the vulnerability yourself.
The fact that you are reading this means you either ignored the three warnings above or you have already looked at the code yourself and either spotted the vulnerability or you have given up after a reasonable amount of time looking at the code. So let us figure out the problem together. Let us have a look at the line that copies the backtrace to user land.
77 err = copyout(bt, req->oldptr, bt_filled * sizeof(uint64_t)); 78 if (err) { 79 goto out; 80 }
As you can see the amount of bytes copied to user land is bt_filled * sizeof(uint64_t). This is the number of filled out backtrace entries times 8 bytes. And now let us have a look at how big the heap buffer is that we are dealing with.
67 bt = kalloc(sizeof(uintptr_t) * bt_len); 68 if (!bt) { 69 return ENOBUFS; 70 }
We kann see here that the size of the heap buffer is determined by the formula sizeof(uintptr_t) \* bt_len. This is the number of maximally retrieved backtrace entries times the size of a pointer. And this is were our previous hint kicks in: The size of a pointer is only 8 on recent devices. Older iOS devices (iPhone 5c and below) and older Apple Watches (Series 3) are internally 32 bit devices and therefore have only 4 byte pointers. This means on these older devices the call to copyout() will allow to copy twice the size of bytes from the heap than the buffer size is. This is a classic heap buffer overread vulneability.
As pointed out this is a 0-day kernel information leak vulnerability that has not been shared with Apple before now and therefore it is still unfixed in the kernel. However there are a number of mitigating factors:
- the vulnerability affects only 32 bit iOS devices - the only devices Apple still supports in current releases that are 32 bit are Apple Watch Series 3 and below
- the vulnerability can only be triggered outside of the app sandbox - so it can only be used as part as a vulnerability chain and not exploited directly from an app
Starting with iOS 12 Apple has added a mitigation to the kernel that checks whenever copyin() or copyout() are executed if the kernel's heap buffer has the necessary size for the operation to continue. The kernel will panic if an attacker tries to read or write accross the boundary of a kernel zone heap element.
However this mitigation does not stop an attacker from exploiting this vulnerability because Apple did not add this protection to 32 bit kernels. It is unknown if they simply forgot to protect their remaining 32 bit devices or if they simply do not care about them at all anymore.
The value of this security vulnerability in the eyes of Apple's security bounty program is exactly 0 USD. There are three reasons for this:
- Apple only pays for vulnerabilities affecting the latest of their devices. It doesn't matter if they officially still support older devices by providing them updates. They will only pay if the bugs affect the recent devices.
- Apple does not pay for vulnerabilities that affect MacOS / tvOS / WatchOS. Only if iOS devices are affected they might pay.
- Apple does not pay for information leak vulnerabilities although many of their mitigations rely on kernel memory being kept confidential.
This vulnerability is one of those things that are hard to explain. It is in relatively new code, so we would assume that kernel developers these days should be careful when writing new code. So it is rather a mystery why for the allocation and for copying the data two different data types are used. It is furthermore hard to explain why a security review of the new kernel code that should happen everytime new code is added did not spot this. The use of two different data types for allocation and copying is pretty obvious and trainees at offensive_con that were just learning about the kernel were pretty fast in seeing the problem.
If you are interested in this kind of content please consider signing up for one of our upcoming trainings.
Stefan Esser