Home

Awesome

x18-leak

iOS 11.2 introduced a kernel information leak that could be used to determine the kASLR slide. The issue was the result of a newly added feature, __ARM_KERNEL_PROTECT__, that inadvertently caused the address of the kernel function Lel0_synchronous_vector_64_long to appear in register x18 when obtaining the values of a thread's registers using thread_get_state. The issue was discovered when kernel pointers started appearing in iOS application crash logs.

The vulnerability

In iOS 11.2, Apple introduced a feature on arm64 called __ARM_KERNEL_PROTECT__. According to a comment in osfmk/arm64/proc_reg.h:

__ARM_KERNEL_PROTECT__ is a feature intended to guard against potential
architectural or microarchitectural vulnerabilities that could allow cores to
read/access EL1-only mappings while in EL0 mode.  This is achieved by
removing as many mappings as possible when the core transitions to EL0 mode
from EL1 mode, and restoring those mappings when the core transitions to EL1
mode from EL0 mode.

That is, when transitioning from EL1 (kernel mode) to EL0 (user mode), as many kernel mappings as possible will be removed. This should limit the possible attack surface against kernel memory mappings when exploiting microarchitectural vulnerabilities like Spectre or Meltdown.

If you look through the diff between XNU versions 4570.20.62 and 4570.31.3, you'll find a number of new references to register x18 pop up in the file osfmk/arm64/locore.s in relation to __ARM_KERNEL_PROTECT__. In particular, you'll see that the exception vector Lel0_synchronous_vector_64, which is the exception vector invoked on a system call (instruction svc #0), now looks like this:

	.text
	.align 7
Lel0_synchronous_vector_64:
	MAP_KERNEL
	BRANCH_TO_KVA_VECTOR Lel0_synchronous_vector_64_long, 8

The macro BRANCH_TO_KVA_VECTOR is defined as:

.macro BRANCH_TO_KVA_VECTOR
#if __ARM_KERNEL_PROTECT__
	/*
	 * Find the kernelcache table for the exception vectors by accessing
	 * the per-CPU data.
	 */
	mrs		x18, TPIDR_EL1
	ldr		x18, [x18, ACT_CPUDATAP]
	ldr		x18, [x18, CPU_EXC_VECTORS]

	/*
	 * Get the handler for this exception and jump to it.
	 */
	ldr		x18, [x18, #($1 << 3)]
	br		x18
#else
	b		$0
#endif /* __ARM_KERNEL_PROTECT__ */
.endmacro

This macro performs an indirect branch to the true exception vector implementation, Lel0_synchronous_vector_64_long, by loading a pointer to that function into the register x18. Notice, however, that this clobber of x18 happens before the userspace registers are saved by the function fleh_dispatch64, which is called by Lel0_synchronous_vector_64_long. This means that when the user registers are saved, x18 will actually be a pointer to Lel0_synchronous_vector_64_long rather than the original value from userspace.

Even though x18 is cleared on exception return, storing a kernel pointer in the user register state is problematic because thread_get_state can be used to copy the saved user register state back to userspace, including the value of register x18. All a thread needs to do in order to obtain the address of the Lel0_synchronous_vector_64_long function is call thread_get_state on itself and look at the reported value of x18. This makes it trivial to determine the kASLR slide by subtracting the value of x18 thus obtained by the static address of Lel0_synchronous_vector_64_long.

Exploitation

As mentioned above, exploitation is trivial: simply call the function thread_get_state, look at the value for register x18, and subtract from it the static address of the kernel function Lel0_synchronous_vector_64_long.

Discovery

I discovered this issue on February 26, 2018, after noticing a kernel pointer in register x18 of an iOS application crash log. A quick check showed that the same value appeared in register x18 of every crash log on the device, which suggested a serious information leak.

I next tried to determine what exactly was going on with register x18 through experimentation. I set a breakpoint in an empty iOS app and used lldb to read the value of register x18, confirming that the leak was not restricted to crashing applications. Next I tried to read the value of x18 using inline assembly and found that the value obtained did not match the value shown by the debugger when using a command like reg read x18. This suggested that perhaps the leak was really in thread_get_state, and that register x18 didn't truly contain a kernel pointer while the CPU was executing in userspace. A quick proof-of-concept that read the value of x18 using thread_get_state confirmed that this function was indeed the source of the leak.

Timeline

I reported the issue to Apple on February 26, 2018, the same day I discovered it.


By Brandon Azad