Lucene search

K
googleprojectzeroGoogleProjectZeroGOOGLEPROJECTZERO:6555AE6CE499D9D30373C3D7100AD02F
HistoryAug 29, 2019 - 12:00 a.m.

In-the-wild iOS Exploit Chain 3

2019-08-2900:00:00
googleprojectzero.blogspot.com
202

7.5 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

0.072 Low

EPSS

Percentile

93.9%

Posted by Ian Beer, Project Zero


TL;DR

This chain targeted iOS 11-11.4.1, spanning almost 10 months. This is the first chain we observed which had a separate sandbox escape exploit.

The sandbox escape vulnerability was a severe security regression in libxpc, where refactoring lead to a < bounds check becoming a != comparison against the boundary value. The value being checked was read directly from an IPC message, and used to index an array to fetch a function pointer.

It’s difficult to understand how this error could be introduced into a core IPC library that shipped to end users. While errors are common in software development, a serious one like this should have quickly been found by a unit test, code review or even fuzzing. It’s especially unfortunate as this location would naturally be one of the first ones an attacker would look, as I detail below.

In-the-wild iOS Exploit Chain 3 - XPC + VXD393/D5500 repeated IOFree

targets: 5s through X, 11.0 through 11.4

Devices:

iPhone6,1 (5s, N51AP)

iPhone6,2 (5s, N53AP)

iPhone7,1 (6 plus, N56AP)

iPhone7,2 (6, N61AP)

iPhone8,1 (6s, N71AP)

iPhone8,2 (6s plus, N66AP)

iPhone8,4 (SE, N69AP)

iPhone9,1 (7, D10AP)

iPhone9,2 (7 plus, D11AP)

iPhone9,3 (7, D101AP)

iPhone9,4 (7 plus, D111AP)

iPhone10,1 (8, D20AP)

iPhone10,2 (8 plus, D21AP)

iPhone10,3 (X, D22AP)

iPhone10,4 (8, D201AP)

iPhone10,5 (8 plus, D211AP)

iPhone10,6 (X, D221AP)

Versions:

15A372 (11.0 - 19 Sep 2017)

15A402 (11.0.1 - 26 Sep 2017)

15A403 (11.0.2 - 26 Sep 2017 - seems to be 8/8plus only, which didn’t get 15A402)

15A421 (11.0.2 - 3 Oct 2017)

15A432 (11.0.3 - 11 Oct 2017)

15B93 (11.1 - 31 Oct 2017)

15B150 (11.1.1 - 9 Nov 2017)

15B202 (11.1.2 - 16 Nov 2017)

15C114 (11.2 - 2 Dec 2017)

15C153 (11.2.1 - 13 Dec 2017)

15C202 (11.2.2 - 8 Jan 2018)

15D60 (11.2.5 - 23 Jan 2018)

15D100 (11.2.6 - 19 Feb 2018)

15E216 (11.3 - 29 Mar 2018)

15E302 (11.3.1 - 24 Apr 2018)

15F79 (11.4 - 29 May 2018)

first unsupported version: 11.4.1 - 9 July 2018

Binary structure

Starting from this third chain the privesc binaries have a different structure. Rather than using the system loader and linking against the required symbols, they instead resolve all the required symbols themselves via dlsym (with the address of dlsym getting passed in from the JSC exploit.) Here’s a snippet from the start of the symbol resolution function:

syscall = dlsym(RTLD_DEFAULT, “syscall”);

memcpy = dlsym(RTLD_DEFAULT, “memcpy”);

memset = dlsym(RTLD_DEFAULT, “memset”);

mach_msg = dlsym(RTLD_DEFAULT, “mach_msg”);

stat = dlsym(RTLD_DEFAULT, “stat”);

open = dlsym(RTLD_DEFAULT, “open”);

read = dlsym(RTLD_DEFAULT, “read”);

close = dlsym(RTLD_DEFAULT, “close”);

Interestingly, this seems to be an append-only list, and there are plenty of symbols which aren’t used. In Appendix A I’ve enumerated those, and guessed what bugs they might have been targeting with earlier versions of this framework.

Checking for prior compromise

Like PE2, after the kernel exploit has successfully run they make a system modification which can be observed from inside the sandbox. This time they add the string “iop114” to the device bootargs which can be read from inside the WebContent sandbox via the kern.bootargs sysctl:


sysctlbyname(“kern.bootargs”, bootargs, &v7, 0LL, 0LL);

if (strcmp(bootargs, “iop114”)) {

syslog(0, “to sleep …”);

while (1)

sleep(1000);

}

Unchecked array index in xpc

XPC (which probably stands for “Cross”-Process Communication) is an IPC mechanism which uses mach messages as a transport layer. It was introduced in 2011 around the time of iOS 5. XPC messages are serialized object trees, typically with a dictionary at the root. XPC also contains functionality for exposing and managing named services; newer IPC services tend to be built on XPC rather than the legacy MIG system.

