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

 

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. Оформи подписку на «Хакер», чтобы читать все статьи на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта, включая эту статью. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи одну статью

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


Комментарии

Подпишитесь на ][, чтобы участвовать в обсуждении

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

Check Also

Мобильные приложения ряда крупных банков уязвимы перед MitM-атаками

Исследователи из университета Бирмингема предупредили, что приложения многих крупных банко…