Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
AbxOverflow — Разбор и эксплойт для CVE-2024-34740: целочисленное переполнение в BinaryXmlSerializer в Android, ведущее к записи файла в system_server, а затем к выполнению кода в system_server из обычного установленного приложения | Kitploit
Инструменты/GitHubGitHub/michalbednarski/abxoverflow
Безопасность AndroidПовышение привилегийАнализ уязвимостейАнализ КодаЭксплуатацияОбучение и ОбразованиеРазработка Полезной НагрузкиЭксплуатация Бинарных Файлов

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться
GitHub
michalbednarski/abxoverflow

AbxOverflow

Разбор и эксплойт для CVE-2024-34740: целочисленное переполнение в BinaryXmlSerializer в Android, ведущее к записи файла в system_server, а затем к выполнению кода в system_server из обычного установленного приложения

Репозиторий
6825211 месяцев назадПроверено Kitploit

Скриншот Android-приложения с заголовком AbxDroppedApk и множеством текста, описывающего, что оно работает внутри system_server

Исправления для проблемы, описанной здесь, появились под CVE-2024-34740 / A-307288067:

  • Бюллетень
  • Патч, указанный в бюллетене
  • Два других патча: 1 2

Android Binary XML

Внутри Android system_server многие сервисы хранят своё состояние между перезагрузками в XML-файлах``` $ adb shell su 0 find /data/system -name '*.xml' | sort /data/system/appops_accesses.xml /data/system/cachequota.xml /data/system/device_policies.xml /data/system/device_policy_state.xml /data/system/display-manager-state.xml /data/system/input-manager-state.xml /data/system/inputmethod/subtypes.xml /data/system/install_sessions.xml /data/system/job/jobs_1000.xml /data/system/job/jobs_10131.xml /data/system/log-files.xml /data/system/netpolicy.xml /data/system/notification_policy.xml /data/system/overlays.xml /data/system/packages.xml /data/system/package-watchdog.xml /data/system/sensor_privacy_impl.xml /data/system/sensor_privacy.xml /data/system/shortcut_service.xml /data/system/users/0/app_idle_stats.xml /data/system/users/0/appwidgets.xml /data/system/users/0/package-restrictions.xml /data/system/users/0/settings_global.xml /data/system/users/0/settings_secure.xml /data/system/users/0/settings_system.xml /data/system/users/0/wallpaper_info.xml /data/system/users/0.xml /data/system/users/userlist.xml /data/system/watchlist_settings.xml

root@kitploit:~
Исторически это были обычные текстовые XML-файлы с отступами, что позволяло разработчикам легко их читать, однако [в Android 12 была представлена новая двоичная версия этого формата, со ссылкой на то, что 1,5% всего времени, проводимого `system_server`, тратилось на эти XML-операции](https://android.googlesource.com/platform/frameworks/base/+/4ccea8796991d678ead4399130ec31edf63ff4fa%5E%21/)

Следует отметить, что этот формат используется системой только внутренне, и файлы имеют магическое значение `"ABX\x00"`. Он отличается от [формата, используемого внутри APK](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/androidfw/ResourceTypes.cpp;l=1770;drc=d4e49e63519397789d284a03aea5fafc119cb1b0) для `AndroidManifest.xml`, `res/xml/*.xml`, `res/layout/*.xml` и т.д., в котором нет явного «магического значения», однако он обычно начинается с `0300 0800` (это [заголовок](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h;l=608;drc=d4e49e63519397789d284a03aea5fafc119cb1b0) с `type=RES_XML_TYPE` и `headerSize=8`)

Когда система читает один из этих внутренних XML-файлов состояния, она [использует магическое значение `"ABX\0"` в файле, чтобы выбрать либо парсер для двоичного XML-файла, либо обычный XML-парсер](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Xml.java;l=188-192;drc=97a370a95275e79c69e79d7ead11aa38934a5575). Сохранение этих файлов в виде двоичного XML [управляется системным свойством](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Xml.java;drc=97a370a95275e79c69e79d7ead11aa38934a5575;l=74?q=Xml.java) и включено по умолчанию

Когда используются двоичные XML-файлы, их содержимое можно прочитать, например, с помощью `adb shell su 0 abx2xml /data/system/packages.xml -`

Одна из функций этого двоичного формата — предоставление типизированных аксессоров, поэтому сериализатор предлагает метод `attributeInt(String namespace, String name, int value)`, который записывает значение как двоичное целое число, избегая преобразования через String, которое потребовало бы нового выделения памяти и последующего объекта для сборщика мусора

Другой тип, который может быть сериализован напрямую, — это массив байтов```java
@Override
public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
        throws IOException {
    if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
    mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
    mOut.writeInternedUTF(name);
    mOut.writeShort(value.length);
    mOut.write(value);
    return this;
}

Существует также аналогичный метод attributeBytesHex, который отличается лишь записываемым тегом TYPE_*. Этот тег используется инструментом abx2xml для преобразования массива байт в соответствующее строковое представление.

mOut — это экземпляр FastDataOutput, который предоставляет функции Java-класса DataOutputStream. writeByte/writeShort/writeInt/writeUTF/write используют тот же формат, что и стандартный DataOutputStream.

Аналогично Parcel, если при записи/чтении возникает несоответствие, последующие читаемые данные будут взяты с неправильных смещений, однако, в отличие от Parcel, ошибки при использовании BinaryXmlSerializer/BinaryXmlPullParser не дают атакующему возможности произвольно изменять читаемые данные (в этом случае атакующий не может ввести новые имена тегов/атрибутов или значения).

Ошибки же внутри самого класса BinaryXmlSerializer или в FastDataOutput такую возможность дают.

В приведённом выше методе, если мы попытаемся записать массив байт длиной 65536, мы запишем длину с помощью writeShort(), что фактически запишет 0, после чего будет записано реальное содержимое массива.

Выбор цели для ABX-инъекции

Чтобы использовать это несоответствие, нам нужно выбрать файл, в который мы сможем внедрить произвольный массив байт через attributeBytesBase64 или attributeBytesHex, а также изменение этого файла должно быть ценно для атакующего.

Класс PackageInstaller предоставляет возможность подготовить пакет к установке. Без необходимости каких-либо разрешений любое приложение может записать новый APK для установки во временный каталог. Когда всё необходимое для установки записано, устанавливающее приложение может commit() PackageInstaller.Session, что означает, что оно больше не сможет вносить изменения в файлы установки, и Session готов либо к одобрению пользователем, либо к фактической установке.

Состояние этих операций хранится в /data/system/install_sessions.xml. Устанавливающее приложение может, например, загрузить половину большого APK во временный каталог, созданный Package Manager Service для своего PackageInstaller.Session, затем после перезагрузки возобновить загрузку, записать оставшуюся половину и зафиксировать установку.

Одна из возможностей — записать данные в install_sessions.xml, чтобы пометить сессию как staged, то есть она будет установлена после следующей перезагрузки.

Другая возможность, представленная здесь, — изменение пути к временному каталогу, в котором подготавливаются файлы установки, поскольку openWrite()/openRead() принимают любое допустимое имя файла, если нет обхода пути, и размещают этот файл в каталоге, указанном полем stageDir, которое читается из XML.

Эксплуатация ABX-инъекции

Теперь нам нужно фактически передать наш контролируемый массив байт в attributeBytesBase64().

PackageInstaller.Session предлагает метод setChecksums().

На стороне system_server переданные Checksum-ы при необходимости проверяются по подписи, предоставленной вызывающей стороной, а затем помещаются в mChecksums.

При записи install_sessions.xml checksum.getValue() передаётся в writeByteArrayAttribute, который, в свою очередь, передаёт его в attributeBytesBase64().

Существует несколько событий, которые запускают запись install_sessions.xml, одно из которых — создание новой Session. Поэтому данный эксплойт после установки Checksum на одной сессии создаёт новую Session, чтобы гарантировать, что первая Session была сохранена в файл.

Теперь мы записываем массив байт длиной 65536; после чтения его размер интерпретируется как ноль, а содержимое этого массива становится исходными данными, которые анализирует BinaryXmlPullParser.

Количество атрибутов не указывается; каждая запись имеет байт тега, содержащий token. В младшей полубайте находится один из типов событий, определённых в XmlPullParser, таких как START_TAG, END_TAG или END_DOCUMENT. Помимо этих типов, существует специальный тип ATTRIBUTE, который не сообщается через next(), но вместо этого после обнаружения токена START_TAG анализатор просматривает следующие токены, пока не увидит токен, отличный от ATTRIBUTE.

Поскольку количество атрибутов не указано, мы можем сразу перейти к закрытию текущего элемента через токен END_TAG. Затем мы также закрываем </session>, поскольку все интересующие атрибуты элементов находятся в открывающем теге <session>, но мы уже прошли эту точку. Однако теперь мы можем открыть новый элемент <session> и задать их там.

Как отмечено выше, FastDataInput совместим с Java DataInputStream, за исключением дополнительного метода readInternedUTF(), который может ссылаться на ранее прочитанные строки. Поскольку мы не знаем, какие строки были интернированы ранее, мы всегда указываем, что была записана ранее не встречавшаяся строка. Это также добавляет только что прочитанные строки в пул, что может вызвать проблему при чтении данных, записанных после нашей точки инъекции. Однако в рамках инъекции я вставляю все закрывающие теги и токен END_DOCUMENT, так что после моей инъекции из этого файла больше ничего читаться не будет.

Использование PackageInstaller.Session с изменённым stageDir

Как только система прочитает изменённый install_sessions.xml, мы получим объект PackageInstallerSession с stageDir, установленным на контролируемое нами значение.

Моей первой идеей было установить stageDir в /proc/self, затем прочитать maps и записать в mem, но это не сработало.

Когда я попытался использовать openRead(), чтобы открыть /proc/self/maps, system_server успешно открыл файл, однако передача этого файла в untrusted_app через Binder была заблокирована SELinux.

Запись же осуществляется не путём передачи сырого файлового дескриптора другому процессу, а через проксирование через system_server, поскольку system_server должен иметь возможность отозвать доступ на запись после фиксации сессии. Означает ли это, что мы можем записать в /proc/self/mem? Оказывается, что хотя system_server может открыть этот файл, перед записью чего-либо он вызывает Os.chmod() для этого файла, что он не может сделать для /proc/self/mem. Поэтому мы не можем использовать это для эксплуатации здесь, хотя в остальном system_server способен открыть этот файл и выполнять записи по указанным нами смещениям, а этот файл позволяет перезаписывать страницы кода, что напрямую дало бы нам выполнение кода.

Поскольку этот вариант недоступен, я попробовал следующую идею — заменить содержимое /data/system/packages.xml. Это файл, который содержит состояние PackageManagerService, прежде всего то, какие приложения установлены и какие uid им назначены.

Похоже, что system_server не имеет права напрямую записывать в этот файл: вместо этого при записи система сначала записывает во временный файл, а затем заменяет packages.xml этим временным файлом и включает защиту на нём.

Однако при чтении /data/system/packages.xml система сначала проверит, присутствует ли файл /data/system/packages-backup.xml, и если да, то сочтёт основной packages.xml повреждённым и прочитает резервную копию вместо него. При нормальной работе файл /data/system/packages-backup.xml отсутствует, и мы можем создать его с помощью сконструированного PackageInstallerSession с stageDir, установленным в /data/system.

Кроме того, system_server может отправить файловый дескриптор /data/system/packages.xml только для чтения, когда я использую openRead(), поэтому я могу легко собрать пропатченный файл, содержащий только мои изменения, не повреждая предыдущее содержимое.

Предоставление доступа sharedUserId="android.uid.system"

В packages.xml у меня есть определения установленных приложений, зарегистрированных, например:```xml <package name="com.android.settings" codePath="/system_ext/priv-app/Settings" ... sharedUserId="1000" ...>

root@kitploit:~
Можем ли мы записать новый APK куда-нибудь в `/data/app` (используя другой `PackageInstallerSession`) и добавить новый элемент `<package>` в `packages.xml`, чтобы приложение было установлено таким образом?

Да, однако мы должны указать в `<cert>` действительную подпись нашего только что установленного APK, и система проверит её по файлу APK во время загрузки

Можем ли мы задать атрибуту `userId` (который используется вместо `sharedUserId` для указания APK без атрибута `<manifest android:sharedUserId>` в `AndroidManifest.xml`) нужное нам значение?

Да, однако мы не должны использовать значение, которое уже используется другим пакетом или `sharedUserId`

Можем ли мы задать `sharedUserId="1000"` для нашего приложения?

Если мы это сделаем, система во время загрузки проверит эту настройку через [`canJoinSharedUserId()`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java;l=658-750;drc=7ec13b04c3bbaeac99cbbc4db9f9f80492c508fe)

В частности, этот метод использует [`checkCapability()`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/SigningDetails.java;l=613-637;drc=97a370a95275e79c69e79d7ead11aa38934a5575), чтобы проверить, совпадают ли подписи полностью, либо подпись с одной стороны совпадает с одной из прошлых подписей с другой стороны

Эти «прошлые подписи» берутся из `packages.xml`: в частности, когда у нас есть элемент `<sigs>` с `<cert>`, мы можем добавить элемент `<pastSigs>` внутрь `<sigs>`, чтобы добавить новые записи в [`SigningDetails.mPastSigningCertificates`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/SigningDetails.java;l=74-87;drc=97a370a95275e79c69e79d7ead11aa38934a5575)

В итоге наш изменённый элемент `<shared-user>` выглядит вот так:```xml
<shared-user name="android.uid.system" userId="1000">
  <sigs count="1" schemeVersion="3">
    <cert index="3" />
    <pastSigs count="2" schemeVersion="3">
      <cert index="19" flags="2" />
      <cert index="19" flags="2" />
    </pastSigs>
  </sigs>
</shared-user>

Элемент <cert> внутри <pastSigs> вставляется дважды, потому что последняя прошлая подпись считается текущей и поэтому не учитывается

flags="2" означает, что сертификат разрешён для sharedUserId

Кроме того, регистрация <package sharedUserId="1000"> должна применяться к приложению, объявляющему android:sharedUserId="android.uid.system" в манифесте, поэтому это должен быть отдельный APK от того, который выполняет эксплуатацию

Недавно установленное приложение с системным uid не запускается

Хотя мне и удалось зарегистрировать новый сертификат, доверенный для android:sharedUserId="android.uid.system", обычно приложение, подписанное этим сертификатом и объявляющее в манифесте только sharedUserId, не смогло бы запуститься. При его запуске в logcat появится следующее сообщение:``` signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr -------- Abort message: 'JNI FatalError called: (com.example.abxoverflow.droppedapk) frameworks/base/core/jni/com_android_internal_os_Zygote.cpp:1976: selinux_android_setcontext(1000, 0, "default:privapp:targetSdkVersion=33:complete", "com.example.abxoverflow.droppedapk") failed'

root@kitploit:~
Это потому, что ни одно из определений в [файле `seapp_contexts`](https://cs.android.com/android/platform/superproject/main/+/main:system/sepolicy/private/seapp_contexts) не совпало

Правило `user=` в этом файле [выводится из `uid`](https://cs.android.com/android/platform/superproject/main/+/main:external/selinux/libselinux/src/android/android_seapp.c;l=819-833;drc=530165a996d8ca5ab5959c33bc040c78951bcb59) (первый аргумент `selinux_android_setcontext()`), в нашем случае это будет `user=system`, для обычных приложений — `user=_app`

Ещё одно условие для совпадения — правило `seinfo=`, которое берётся из третьего аргумента `selinux_android_setcontext()` до первого двоеточия. Изначально это значение получается при сравнении подписи запускаемого приложения с подписями, определёнными в `/system/etc/selinux/plat_mac_permissions.xml`

В итоге наше приложение пытается сопоставить `user=system seinfo=default`, и в `seapp_contexts` нет такого правила

Однако, хотя процесс для нашего нового приложения с `android:sharedUserId="android.uid.system"` не может быть запущен, приложение всё же может быть загружено в существующий процесс, если указан [атрибут `android:process`](https://developer.android.com/guide/topics/manifest/application-element#proc). В частности, приложения, работающие под `android.uid.system`, могут указывать `android:process="system"`, чтобы быть загруженными в `system_server`

# Падение системы

В целом [вызов приложением падения `system_server` считается ошибкой с пренебрежимо малым влиянием на безопасность](https://bughunters.google.com/learn/invalid-reports/android-platform/5148417640366080/bugs-with-negligible-security-impact#triggering-a-local-temporary-denial-of-service), и здесь об этом стоит упомянуть лишь потому, что это часть цепочки эксплойта, требующей двух перезапусков `system_server`

В любом случае, у нас есть цепочка `Parcelable`:

* [AIDL-метод `IAlarmManager.set()` принимает `AlarmManager.AlarmClockInfo`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl;l=32-35;drc=ca41ed611ac9c6584c6d5c38ae8428b8e4f3b135)
* [`AlarmClockInfo` вызывает устаревший `readParcelable()` без аргумента типа](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/apex/jobscheduler/framework/java/android/app/AlarmManager.java;l=1598;drc=04bf84e220ade9d7ad8ef0b2f7e6ce6ec72841c8) (потому что он находится в модуле apex, и эти вызовы не были переведены на новые методы)
* Я указываю [`android.content.pm.PackageParser$Activity`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=8240;drc=7d3ffbae618e9e728644a96647ed709bf39ae759) в качестве класса `Parcelable`
* Его чтение приводит к [вызову любого публичного конструктора, принимающего единственный аргумент `Parcel`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759)
* Я указываю [`android.os.PooledStringWriter`, который вызывает `writeInt(0)` на переданном `Parcel`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/PooledStringWriter.java;l=55;drc=782d49826862cbdc9d020fc9d85f8a6f64675dcb)
* Этот вызов `writeInt()` был выполнен на объекте `Parcel`, полученном в качестве аргумента `data` метода [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)), который основан на памяти только для чтения, отображаемой через `mmap` из `/dev/binder`. Запись в него вызывает `SIGSEGV`

Также стоит отметить, что [я использовал комбинацию `PackageParser`+`PooledStringWriter` в предыдущих отчётах, например, для CVE-2023-21098](https://github.com/michalbednarski/TheLastBundleMismatch)

# Весь процесс

Вот что происходит, когда вы нажимаете кнопку «Do everything» в приложении

1. `RebootBackgroundRunner` запускается как отдельный процесс, который теперь просто использует [`setsid()`](https://man7.org/linux/man-pages/man2/setsid.2.html), чтобы пережить перезагрузку userspace, и после этого будет ожидать в фоне
2. Выделяется новый `PackageInstaller.Session`, и в него добавляется новый объект `Checksum`. Этот объект `Checksum` содержит массив байтов, размер которого вызовет целочисленное переполнение при сериализации, и как только его данные будут десериализованы обратно, система увидит сессии `PackageInstaller.Session`, чьи данные ранее были полезной нагрузкой `Checksum`. В частности, внедряются две сессии
    * Одна с `sessionStageDir="/data/system"` и `prepared="true"` (то есть каталог стадии уже готов и его не нужно создавать)
    * Одна с `sessionStageDir="/data/app/dropped_apk"` и `prepared="false"` (то есть каталог будет создан при первом `Session.openWrite()`)
3. Выделяется новый `PackageInstaller.Session`, который затем немедленно уничтожается. Это заставляет систему записать обновлённое содержимое в `install_sessions.xml`
4. Через небольшую задержку вызывается падение `system_server`
5. При следующем запуске `system_server` файл `install_sessions.xml` считывается, и теперь внедрённые нами сессии `PackageInstaller.Session` могут быть использованы
6. `RebootBackgroundRunner` ожидал в фоне во время перезагрузки userspace, и как только он замечает, что система снова поднялась и готова, он выполняет следующие шаги
7. С помощью одной `PackageInstaller.Session` новый APK извлекается из assets и записывается в `/data/app/dropped_apk/base.apk`
8. Другая сессия используется для чтения `/data/system/packages.xml`; этот файл модифицируется так, чтобы объявить, что только что размещённый APK уже установлен, а сертификат, использованный для него, ранее использовался для `android:sharedUserId="android.uid.system"` и всё ещё доверен для этой цели. Изменённый файл записывается как `/data/system/packages-backup.xml`
9. Вызывается ещё одно падение `system_server`
10. Когда `system_server` при запуске видит `packages-backup.xml`, он считает оригинальный `packages.xml` повреждённым и использует вместо него резервную копию
11. Так как система прочитала изменённый `packages.xml`, только что размещённое приложение присутствует и запускается само по событию [`ACTION_BOOT_COMPLETED`](https://developer.android.com/reference/android/content/Intent#ACTION_BOOT_COMPLETED). Это новое приложение работает внутри `system_server`, потому что в `AndroidManifest.xml` у него есть `<manifest android:sharedUserId="android.uid.system">` и `<application android:process="system">`

# Скрипты в `utils/`

Вместе с PoC-приложением имеется каталог `utils` с несколькими скриптами

* `moveapk.sh` перемещает скомпилированный APK для размещения в `assets` дроппера; запускать после `gradle :droppedapk:assembleRelease`
* `peeksessions.sh` позволяет просматривать текущее содержимое `install_sessions.xml` (требуется сборка Android `eng`/`userdebug`)
* `wipesessions.sh` очищает все имеющиеся сессии `PackageInstaller.Session` и перезапускает систему (требуется сборка Android `eng`/`userdebug`)

# Интересные факты

Не уверен, связано ли это, но, просматривая историю на предмет возможных ошибок, связанных с ABX (`cd frameworks/base ; git log -S ABX`), я нашёл [коммит "Stop processing on IOException"](https://android.googlesource.com/platform/frameworks/base/+/5112cfef2a2023a2629a426154547444593e9f9b%5E!/), который **включает добавление модульного теста с усечённым ABX-файлом**. Этот коммит был продолжением ["Ignore malformed shortcuts"](https://android.googlesource.com/platform/frameworks/base/+/d5122bfaf18f1503e73c1a3a177a56d0f604a008%5E%21/), который был [описан в бюллетене как DoS](https://source.android.com/docs/security/bulletin/2022-12-01#framework)
Скачать инструмент