XPC was marketed as a security boundary; at the 2011 Apple World Wide Developers Conference (WWDC) Apple explicitly stated the benefits of isolation via XPC as “Little to no harm if service is exploited” and that it “Minimizes impact of exploits.” Unfortunately, there has been a long history of bugs in XPC; both in the core library as well as in how services used its APIs. See for example the following P0 issues: 80, 92, 121, 130, 1247, 1713. Core XPC bugs are quite useful, as they allow you to target any process which uses XPC.

This particular bug appears to have been introduced via some refactoring in iOS 11 in the way that the XPC code parses serialized xpc dictionary objects in “fast mode”. Here’s the old code:

struct _context {

xpc_dictionary* dict;

char* target_key;

xpc_serializer* result;

int* found

};

int64

_xpc_dictionary_look_up_wire_apply(

char *current_key,

xpc_serializer* serializer,

struct _context *context)

{

if ( !current_key )

return 0;

if (strcmp(context->target_key, current_key))

return _skip_value(serializer);

// key matches; result is current state of serializer

memcpy(context->result, serializer, 0xB0);

*(context->found) = 1;

return 0;

}

An xpc_serializer object is a wrapper around a raw, unparsed XPC message. (The xpc_serializer type is responsible for both serialization and deserialization.)

Here’s an example serialized XPC message:
Diagram showing a byte-by-byte breakdown of a simple xpc object serialized in to a mach message. It shows the basic serialization format used by XPC of 32-bit type identifiers eg 0x9000 for a string, 0x4000 for a uint64 followed by the values of the types, with either variable or fixed lengths. In XPC’s “slow mode” an incoming message is completely deserialized into XPC objects when it’s received. The fast mode instead attempts to lazily search for values inside the serialized dictionaries when they’re first requested, rather than parsing everything upfront. It does this by comparing the keys in the serialized dictionary against the desired key; if the current key doesn’t match they call skip_value to jump over the payload value of the current key to the next key in the serialized XPC dictionary object.

int skip_value(xpc_serializer* serializer)

{

uint32_t wireid;

uint64_t wire_length;

wireid = read_id(xpc_serializer);

if (wireid == 0x1A000)

return 0LL;

wire_length = xpc_types[wireid >> 12]->wire_length(serializer);

if (wire_length == -1 ||

wire_length > serializer->remaining)

return 0;

// skip over the value

xpc_serializer_advance(serializer, wire_length);

return 1;

}


uint32_t read_id(xpc_serializer* serializer)

{

// ensure there are 4 bytes to be read; return pointer to them

wireid_ptr = xpc_serializer_read(serializer, 4, 0, 0);

if ( !wireid_ptr )

return 0x1A000;

uint32_t wireid = *wireid_ptr;

uint32_t typeid = wireid >> 12;

// if any bits other than 12-20 are set,

// or the type_index is 0, fail

if (wireid & 0xFFF00FFF ||

typeid == 0

typeid >= _xpc_ntypes) { // 0x19

return 0x1A000LL;

}

return wireid;

}

skip_value first calls read_id, which reads 4 bytes from the serialized message. Those four bytes are the wireid value, which tells XPC the type of the serialized value. read_id also verifies that the wireid is valid: the xpc typeid is contained in bits 12-20 of the wireid, only those bits may be set and the value of the typeid must be greater than zero and less than 0x19. If these conditions aren’t met then read_id returns the sentinel wireid value of 0x1A000. skip_id checks for this sentinel return value from read_id and aborts. If read_id returns a valid wireid value, then skip_id uses the typeid bits to index the xpc_types array and call a function pointer read indirectly from there.

Let’s take a look at how this code changed in iOS 11. The prototype for xpc_dictionary_look_up_wire_apply is unchanged:

int64

_xpc_dictionary_look_up_wire_apply(

char *current_key,

xpc_serializer* serializer,

struct _context *context)

{

if (!current_key)

return 0;

if (strcmp(context->target_key, current_key))

return skip_id_and_value(serializer);

memcpy(context->result, serializer, 0xB0);

*(context->found) = 1;

return 0;

}

The call to skip_value has been replaced with a call to skip_id_and_value however:

int64 skip_id_and_value(xpc_serializer* serializer)

{

uint32_t* wireid_ptr = xpc_serializer_read(serializer, 4, 0, 0);

if (!wireid_ptr)

return 0;

uint32_t wireid = *wireid_ptr;

if (wireid != 0x1B000)

return skip_value(xpc_serializer, wireid);

return 0;

}

There’s no call to read_id anymore (which was responsible for both reading and verifying the id) instead skip_id_and_value reads the four byte wireid value itself. Curiously it compares the four-byte wireid value against 0x1B000. Is this comparison supposed to actually be something like this?

wireid < 0x1B000

Something seems very wrong.

The controlled wireid value, which can now be any value apart from 0x1B000, is passed to skip_value; which has a different prototype to before now taking a wireid in addition to the xpc_serializer:

int64

skip_value(xpc_serializer* serializer, uint32_t wireid)

{

// declare function pointer

uint32_t (wire_length_fptr*)(xpc_serializer*);

wire_length_fptr = xpc_wire_length_from_wire_id(wireid);

uint32_t wire_length = wire_length_fptr(serializer)

if (wire_length == -1 ||

wire_length > serializer->remaining) {

return 0;

}

xpc_serializer_advance(serializer, wire_length);

return 1;

}


