[Перевод] Эксплойт iPhone 4, часть 1: получение доступа
В этой серии я рассказываю о том, как я создаю Gala — джейлбрейк iOS 4 для iPhone 4.
Оглавление
Введение
Несколько лет назад я принимал активное участие в разработке твиков для iOS. Я создал множество продуктов и инструментов, распространяемых в Cydia, которые изменяли поведение системы iOS и добавляли новые функции в SpringBoard. Это было действительно веселое время, и оно дало мне ценный опыт в начале карьеры в области реверс-инжиниринга двоичных файлов с закрытым исходным кодом, непосредственного взаимодействия с рантаймом Objective-C, и антрепренёрства. Я очень благодарен за эти годы.
Однако одним аспектом разработки джейлбрейков, который мне всегда казался черной магией, был сам процесс взлома. Перспектива весьма примечательна: возьмите любой готовый iPhone, затем проведите непристойные ритуалы и произнесите жуткие заклинания, пока кандалы не спадут. Теперь ОС позволит вам запускать любой код, который вы укажете, независимо от того, прошел ли этот код благословенную процедуру подписи Apple, открывая путь таким трудолюбивым разработчикам твиков, как я.
Несколько недель назад мне захотелось снять эту завесу тайны с джейлбрейков, написав свой собственный. Одно предостережение: действительно интересную работу здесь проделали мои предки. Я особенно признателен p0sixninja и axi0mx, которые любезно поделились своими знаниями через открытый исходный код.
Порт въезда
Первый шаг. Приобретите устройство. Я ничего не знаю о написании джейлбрейка или о том, как будет выглядеть мой подход, поэтому давайте начнем с чего-то очевидного. Я беру iPhone 4 и 3GS на eBay. Старые устройства кажутся хорошей отправной точкой, поскольку их безопасность, по-видимому, хуже, но вам нужно найти золотую середину: действительно старые устройства чрезвычайно ценны.
Теперь, когда у меня их два, зачем мне на этом останавливаться? eBay отдергивает полы своего темного пальто, открывая торс, заставленный старыми айфонами, специальное предложение: два по цене трех.
Аппараты приехали! У меня есть смутные представления о том, как можно их использовать, основываясь на фрагментах, которые я читал на протяжении многих лет: некоторые ошибки в анализе PDF здесь, какой-то уязвимый код фреймбуфера там. Чтобы попробовать свои силы в использовании любого из них, мне нужно иметь возможность запустить некоторый код на устройстве. Воображаемый путь здесь заключается в том, что мне удастся настроить цепочку инструментов, которая сможет создавать и устанавливать приложения так, как это было сделано еще в 2010 году. Используя это, я затем напишу приложение и покопаюсь в песочнице, чтобы исследовать внешнюю сторону атаки.
Хм… похоже, что последние версии Xcode не позволяют ориентироваться на версии iOS старше пары лет. Может быть, мы сможем скачать более старую версию Xcode?
Ну и чёрт с ним. Я загружаю несколько старых версий Mac OS X, которые собираюсь установить на виртуальной машине, чтобы иметь возможность запускать более старую версию Xcode, а затем понимаю, что мне скучно. Даже если бы мне удалось настроить старый набор инструментов, неясно, подпишет ли Apple вообще двоичный файл, ориентированный на устаревшую версию iOS. Давайте попробуем что-нибудь еще.
В конце концов я решил проверить уязвимость загрузочного ПЗУ. У него есть несколько интересных преимуществ, таких как отсутствие необходимости настраивать старый набор инструментов и работать на виртуальной машине, поскольку уязвимость загрузочного ПЗУ обычно используется путем написания некоторого кода на хост-машине, который взаимодействует с устройством через USB.
Я знаю, что современные устройства имеют общеизвестную уязвимость загрузочного ПЗУ, и иду на iPhone Wiki , чтобы узнать больше. У них есть раздел под названием Vulnerabilities and Exploits
— отлично! Я прочитал некоторые из них и увидел, что у limera1n есть код эксплойта прямо на странице Wiki. Очень интересно просто попробовать этот код и посмотреть, что произойдет.
Отступление о доверии
Загрузочное ПЗУ, или SecureROM на языке Apple, является первым этапом процесса загрузки iOS и запускает следующие этапы процесса загрузки. В обязанности SecureROM входит обеспечение того, чтобы все, что он загружает дальше, было доверенным — другими словами, что он будет запускать только тот образ, который Apple отправила и подписала.
SecureROM с радостью загрузит один из двух компонентов, в зависимости от того, что происходит:
Если устройство выполняет «обычную» загрузку из файловой системы, SecureROM загрузит компонент, называемый
Low Level Bootloader
LLB, из раздела диска в NOR.Если устройство находится в режиме DFU и подключено к компьютеру через USB, процесс
Restore iPhone
можно инициировать отправкой загрузчика iBSS (iBoot Single Stage
).
Точно так же, как SecureROM несет ответственность за проверку надежности LLB или iBSS, и LLB, и iBSS аналогичным образом должны гарантировать, что то, что они загружают дальше, также является надежным. Каждый последующий этап гарантирует, что он доверяет тому, что следует за ним. Процесс восстановления выглядит примерно так:
Это наша цепочка доверия: каждый этап загружает только то, чему он доверяет, и поэтому окончательный код, ориентированный на пользователя, всегда заслуживает доверия.
То есть, если только мы не разорвем эту цепочку! Обратите внимание, что каждый последующий этап проверяется предыдущим этапом, за исключением первого этапа. Наша диаграмма действительно выглядит так:
SecureROM пользуется неявным доверием, и это тяжелое бремя. Хотя все остальные этапы можно заменить, если будут обнаружены какие-либо уязвимости, выпустив обновленную версию iOS, SecureROM записывается в постоянную память при изготовлении устройства. Это означает, что каждое устройство, изготовленное с использованием определенной версии SecureROM, будет постоянно уязвимо к любым проблемам в этой версии.
И, как оказалось, такие уязвимости существуют и ими можно воспользоваться!
limera1n
limera1n — это имя, данное одному из таких эксплойтов, который был выпущен компанией geohot и упакован в одноименный инструмент для джейлбрейка в 2010 году. limera1n можно использовать, когда устройство в режиме DFU ожидает отправки iBSS от хоста через USB. SecureROM, поставляемый с SoC A4, уязвим, поэтому iPhone 4, который я купил, должен стать отличной целью.
Одна вещь, которая мне очень интересна в limera1n, это то, что никто точно не знает✱, как он работает. geohot сказал, что понятия не имеет, почему это работает, а p0sixninja выдвинул теории. Инструменты, позволяющие понять, что происходит, определенно существуют (особенно после утечки исходного кода iBoot iOS 9), но, насколько мне известно, никто не утверждал, что собрал все воедино. Авария, которая привела к limera1n, была обнаружена путем фаззинга управляющих сообщений USB и, по-видимому, является состоянием гонки, которое приводит к переполнению кучи, позволяя злоумышленнику внедрить и запустить шеллкод. Фаззинг двоичных файлов с закрытым исходным кодом подарил нам инопланетную технологию: мы можем использовать ее, она мощная, но мы не знаем, что она делает.
✱ Примечание
Публично, по крайней мере
Чтение данных с устройства в режиме DFU
Я начал искать реализации limera1n, чтобы понять, как их воспроизвести. Я быстро наткнулся на дамп SecureROM pod2g, который мне очень помог. Одним махом он показал мне:
Как внедрить limera1n
Какой код может находиться в пэйлоаде
Как прочитать память с устройства через USB
Этот последний пункт был невероятным. SecureROM работает на устройстве, и если вы хотите его проанализировать, вам нужно каким-то образом удалить его с устройства. Дамп SecureROM pod2g копирует память, в которой отображается SecureROM ( 0x0
), в область приема USB. Затем на стороне хоста он отправляет управляющие сообщения USB на read
данных с устройства.
Насколько мне известно, вторую половину этого никто явно не описал: вы не только можете записывать данные на устройство iOS через USB, но и устройство также будет отвечать на запросы чтения. Я нашел это весьма удивительным, поскольку представлял себе устройство в виде черной дыры, которая поглощает кусочки и никогда не раскрывает ничего о своем собственном состоянии.
Этот механизм не объясняется нигде в Интернете, насколько я вижу. Вот мое понимание:
MMU A4 отображает базу SRAM в
0x84000000
.Хосты, взаимодействующие с устройством DFU (например, программное обеспечение Apple, работающее на Mac для восстановления iPhone), могут отправлять образ iBSS по частям, отправляя пакеты управления USB с типом запроса
0x21
и идентификатором запроса1
. Данные, отправленные в управляющих пакетах, будут копироваться в SRAM, начиная с0x84000000
и перемещаясь к более высоким адресам по мере того, как хост отправляет больше пакетов (чтобы не перезаписывать предыдущие данные). SecureROM поддерживает некоторые внутренние счетчики, отслеживающие, куда должен быть скопирован следующий пакет, и эти счетчики могут быть очищены (предположительно, если хост хочет отменить передачу и начать все сначала).
Устройство также ответит, если хост отправит управляющий пакет с
request type 0xA1, request ID 2
. Устройство прочитает содержимое памяти0x84000000
и отправит его на хост. Это кажется сомнительно полезным, если эта память якобы содержит только те данные, которые хост уже отправил, но становится действительно удобным, когда у нас есть возможность выполнять код на устройстве и копировать все, что захотим в0x84000000
.
Таким образом, приведенный выше дамп использует limera1n для выполнения полезной нагрузки, которая копирует память
0x0
(содержащую SecureROM) в0x84000000
, а затем возвращается обратно в исходный цикл SecureROM DFU. Затем хост отправляет несколькоA1:2
запросов на чтение, по сути извлекая дамп SecureROM с устройства.
Я пока мало что знаю о USB✱, поэтому мне любопытно, имеют ли 0x21:1
они 0xA1:2
какое-то более глубокое значение или это произвольные значения, жестко закодированные в бизнес-логике SecureROM. В одном сообщении Stack Overflow подразумевается, что они кодируют некоторую стандартную информацию:
✱ Примечание
Мой основной метод изучения подобных вещей — реализация их в axle, и я еще не реализовал стек axle для USB.
Первый байт (bmRequestType) в установочном пакете состоит из 3 полей. Первые (наименее значимые) 5 бит — это получатель, следующие 2 бита — тип и последний бит — направление.
p0sixninja представил презентацию об этой утилите в 2013 году на Hack In the Box Malaysia , но, насколько я могу судить, в его слайдах есть ошибка: он говорит, что дамп SecureROM pod2g построен на реализации SHAtter (еще один эксплойт SecureROM, разработанный одновременно с limera1n), но утилита pod2g на самом деле использует реализацию limera1n.
Я написал свою собственную реализацию limera1n на основе дампера SecureROM pod2g и попробовал также создать дамп SecureROM. Я был в восторге, когда это сработало!
$ Дамп SecureROM
Написание пэйлоада
Теперь у меня есть возможность выполнить код на этом iPhone 4, и я готов двигаться в своем направлении. Для начала я могу запустить сборку, но непонятно, где эта сборка выполняется. Где мой стек? Какую память перезаписывает мой шеллкод? Каковы пределы размера моей программы с шеллкодом, прежде чем я начну перезаписывать что-то важное в памяти?
Прежде чем мы сможем ответить на любой из этих вопросов, нам понадобится какой-то способ получить отладочные данные с устройства. Поток чтения памяти 0x84000000
, используемый в дампе SecureROM, кажется действительно полезным инструментом для этого! Я написал некоторый шелл-код, который копирует значения указателя инструкции и указателя стека в 0x84000000
, а затем использовал тот же код на стороне хоста для обратного чтения значений. Таким образом, я сделал print()
бедняка, который позволяет мне передавать информацию, которую я собираю на устройстве, посредством дампов памяти, которые я получаю на хосте.
let communication_area_base = unsafe {
slice::from_raw_parts_mut(0x84000000 as *mut _, 1024)
};
communication_area[0] = pc;
communication_area[1] = sp;
Я написал несколько сценариев для автоматического запуска моего эксплойта и вывода первых нескольких слов из 0x84000000
в окно вывода, чтобы я мог проверить скопированные значения указателя инструкции и указателя стека. Эти скрипты позволили мне быстро выполнить итерацию после внесения изменений в мой шеллкод.
$ Инспекция среды
Глядя на первые два слова дампа памяти, мы видим, что наш шеллкод бегает 0x8402b048
( 48b00284
в дампе памяти), а указатель стека находится в позиции 0x8403bfa0
( a0bf0384
). Это имеет смысл! Указатель стека находится в пределах обычной области стека, которую устанавливает само SecureROM, а указатель инструкции находится в области приёма изображения. Поскольку мы эксплуатировали переполнение для обеспечения выполнения кода, то, что наш код выполняется из буфера доставки, неудивительно.
Восхождение из ассемблера
Делая свою жизнь лучше, я также упростил разработку логики пэйлоада. Написание программного обеспечения непосредственно на ассемблере в некоторых случаях полезно, но здесь это просто помеха. Я установил систему сборки, которая позволила мне написать пэйлоад на Rust✱, который затем конвертировалась в шеллкод и отправлялся на устройство. Я выбрал Rust, потому что знал, что однажды напишу об этой работе в блоге, и этот выбор соберет больше всего глаз. Плюс, насколько здорово запускать пэйлоад эксплойта Rust на устройстве, которое было произведено до появления Rust?!
✱ Примечание
Проект Rust прекратил поддержку этой armv7-apple-ios
цели в начале 2020 года, но легко вернуться к более старой поддерживаемой цепочке инструментов с помощью rustup
.
Однако написание шеллкода на любом языке более высокого уровня налагает некоторые дополнительные трудности, которых нет в ассемблере. Важно помнить, что когда вы компилируете код на языке высокого уровня, вы не получаете необработанный машинный код: вместо этого цепочки инструментов компилируют ваш код в двоичный файл .который содержит массу метаданных, включая (помимо прочего) инструкции для ОС о том, как настроить виртуальное адресное пространство так, как ожидает программа, таблицы символов для отладки и информацию о компоновщике. Мы не хотим ничего этого здесь! Наш эксплойт дает нам возможность внедрить байты в память и переходить к ним, и нам не нужны все возможности, которые обычно предоставляются при компиляции двоичного файла в контролируемой среде. Это Дикий Запад, детка, и мы программируем странную машину.
В macOS двоичные файлы обычно имеют следующий вид:
Другими словами, двоичный файл (размещенный в формате Mach-O) представляет собой набор сегментов, каждый из которых содержит раздел, представляющий те или иные данные. Один раздел может предназначаться для хранения метаданных Objective-C, а другой — для хранения статически встроенных строк C. Только один из этих разделов содержит необработанный машинный код, который мы хотим загрузить на iPhone: раздел __text
в сегменте __TEXT
. Так получилось, что я написал strongarm, обширную библиотеку анализа Mach-O, поэтому добавил в систему сборки быстрый скрипт: он компилирует пэйлоад и компонует ее в Mach-O, затем использует strongarm для извлечения содержимого раздела __TEXT,__text
и записывает его в файл. Содержимое этого файла — это то, что мы используем в limera1n для выполнения на устройстве.
from strongarm.macho import MachoParser
def dump_text_section_to_file(input_binary: Path, output_file: Path) -> None:
with open(output_file.as_posix(), "wb") as f:
f.write(dump_text_section(input_binary))
def dump_text_section(input_file: Path) -> bytes:
parser = MachoParser(input_file)
binary = parser.get_armv7_slice()
text_section = binary.section_with_name("__text", "__TEXT")
return binary.get_content_from_virtual_address(text_section.address, text_section.size)
Умиротворение компоновщика
Здесь мы не компилируем типичный двоичный файл, а типичные двоичные файлы имеют соглашение с инфраструктурой ОС о том, как будет называться их точка входа. По умолчанию компоновщик ожидает, что наш двоичный файл будет определять символы start
или _main
, которые не нужны для нашего варианта использования. Если мы не сообщим компоновщику, что делаем что-то необычное, он остановит нас и пожалуется, что отсутствуют стандартные символы.
$ as -arch armv7 entry.s -o entry.o
$ ld entry.o
Undefined symbols for architecture armv7:
"_main", referenced from:
implicit entry/start for main executable
ld: symbol(s) not found for architecture armv7
Давайте скажем компоновщику, что мы не будем их предоставлять, и попробуем!
$ ld entry.o -U _main
ld: dynamic executables or dylibs must link with libSystem.dylib for architecture armv7Упс, теперь ldдумает, что мы компилируем динамическую библиотеку. Ну, это нормально. Будет ли он отключен, если мы скажем ему, что установим связь libSystem.dylib? Давайте попробуем!
Упс, теперь ld
думает, что мы компилируем динамическую библиотеку. Ну, это нормально. Будет ли он отключен, если мы скажем ему, что установим связь с libSystem.dylib
? Давайте попробуем!
$ ld entry.o -U _main -framework libSystem.dylib -o output.o
ld: framework not found libSystem.dylib
Давайте… давайте не торопимся. Это справедливо, что в libSystem.dylib
это недоступно при кросс-компиляции в armv7
, и у нас нет системного рута для iOS 4 под рукой. Я вижу многообещающий вариант в manual
.
-static
Produces a mach-o file that does not use the dyld. Only used building the kernel.
Ну, мы определенно не собираем здесь ядро. Мы создаем двоичный файл, который не использует dyld. Могли бы мы…? Нет-нет, конечно нет…, но может быть?
$ ld entry.o -U _main -o output.o -static
Undefined symbols for architecture armv7:
"start", referenced from:
-u command line option
ld: symbol(s) not found for architecture armv7
Круто, это прогресс! Давайте просто скажем компоновщику, что start
мы тоже не собираемся определять…
$ ld entry.o -U _main -U start -static -o output.o
# Success
Здорово! Теперь мы передадим его в strongarm, чтобы извлечь содержимое __TEXT,__text
…
Traceback (most recent call last):
File "payload_stage1/../dump_shellcode.py", line 15, in dump_text_section_to_file
f.write(dump_text_section(input_file))
File "payload_stage1/../dump_shellcode.py", line 7, in dump_text_section
parser = MachoParser(input_file)
File "strongarm/macho/macho_binary.py", line 847, in dyld_info
raise LoadCommandMissingError()
strongarm.macho.macho_binary.LoadCommandMissingError
Ой-ой, strongarm сломался! Это связано с тем, что во время первоначального анализа двоичного файла strongarm ожидает найти LC_DYLD_INFO
команду загрузки. Поскольку мы создали автономный двоичный файл, который вообще не использует dyld, эта команда загрузки отсутствует. Это не было обработано, потому что я никогда раньше не сталкивался с подобным двоичным файлом: большинство двоичных файлов используют dyld! Я добавил быстрый патч в strongarm, чтобы справиться с этим, и теперь все в порядке.
Помня о наших ограничениях
В ходе расширения полезной нагрузки я начал включать в дамп памяти довольно много значений. Стало немного сложно запомнить: «слово 3 — это возвращаемое значение этого вызова, слово 7 — это адрес этой функции», поэтому я внес по существу мягкое изменение, включив некоторые строки в данные, которые я поместил в пространство связи. Код выглядит примерно так:
let communication_area_base = unsafe {
slice::from_raw_parts_mut(0x84000000 as *mut _, 2048)
};
let mut cursor = 0;
write_str(
communication_area_base,
&mut cursor,
"Output from image3_decrypt_payload: ",
);
write_u32(
communication_area_base,
&mut cursor,
ret
);
Давайте попробуем!
$ Попытка передать строки
Хм, это странно. Похоже, что-то сломалось. Причина становится совершенно ясной, если мы внимательно посмотрим на наш двоичный файл пэйлоада перед извлечением его содержимого __TEXT,__text
:
Ой! Статическая строка в нашем исходном коде была помещена в __const
, и наш скомпилированный код Rust пытается получить доступ к строке, загружая память по адресу, где двоичный файл запрашивает размещение строки в виртуальном адресном пространстве. Поскольку мы полностью отбрасываем всё, кроме __TEXT,__text
, эти сопоставления виртуального адресного пространства представляют собой бессильный запрос со стороны двоичного файла, и данные в __const
никогда не загружаются в память. Поэтому наш код запрашивает загрузку строки с совершенно не сопоставленным адресом, и наш двоичный файл аварийно завершает работу. Исправление довольно простое и дает вескую причину использовать ассемблер для написания наших пэйлоадов: ассемблер дает программисту явный и прямой контроль над размещением статических данных, в то время как скомпилированные языки пытаются обрабатывать их от имени программиста.
Чтобы это исправить, нам нужно убедиться, что все определяемые нами статические данные встроены в __TEXT,__text
, чтобы быть уверенными, что они не потеряются при извлечении шеллкода. Нам также необходимо следить за тем, чтобы при любом доступе к статическим данным использовалась адресация относительно указателя инструкций, а не абсолютные адреса, поскольку мы не можем рассчитывать на загрузку по какому-либо стабильному адресу памяти. На данный момент я определяю любые строки, которые хочу использовать в сборке, и передаю их адреса в точку входа пэйлоада Rust:
.text
shellcode_start:
adr r0, msg1
mov r1, msg1_len
bl _rust_entry_point
# ...
msg1:
.asciz "Output from image3_decrypt_payload: "
msg1_len:
.equ . - msg1
Мы находимся в очень хорошем месте! Теперь у нас есть этот пайплайн:
Внесите изменения в пйэлоад Rust.
Нажмите кнопку
Пейлоад будет скомпилирован
Шелл-код будет извлечен из двоичного файла.
Раннер будет использовать Limera1n на подключенном DFU iPhone для выполнения пэйлоада.
Раннер автоматически прочитает данные из файла
0x84000000
, который мы используем в качестве коммуникационного пространства, и представит их в виде шестнадцатеричного дампа.
Что дальше? Отсюда мы можем делать практически все, поскольку можем запускать на устройстве произвольный код. С одной точки зрения, игра окончена. Однако с другой стороны, веселье только начинается. Одно дело — уметь делать что угодно в теории. Совсем другое дело — заставить устройство делать что-то интересное.