Введение
В середине апреля мы опубликовали новость о троянце Android.InfectionAds.1, который эксплуатировал несколько критических уязвимостей в ОС Android. Одна из них — CVE-2017-13156 (также известна как Janus) — позволяет вредоносной программе заражать APK-файлы, не повреждая их цифровую подпись.
Другая — CVE-2017-13315. Она дает троянцу расширенные полномочия, и тот может самостоятельно устанавливать и удалять приложения. Детальный анализ Android.InfectionAds.1 размещен в нашей вирусной библиотеке, с ним можно ознакомиться здесь. Мы же подробнее остановимся на уязвимости CVE-2017-13315 и посмотрим, что она из себя представляет.
CVE-2017-13315 относится к группе уязвимостей, получивших общее наименование EvilParcel. Они обнаруживаются в различных системных классах ОС Android. Из-за ошибок в последних при обмене данными между приложениями и системой становится возможной подмена этих данных. Вредоносные программы, эксплуатирующие уязвимости EvilParcel, получают более высокие привилегии и с их помощью могут делать следующее:
- устанавливать и удалять приложения с любыми разрешениями без подтверждения пользователя;
- при использовании совместно с другими уязвимостями заражать установленные на устройстве программы и подменять «чистые» оригиналы инфицированными копиями;
- сбрасывать код блокировки экрана Android-устройства;
- сбрасывать PIN-код блокировки экрана Android-устройства.
На данный момент известно о 7 уязвимостях этого типа:
- CVE-2017-0806 (ошибка в классе GateKeeperResponse), опубликована в октябре 2017 г.;
- CVE-2017-13286 (ошибка в классе OutputConfiguration, опубликована в апреле 2018 г.;
- CVE-2017-13287 (ошибка в классе VerifyCredentialResponse), опубликована в апреле 2018 г.;
- CVE-2017-13288 (ошибка в классе PeriodicAdvertizingReport), опубликована в апреле 2018 г.;
- CVE-2017-13289 (ошибка в классе ParcelableRttResults), опубликована в апреле 2018 г.;
- CVE-2017-13311 (ошибка в классе SparseMappingTable), опубликована в мае 2018 г.;
- CVE-2017-13315 (ошибка в классе DcParamObject), опубликована в мае 2018 г.
Все они угрожают устройствам под управлением ОС Android версий 5.0 — 8.1, на которых не установлено майское обновление безопасности 2018 года и более поздние.
Предпосылки для возникновения уязвимостей EvilParcel
Давайте разберемся, как возникают уязвимости EvilParcel. Прежде всего, рассмотрим некоторые особенности работы Android-приложений. В ОС Android все программы взаимодействуют друг с другом, а также с самой операционной системой через отправку и получение объектов типа Intent. Эти объекты могут содержать произвольное количество пар «ключ-значение» внутри объекта типа Bundle.
При передаче Intent объект Bundle преобразуется (сериализуется) в байтовый массив, облаченный в Parcel, а при чтении ключей и значений из сериализованного Bundle тот автоматически десериализуется.
В Bundle ключом выступает строка, а значение может быть практически любым. Например, примитивным типом, строкой или контейнером, содержащим примитивные типы или строки. Кроме того, он может быть и объектом типа Parcelable.
Таким образом, в Bundle можно поместить объект произвольного типа, реализующий интерфейс Parcelable. Для этого потребуется реализовать методы writeToParcel() и createFromParcel() для сериализации и десериализации объекта.
В качестве наглядного примера создадим простой сериализованный Bundle. Напишем код, который поместит в Bundle три пары «ключ-значение» и сериализует его:
Bundle demo = new Bundle();
demo.putString(«String», «Hello, World!»);
demo.putInt(«Integer», 42);
demo.putByteArray(«ByteArray», new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
Parcel parcel = Parcel.obtain();
parcel.writeBundle(demo);
После выполнения этого кода мы получим Bundle следующего вида:
Рисунок 1. Структура сериализованного объекта Bundle.
Обратим внимание на следующие особенности сериализации Bundle:
- все пары ключ-значение записаны друг за другом;
- перед каждым значением указан его тип (13 для массива байт, 1 для Integer, 0 для строки и так далее);
- перед данными переменной длины указан их размер (длина для строки, количество байт для массива);
- все значения записаны с выравниванием 4 байта.
Из-за того, что все ключи и значения в Bundle записаны последовательно, при обращении к какому-либо ключу или значению сериализованного объекта Bundle последний десериализуется полностью, в том числе инициализирует все содержащиеся в нём объекты Parcelable.
Казалось бы, в чем может быть проблема? А она – в том, что в некоторых системных классах, реализующих Parcelable, в методах createFromParcel() и writeToParcel() могут встречаться ошибки. В этих классах количество прочитанных байтов в методе createFromParcel() будет отличаться от количества записанных байтов в методе writeToParcel(). Если поместить объект такого класса внутрь Bundle, границы объекта внутри Bundle после повторной сериализации изменятся. И именно здесь создаются условия для эксплуатации уязвимости EvilParcel.
Приведем пример класса с подобной ошибкой:
class Demo implements Parcelable {
byte[] data;
public Demo() {
this.data = new byte[0];
}
protected Demo(Parcel in) {
int length = in.readInt();
data = new byte[length];
if (length > 0) {
in.readByteArray(data);
}
}
public static final Creator<Demo> CREATOR = new Creator<Demo>() {
@Override
public Demo createFromParcel(Parcel in) {
return new Demo(in);
}
};
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeInt(data.length);
parcel.writeByteArray(data);
}
}
Если размер массива data будет равен 0, то при создании объекта в createFromParcel() будет прочитан один int (4 байта), а в writeToParcel() будет записано два int (8 байт). Первый int будет записан явным вызовом writeInt. Второй int будет записан при вызове writeByteArray(), поскольку перед массивом в Parcel всегда записывается его длина (см. Рисунок 1).
Ситуации, когда размер массива data равен 0, возникают редко. Но даже когда это случается, программа все равно продолжает работать, если за один раз передавать в сериализованном виде только один объект (в нашем примере — объект Demo). Поэтому подобные ошибки, как правило, остаются незамеченными.
Теперь попробуем поместить объект Demo с нулевой длиной массива в Bundle:
Рисунок 2. Результат добавления объекта Demo с нулевой длиной массива в Bundle.
Сериализуем объект:
Рисунок 3. Объект Bundle после сериализации.
Попробуем его десериализовать:
Рисунок 4. После десериализации объекта Bundle.
Каков же результат? Рассмотрим фрагмент Parcel:
Рисунок 5. Структура Parcel после десериализации Bundle.
Из рисунков 4 и 5 мы видим, что при десериализации в методе createFromParcel был прочитан один int вместо двух записанных ранее. Поэтому все последующие значения из Bundle были прочитаны неверно. Значение 0x0 по адресу 0x60 было прочитано как длина следующего ключа. А значение 0x1 по адресу 0x64 было прочитано как ключ. При этом значение 0x31 по адресу 0x68 было прочитано как тип значения. В Parcel нет значений, тип которых равен 0x31, поэтому readFromParcel() добросовестно отрапортовал об ошибке (exception).
Как это может использоваться на практике и стать уязвимостью? Давайте посмотрим! Описанная выше ошибка в системных классах Parcelable позволяет конструировать Bundle, которые при первой и повторной десериализациях могут отличаться. Чтобы это продемонстрировать, модифицируем предыдущий пример:
Parcel data = Parcel.obtain();
data.writeInt(3); // 3 entries
data.writeString("vuln_class");
data.writeInt(4); // value is Parcelable
data.writeString("com.drweb.testbundlemismatch.Demo");
data.writeInt(0); // data.length
data.writeInt(1); // key length -> key value
data.writeInt(6); // key value -> value is long
data.writeInt(0xD); // value is bytearray -> low(long)
data.writeInt(-1); // bytearray length dummy -> high(long)
int startPos = data.dataPosition();
data.writeString("hidden"); // bytearray data -> hidden key
data.writeInt(0); // value is string
data.writeString("Hi there"); // hidden value
int endPos = data.dataPosition();
int triggerLen = endPos - startPos;
data.setDataPosition(startPos - 4);
data.writeInt(triggerLen); // overwrite dummy value with the real value
data.setDataPosition(endPos);
data.writeString("A padding");
data.writeInt(0); // value is string
data.writeString("to match pair count");
int length = data.dataSize();
Parcel bndl = Parcel.obtain();
bndl.writeInt(length);
bndl.writeInt(0x4C444E42); // bundle magic
bndl.appendFrom(data, 0, length);
bndl.setDataPosition(0);
Этот код создает сериализованный Bundle, который содержит в себе уязвимый класс. Посмотрим на результат выполнения данного кода:
Рисунок 6. Создание Bundle с уязвимым классом.
После первой десериализации этот Bundle будет содержать следующие ключи:
Рисунок 7. Результат десериализации Bundle с уязвимым классом.
Теперь вновь сериализуем полученный Bundle, затем опять десериализуем его и посмотрим на список ключей:
Рисунок 8. Результат повторной сериализации и десериализации Bundle с уязвимым классом.
Что мы видим? В Bundle появился ключ hidden (со строковым значением «Hi there!»), которого раньше не было. Рассмотрим фрагмент Parcel этого Bundle, чтобы понять, почему так произошло:
Рисунок 9. Структура Parcel объекта Bundle с уязвимым классом после двух циклов сериализации-десериализации.
Здесь суть уязвимостей EvilParcel становится более понятной. Возможно создать специально сформированный Bundle, который будет содержать уязвимый класс. Изменение границ такого класса позволит разместить в этом Bundle любой объект – например, Intent, который появится в Bundle только после второй десериализации. Это даст возможность скрыть Intent от механизмов защиты операционной системы.
Эксплуатация EvilParcel
Android.InfectionAds.1 с помощью CVE-2017-13315 самостоятельно устанавливал и удалял программы без вмешательства владельца зараженного устройства. Но как это происходит?
В 2013 году была обнаружена ошибка 7699048, также известная как Launch AnyWhere. Она позволяла стороннему приложению запускать произвольные activity от имени более привилегированного пользователя system. На диаграмме ниже показан механизм ее действия:
Рисунок 10. Схема работы ошибки 7699048.
С помощью этой уязвимости приложение-эксплойт может реализовать сервис AccountAuthenticator, который предназначен для добавления новых аккаунтов в операционную систему. Благодаря ошибке 7699048 эксплойт способен запускать activity на установку, удаление, замену приложений, сброс PIN-кода или Pattern Lock и делать другие неприятные вещи.
Корпорация Google устранила эту брешь, запретив запуск из AccountManager произвольных activity. Теперь AccountManager разрешает запуск только activity, исходящих от того же самого приложения. Для этого выполняется проверка и сопоставление цифровой подписи программы, инициировавшей запуск activity, с подписью приложения, в котором находится запускаемая activity. Выглядит это так:
if (result != null
&& (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
/*
* The Authenticator API allows third party authenticators to
* supply arbitrary intents to other apps that they can run,
* this can be very bad when those apps are in the system like
* the System Settings.
*/
int authenticatorUid = Binder.getCallingUid();
long bid = Binder.clearCallingIdentity();
try {
PackageManager pm = mContext.getPackageManager();
ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mAccounts.userId);
int targetUid = resolveInfo.activityInfo.applicationInfo.uid;
if (PackageManager.SIGNATURE_MATCH !=
pm.checkSignatures(authenticatorUid, targetUid)) {
throw new SecurityException(
"Activity to be started with KEY_INTENT must " +
"share Authenticator's signatures");
}
} finally {
Binder.restoreCallingIdentity(bid);
}
}
Казалось бы, проблема решена, однако не все здесь так гладко. Оказалось, что данный фикс можно обойти, используя уже хорошо нам известную уязвимость EvilParcel CVE-2017-13315! Как мы уже знаем, после исправления Launch AnyWhere система проверяет цифровую подпись приложения. Если эта проверка выполняется успешно, Bundle передаётся в IAccountManagerResponse.onResult(). При этом вызов onResult() происходит через механизм IPC, поэтому Bundle снова сериализуется. В реализации onResult() происходит следующее:
/** Handles the responses from the AccountManager */
private class Response extends IAccountManagerResponse.Stub {
public void onResult(Bundle bundle) {
Intent intent = bundle.getParcelable(KEY_INTENT);
if (intent != null && mActivity != null) {
// since the user provided an Activity we will silently start intents
// that we see
mActivity.startActivity(intent);
// leave the Future running to wait for the real response to this request
}
//<.....>
}
//<.....>
}
Далее Bundle извлекается ключ intent и запускается activity уже без проверок. В результате для запуска произвольной activity с правами system достаточно лишь сконструировать Bundle таким образом, чтобы при первой десериализации поле intent было скрыто, а при повторной – появилось. А, как мы уже убедились, как раз эту задачу и выполняют уязвимости EvilParcel.
На данный момент все известные уязвимости этого типа исправлены фиксами в самих уязвимых классах Parcelable. Однако нельзя исключать повторного появления уязвимых классов в будущем. Реализация Bundle и механизм добавления новых аккаунтов по-прежнему остаются теми же, что и раньше. Они до сих пор позволяют создать точно такой же эксплойт при обнаружении (или появлении новых) уязвимых классов Parcelable. Более того, реализация этих классов все еще выполняется вручную, и за соблюдением постоянной длины сериализованного объекта Parcelable должен следить программист. А это — человеческий фактор со всеми вытекающими. Однако будем надеяться, что подобных ошибок будет как можно меньше, и уязвимости EvilParcel не станут докучать пользователям Android-устройств.
Проверить свое мобильное устройство на наличие уязвимостей EvilParcel можно с помощью нашего антивируса Dr.Web Security Space. Встроенный в него «Аудитор безопасности» сообщит о выявленных проблемах и даст рекомендации по их устранению.
KarasikovSergey
Один из двух крутецких докладов на Оффзоне, почему-то в зале было 3.5 зрителя, в то время как на всяких паяльниках и в коридоре чиллила толпа народу.