uint32_t ()(xpc_serializer)

xpc_wire_length_from_wire_id(uint32_t wireid)

{

return xpc_types[wireid >> 12]->wire_length;

}

Not only has the prototype of skip_value changed; the precondition has changed too: it used to be the case that skip_value was responsible for verifying the wireid value in the message. That’s no longer the case. The wireid value is passed directly to xpc_wire_length_from_wire_id where the lower 12-bits are shifted out and the upper 20 are used to directly index the xpc_types array. xpc_types is an array of pointers to Objective-C classes; the field at +0x90 is the wire_length function pointer, which will be called by skip_value.

What happened to all the bounds checking? Lots of code changed subtly here; the semantics of the functions changed and in the end a correct bounds check seems to have become a comparison against just a single invalid value.

Looking at the other xpc_wire_length_from_wire_id call-sites they are all dominated by calls to _xpc_class_id_from_wire_valid, which actually validates the wireid:

int xpc_class_id_from_wire_valid(uint32_t wireid)

{

if (((wire_id - 0x1000) < 0x1A000) &&

((wire_id & 0xFFF00F00) == 0)) {

return 1;

}

return 0;

}

It’s very simple to hit this bug; anywhere between iOS 11.0 and 11.4.1 just flip a few bits in an XPC message and you’ll probably hit it. This is why I believe that fuzzing or a unit test would have quickly found this issue.

XPC eXploitation

Let’s take a closer look at exactly what will happen when the vulnerability is triggered:

int64 skip_id_and_value(xpc_serializer* serializer)

{

uint32_t* wireid_ptr = xpc_serializer_read(serializer, 4, 0, 0);

if (!wireid_ptr)

return 0;

uint32_t wireid = *wireid_ptr;

if (wireid != 0x1B000)

return skip_value(xpc_serializer, wireid);

xpc_serializer_read returns a pointer into the raw mach message buffer; it’s just ensuring that there are at least 4 bytes left to read. As long as those 4 bytes don’t contain the value 0x1B000, they’ll pass the checks.

Let’s look at the iOS 11 version of skip_value again:

int64

skip_value(xpc_serializer* serializer, uint32_t wireid)

{

// declare function pointer

uint32_t (wire_length_fptr*)(xpc_serializer*);

wire_length_fptr = xpc_wire_length_from_wire_id(wireid);

uint32_t wire_length = wire_length_fptr(serializer)

Each XPC type (eg xpc_dictionary, xpc_string, xpc_uint64) defines a function to determine how large their serialized payload is. For fixed-sized objects, such as an xpc_uint64, this will just return a constant (an xpc_uint64 payload is always 8 bytes):

__xpc_uint64_wire_length

MOV W0, #8

RET

Similarly, an xpc_uuid object always has a 0x10 byte payload:

__xpc_uuid_wire_length

MOV W0, #0x10

RET

For variable-sized types the length needs to be read from the serialized object:

__xpc_string_wire_length

B __xpc_wire_length

All variable-sized xpc objects record their size in bytes directly after their wireid, so _xpc_wire_length just reads the next 4 bytes without consuming them.

_xpc_wire_length_from_wire_id looks up the correct function pointer to call:

uint32_t ()(xpc_serializer)

xpc_wire_length_from_wire_id(uint32_t wireid)

{

return xpc_types[wireid >> 12]->wire_length;

}

xpc_types is an array of pointers to the relevant Objective-C class objects:

__xpc_types:

libxpc:__const:DCQ 0

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_null

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_bool

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_int64

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_uint64

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_double

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_pointer

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_date

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_data

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_string

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_uuid

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_fd

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_shmem

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_mach_send

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_array

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_dictionary

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_error

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_connection

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_endpoint

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_serializer

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_pipe

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_mach_recv

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_bundle

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_service

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_service_instance

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_activity

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_file_transfer

__xpc_ool_types:

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_fd

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_shmem

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_mach_send

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_connection

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_endpoint

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_mach_recv

libxpc:__const:DCQ OBJC_CLASS$_OS_xpc_file_transfer

The value at offset +0x90 in each xpc type’s class object is its wire_length function pointer. That function pointer will be called with one argument, which is a pointer to the current xpc_serializer object.

This gives quite an interesting exploitation primitive:

They control an array index i, which can be between 0x1c and 0x100000 (since it’s the upper 20 bits of the controlled wireid value). That will index the xpc_types array, in the const segment of the libxpc.dylib library in the shared cache. The code will read the pointer at the offset they provide (without bounds checking) then call the function pointer at offset +0x90 from that:
Diagram showing the series of pointer dereferences as a consequence of the unchecked array bounds. The unchecked index reads off of the end of the xpc_types array, reading an objective-c class pointer out-of-bounds. A function pointer is called at offset +0x90 from that class pointer.

When F_PTR gets called, no register will point to controlled data. X0 will point to the current xpc_serializer, so that seems like the logical choice for targeting to make something more interesting happen. The relevant fields of an xpc_serializer object which can be indirectly controlled are:

7.5 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

0.072 Low

EPSS

Percentile

93.9%