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

 

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.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


Check Also

Рекламный вредонос маскируется под блокировщик рекламы

Эксперты обнаружили вредоносное приложение для Android, которое выдает себя за блокировщик…

Оставить мнение