Как взломать iPhone обычной GIF'кой
Подробнейшая лекция, в которой мы рассказываем, как работает эксплоит zero-click для iMessage
Всем салют, дорогие друзья!
В израильской NSO Group создали эксплоит для iMessage, наделавший много шума. С помощью именно этого эксплоита троян Pegasus был внедрен в телефоны публичных деятелей и политиков. Apple уже подала иск на NSO. Однако оставим в стороне политику — в этой статье мы сосредоточимся на самом эксплоите, тем более что он просто взрывной! Заражает девайсы без участия юзера, укрывается внутри GIF и включает в себя крошечный виртуальный компьютер.
NSO
Начало тому, о чем мы будем говорить, было положено в августе 2016 года, когда израильская компания NSO Group, специализирующаяся на кибероружии, разработала и выпустила шпионское ПО Pegasus, предназначенное для заражения мобильных устройств под управлением Android и iOS. «Пегас» был способен читать текстовые сообщения, отслеживать звонки и местоположение, собирать пароли, получать информацию с микрофона и камеры, а также доступ к личной информации пользователя.
Тот старый «Пегас» 2016 года использовал эксплоит «одного нажатия» (one-click). То есть, когда пользователь‑жертва получал на свой смартфон «заряженное» сообщение, чтобы активировать подложенный сюрприз, ему нужно было что‑то сделать, например кликнуть по ссылке. Заражения было легко избежать — достаточно было не нажимать на что попало.
В июле 2021 года удалось изучить эксплоит «нулевого нажатия» для iMessage, обнаруженный на смартфоне активиста из Саудовской Аравии. Эксплоит работал вообще без участия пользователя и срабатывал сам — хакеру достаточно лишь отправить полезную нагрузку в мессенджере.
ОПАСНЫЙ ПРИЕМ
Входная точка «Пегаса» в iPhone — приложение iMessage. Это значит, что атакующему достаточно знать телефонный номер или Apple ID жертвы.
В iMessage есть нативная поддержка GIF-анимации. Присланная в чат гифка воспроизводится в цикле бесконечно. Как только iMessage получает сообщение, еще до его вывода на экран вызывается метод в процессе IMTranscoderAgent
. Он, в свою очередь, выполняется за пределами песочницы BlastDoor. При этом в параметре метода передается любое изображение с расширением gif. Вот таким образом:
[IMGIFUtils copyGifFromPath:toDestinationPath:error]
Это пример кода на языке Objective-C. Посмотри на селектор. Здесь, вероятно, было намерение просто скопировать файл GIF перед редактированием поля счетчика циклов, но семантика этого метода иная. Внутри он использует API CoreGraphics, чтобы отобразить исходное изображение в новый GIF-файл по заданному пути. Однако то, что файл имеет расширение gif, вовсе не означает, что он на самом деле гифка.
Библиотека ImageIO применяется, чтобы опознать формат файла и проанализировать его, но при этом полностью игнорирует его расширение. При использовании этого трюка с «поддельными гифками» более 20 графических кодеков становятся потенциальными жертвами для атаки нулевого нажатия в iMessage. Некоторые из них очень сложны и состоят из сотен тысяч строк кода. Огромное пространство для хакерской смекалки!
С iOS 14.8.1 (26 октября 2021 года) Apple ограничила форматы в ImageIO, доступные из процессаIMTranscoderAgent
. Также инженеры компании полностью удалили код для доступа к GIF изIMTranscoderAgent
с версии iOS 15.0 (20 сентября 2021 года), вместе с тем полностью перенеся декодирование GIF внутрь BlastDoor.
PDF ВНУТРИ GIF
В NSO использовали дыру «поддельный GIF», чтобы через нее заюзать уязвимость в парсере CoreGraphics PDF.
Долгие годы формат PDF был излюбленной целью для атак — он доступен везде и обладает достаточной сложностью. Приятный бонус для хакеров — поддержка JavaScript внутри PDF. CoreGraphics PDF не интерпретирует JavaScript, тем не менее NSO удалось найти в недрах парсера кое‑что не менее мощное.
ЭКСТРЕМАЛЬНОЕ СЖАТИЕ
В конце девяностых мало у кого был стабильный и быстрый интернет, большинство юзеров дозванивались к провайдеру по dial-up и работали на смешных сейчас скоростях. Да и диски не отличались большими емкостями, поэтому сжатие данных было важной технологией. PNG, JPEG и GIF нам знакомы и по сей день, но были и другие.
Формат JBIG2 предназначался для сжатия монохромных изображений (где пиксели могут быть только черными или белыми). Он применялся в офисных сканерах высокой ценовой категории, таких как Xerox WorkCenter.
Если лет десять‑двадцать тому назад тебе доводилось использовать функцию «сканирования в PDF» на подобном устройстве, в получавшихся у тебя PDF, скорее всего, был поток JBIG2.
Примечательно, что эти файлы даже при достойном разрешении сканирования занимали всего несколько килобайтов. JBIG2 использует два метода для достижения такого мощного сжатия. Сейчас мы их обсудим. Только не думай, что мы тут отвлеклись на какую‑то побочную ерунду, — все это имеет непосредственное отношение к эксплуатации дыры в iMessage!
Техника 1: сегментация и замещение
Текстовый документ, особенно написанный на языках с небольшими алфавитами (английский или, к примеру, русский), состоит из множества часто встречающихся символов. Вместе буквы, диакритику, знаки препинания и прочие загогулины называют глифами.
JBIG2 пытается сегментировать каждую страницу на глифы, а затем использует простое сопоставление с образцом, чтобы выделить глифы, которые выглядят одинаково.
При этом JBIG2 ничего не знает о самих глифах и не пытается распознавать их и сопоставлять с алфавитом (OCR). Кодировщик JBIG2 просто ищет связанные области пикселей и группирует похожие.
При этом алгоритм сжатия заменяет все достаточно похожие на вид области копией только одной из них.
В таком случае текст по‑прежнему прекрасно читается, однако объем хранимой информации становится меньше. Вместо того чтобы хранить данные о пикселях всей страницы, для их отображения нужна только сжатая версия «ссылочного глифа» для каждого символа и относительные координаты мест, где должны быть размещены его копии. При распаковке алгоритм расставляет глифы по местам, как бы рисуя ими на холсте.
Такой подход таит в себе существенный недостаток: кривой кодировщик может случайно спутать похожие на вид символы. А это приводит к печальным последствиям. Для нас в данном случае эти проблемы не важны — разве что ими можно объяснить почти полное вымирание JBIG2.
Техника 2: уточняющее кодирование
Итак, результат сжатия на основе подстановки отображается с потерями. То есть после сжатия и распаковки вывод на вид не будет в точности соответствовать вводу. Поэтому JBIG2 поддерживает и сжатие без потерь, в которое входит промежуточный этап «сжатия с меньшими потерями».
В этом случае дополнительно используется информация о разнице между замещенным глифом и исходным — тоже, конечно же, сжатая. Вот пример, показывающий различия между замещенным символом слева и исходным символом без потерь посередине.
В примере выше кодировщик сохраняет маску разности, показанную справа, затем во время распаковки она подвергается операции XOR с замененным символом, чтобы восстановить точные пиксели, составляющие исходный символ.
Вместо того чтобы полностью кодировать всю разность за один раз, это можно сделать поэтапно, при этом на каждой итерации используется логический оператор (один из AND, OR, XOR или XNOR) для установки, сброса или переключения битов.
Каждый последующий шаг уточнения приближает конечный результат к оригиналу, и это позволяет контролировать потери качества при сжатии. Реализация этих шагов уточняющего кодирования очень гибкая. А еще здесь есть возможность читать значения, уже присутствующие в рабочей области вывода. А это, как ты уже, возможно, догадываешься, ведет нас к полноте по Тьюрингу... Но сначала нужно поговорить про важную уязвимость.
ПОТОК JBIG2
Большая часть декодера CoreGraphics PDF — это проприетарный код Apple, но реализация JBIG2 взята из проекта Xpdf, исходный код которого находится в свободном доступе.
Формат JBIG2 представляет собой набор сегментов, который можно рассматривать как серию команд рисования — они выполняются последовательно за один проход. Анализатор CoreGraphics JBIG2 поддерживает 19 различных типов сегментов, которые включают такие операции, как определение новой страницы, декодирование таблицы Хаффмана и визуализация растрового изображения с заданными координатами.
Сегменты представлены классом JBIG2Segment
и его подклассами JBIG2Bitmap
и JBIG2SymbolDict
. JBIG2Bitmap
представляет собой прямоугольный массив пикселей. Его поле данных указывает на задний буфер, содержащий поверхность для рендеринга. JBIG2SymbolDict
группирует битмапы. А целевая страница представлена как JBIG2Bitmap
и состоит из отдельных глифов. На сегменты (JBIG2Segment
) можно ссылаться по номеру, а векторный тип GList
хранит указатели на все сегменты. Чтобы найти сегмент по его номеру, GList
сканируется последовательно.
Кстати, если ты сейчас прочитал слово «джи‑лист» неправильно и развеселился, как детсадовец, то постарайся сконцентрироваться, нас ждут куда более интересные открытия.
УЯЗВИМОСТЬ
Уязвимость представляет собой классическое целочисленное переполнение при сопоставлении ссылочных сегментов. Вот два сегмента кода, которые делают его возможным.
for (i = 0; i < nRefSegs; ++i) {
if ((seg = findSegment(refSegs[i]))) {
if (seg->getType() == jbig2SegSymbolDict) {
numSyms += ((JBIG2SymbolDict *)seg)->getSize(); // (2)
} else if (seg->getType() == jbig2SegCodeTable) {
error(errSyntaxError, getPos(),
"Invalid segment reference in JBIG2 text region");
syms = (JBIG2Bitmap **)gmallocn(numSyms, sizeof(JBIG2Bitmap *)); // (3)
for (i = 0; i < nRefSegs; ++i) {
if ((seg = findSegment(refSegs[i]))) {
if (seg->getType() == jbig2SegSymbolDict) {
symbolDict = (JBIG2SymbolDict *)seg;
for (k = 0; k < symbolDict->getSize(); ++k) {
syms[kk++] = symbolDict->getBitmap(k); // (4)
Здесь переменная numSyms
объявлена как 32-битное целое (смотри пометку 1). Если мы будем раз за разом добавлять специально подготовленные ссылочные сегменты (2), в конце концов это приведет к переполнению numSyms
до контролируемого небольшого значения. Это значение используется для выделения кучи (3), из чего следует, что syms
будет указывать на буфер недостаточного размера. Во внутреннем цикле (4) значения указателя JBIG2Bitmap
записываются в буфер syms
меньшего размера.
Без дополнительных ухищрений этот цикл записал бы более 32 Гбайт данных в непригодный по размерам буфер syms
, а это приведет к сбою. Чтобы такого не происходило, куча обрабатывается так, чтобы первые несколько записей из конца буфера syms
повреждали задний буфер GList
. Список GList
хранит все известные сегменты и используется функцией findSegments
для сопоставления номеров сегментов, переданных в refSegs
, с указателями JBIG2Segment
. Переполнение приводит к перезаписи указателей JBIG2Segment
в GList
указателями JBIG2Bitmap
(4).
Так как JBIG2Bitmap
наследуется от JBIG2Segment
, виртуальный вызов seg->getType()
выполняется успешно даже на устройствах, где включена проверка подлинности указателя (она используется для выполнения слабой проверки типа виртуальных вызовов). Но возвращаемый тип теперь не будет равен jbig2SegSymbolDict
, в результате чего дальнейшая запись не происходит (4) и степень повреждения памяти ограничивается.
БЕСПРЕДЕЛЬНЫЙ И НЕОГРАНИЧЕННЫЙ ДОСТУП
После того как сегменты в списке GList
повреждены, хакер переходит к порче объекта JBIG2Bitmap
, представляющего собой текущую страницу (место, где команды рисования выполняют визуализацию). Проще говоря, JBIG2Bitmap
— это оболочки заднего буфера, хранящие ширину и высоту буфера (в битах), а также значение, которое определяет, сколько байтов хранится для каждой строки.
Если тщательно структурировать refSegs
, они могут остановить переполнение после записи еще трех указателей JBIG2Bitmap
после конца буфера сегментов GList
. Такой подход позволяет перезаписать указатель на vtable
и первые четыре поля JBIG2Bitmap
, представляющих текущую страницу.
Ввиду устройства адресного пространства iOS эти указатели, скорее всего, будут находиться во вторых 4 Гбайт виртуальной памяти с адресами от 0x100000000 до 0x1ffffffff. На девайсах, использующих iOS, применяется прямой порядок байтов (little-endian). Это означает, что поля w
и line
будут перезаписаны на 0x1 — наиболее значимую половину указателя JBIG2Bitmap
, а поля segNum
и h
заменятся наименее значимой половиной этого же указателя — случайным значением, зависящим от размещения кучи и ASLR (рандомизация размещения адресного пространства), где‑то между 0x100000 и 0xffffffff.
В итоге выходит, что целевая страница JBIG2Bitmap
получает слишком большое значение h
. Поскольку это значение используется для проверки границ и должно отражать выделенный размер буфера страницы, получается эффект развертывания рабочей области для вывода изображения. Это означает, что последующие команды сегмента JBIG2 могут читать и записывать память за пределами исходных границ буфера поддержки страницы.
Обработчик кучи также размещает буфер поддержки текущей страницы чуть ниже неподходящего по размерам буфера syms
, поэтому, когда страница JBIG2Bitmap
не ограничена, она может читать и записывать свои собственные поля.
Благодаря отрисовке четырехбайтовых растров с правильными координатами можно производить запись во все поля страницы JBIG2Bitmap
, а тщательно выбирая новые значения для w
, h
и line
, можно записывать произвольные смещения в задний буфер страницы.
На этом этапе уже можно было бы писать абсолютные адреса памяти, если бы мы знали их смещения в заднем буфере страницы. Но как вычислить эти смещения? До сих пор этот эксплоит действовал очень похоже на обычный эксплоит традиционного языка сценариев. В JavaScript это могло бы закончиться неограниченным объектом ArrayBuffer
с доступом к памяти. В таком случае злоумышленник имел бы возможность запускать произвольный код на JavaScript, который бы использовался для вычисления смещений и выполнения произвольных действий. Как это сделать в однопроходном парсере изображений?
ДРУГОЙ ФОРМАТ СЖАТИЯ — ПОЛНЫЙ ПО ТЬЮРИНГУ!
Как ты помнишь, последовательность шагов, реализующих уточнение в JBIG2, очень гибкая. Шаги уточнения могут ссылаться как на растровое изображение вывода, так и на любые ранее созданные сегменты, а также отображать вывод либо на текущей странице, либо на сегменте. Хитроумно работая с зависящей от контекста декомпрессией уточнения, можно создавать последовательности сегментов, в которых эффект будут иметь только операторы комбинации усовершенствований.
На практике это значит, что можно применять логические операторы AND, OR, XOR и XNOR между областями памяти с произвольными смещениями заднего буфера JBIG2Bitmap
текущей страницы. И поскольку ограничений нет, можно выполнять эти логические операции с памятью с произвольными смещениями за пределами границ.
Это немного усложнит задачу, но при желании можно вместо глифов работать с отдельными битами. В качестве входных данных можно подать набор команд сегмента JBIG2, которые реализуют последовательность логических битовых операций, применяемых к странице. А поскольку буфер страницы не ограничен, эти битовые операции могут работать с произвольной памятью.
С помощью доступных логических операторов AND, OR, XOR и XNOR можно выполнить любую математическую функцию.
ВЫВОДЫ
В JBIG2 нет возможности выполнять скрипты, но в сочетании с уязвимостью он может имитировать схемы разных логических вентилей, работающих с произвольной памятью. Так почему бы не использовать это и не создать собственную компьютерную архитектуру для выполнения своих сценариев?
Это как раз то, что делает эксплоит NSO. В нем на основе 70 тысяч сегментных команд реализована архитектура небольшого компьютера. Здесь есть регистры, а также полные 64-битный сумматор и компаратор, которые используются для поиска в памяти и выполнения арифметических операций. Все это работает не так быстро, как JavaScript, но можно добиться схожих результатов.
Операции начальной загрузки, благодаря которым эксплоит выходит из песочницы, написаны целиком на этой причудливой эмулируемой элементарной логике, созданной из одного прохода декомпрессии потока JBIG2.
Хоть деятельность NSO и вызывает вопросы с точки зрения этики, но нельзя не отдать должное изобретательности этой схемы!