Pull to refresh

Подменяем Runtime permissions в Android

Reading time 5 min
Views 7.1K

Здравствуйте, меня зовут Виталий.

Мне 25 лет, закончил магистратуру СПБГЭТУ «ЛЭТИ» в своем родном городе. Уже 10 лет занимаюсь программированием, из которых 4 пишу под Android. Автор многих Homebrew программ, известный под ником VITTACH, для Sony PlayStation Portable (PSP).

Сегодня я бы хотел обсудить с вами проблему безопасности мобильных приложений. Разработчики из Google постоянно улучшают Android, находя и исправляя уязвимости не без помощи обширного сообщества, собранного благодаря программе Android Security Rewards, о которой мы поговорим позже. Тем не менее, проблемы все еще остаются, и наша общая задача как коммьюнити сообщать о них, чтобы их своевременно исправляли.

Уязвимость о которой я буду говорить, относится к классу с Priority:​ P2 и Severity:​ S2, что согласно таблице в широком смысле означает:

  • Проблему, которую необходимо решить в разумные сроки;

  • Проблему, которая важна для большого процента пользователей и связана с основными функциями.

Runtime permission

Речь в статье пойдет о такой известной всем разработчикам вещи как Runtime permission, а именно – о возможности введения в заблуждение конечного пользователя путем демонстрации диалогового окна выдачи разрешения со своим текстом и иконкой поверх системного. Нетрудно догадаться, что подобный подход позволил бы разработчикам запрашивать у пользователя разрешение, скажем, к файловой системе, а по факту – к выдаче доступа к геопозиционированию, камере или чему-то еще.

Это невозможно

Не один раз задавался подобный вопрос на специализированных форумах, в частности на StackOverflow. Единственным правильным ответом было то, что это невозможно. И это действительно так: невозможно подменить текст в самом системном диалоге, но возможно его перекрыть своим.

Что под капотом

Runtime Permission впервые появились в Android 6.0 в ответ на потребность повышенного внимания в области выдачи dangerous-разрешений. Фактически основная идея состоит в том, чтобы взаимодействовать с пользователем при запросе разрешений через всплывающее окно. Поэтому теперь разрешения из списка dangerous необходимо запрашивать у пользователя как только они понадобятся приложению.

Dangerous permissions
  • android.permission_group.CALENDAR

    • android.permission.READ_CALENDAR

    • android.permission.WRITE_CALENDAR

  • android.permission_group.CAMERA

    • android.permission.CAMERA

  • android.permission_group.CONTACTS

    • android.permission.READ_CONTACTS

    • android.permission.WRITE_CONTACTS

    • android.permission.GET_ACCOUNTS

  • android.permission_group.LOCATION

    • android.permission.ACCESSFINELOCATION

    • android.permission.ACCESSCOARSELOCATION

  • android.permission_group.MICROPHONE

    • android.permission.RECORD_AUDIO

  • android.permission_group.PHONE

    • android.permission.READPHONESTATE

    • android.permission.CALL_PHONE

    • android.permission.READCALLLOG

    • android.permission.WRITECALLLOG

    • android.permission.ADD_VOICEMAIL

    • android.permission.USE_SIP

    • android.permission.PROCESSOUTGOINGCALLS

  • android.permission_group.SENSORS

    • android.permission.BODY_SENSORS

  • android.permission_group.SMS

    • android.permission.SEND_SMS

    • android.permission.RECEIVE_SMS

    • android.permission.READ_SMS

    • android.permission.RECEIVEWAPPUSH

    • android.permission.RECEIVE_MMS

    • android.permission.READCELLBROADCASTS

  • android.permission_group.STORAGE

    • android.permission.READEXTERNALSTORAGE

    • android.permission.WRITEEXTERNALSTORAGE

Для отображения системного диалога в Android есть GrantPermissionsActivity, который запускается после запроса на выдачу разрешений и отображает нам диалог со знакомым интерфейсом.

ActivityCompat.requestPermissions(
    MainActivity.this,
    arrayOf(Manifest.permission.READ_CONTACTS),
    PERMISSION_REQUEST_CODE
)

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

Теперь давайте посмотрим на пример:

Пусть есть Activity с флагом android:windowIsTranslucent=true (чтобы сделать Activity с прозрачным фоном, позволяющим видеть, что за ним стоит) и оно запускается другим Activity , который я назову фоновым. Визуально вы все еще можете видеть некоторую часть фонового Activity через прозрачные пиксели в Activity переднего плана.

Синий – это активное Activity с полупрозрачным окном, а фиолетовый – Activity прямо под ним. Что произойдет с Activity, если вы поместите приложение в фоновый режим?

Фоновая активность, несомненно, будет убита, а передний план деятельности может выжить, пока вы не переключитесь на другое приложение. И вот что произойдет, когда вы вернете свое приложение на передний план:

Фоновая Activity создается, а затем onResume и onPause вызываются по порядку. Затем оживает передний план Activity.

Идея заключается в том, чтобы перекрыть эту Activity в стеке своей собственной с кастомным диалогом, но так чтобы оставить возможность взаимодействовать с кнопками системного диалога. И вот это уже – возможно!

Попробуй сам, это не сложно

Для демонстрации использован язык программирования Kotlin

  • Создать стиль

    К сожалению, невозможно настроить эти параметры иначе как из стилей

    <style name="Theme.Transparent" parent="AppTheme">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowIsTranslucent">true</item>
    </style>
  • Прописать в манифесте Activity со стилем

    ...
    <activity android:name=".PermissionActivity"
              android:theme="@style/Theme.Transparent">
  • Создать PermissionActivity со своим layout

    В методе onCreate написать код:

    window.addFlags(
      FLAG_NOT_FOCUSABLE or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_TOUCHABLE
    )

    Здесь мы используем трюк с использованием следующих флагов:

    • FLAG_NOT_FOCUSABLE: window, в которой установлен параметр FLAG_NOT_FOCUSABLE, не может взаимодействовать с методом ввода;

    • FLAG_NOT_TOUCH_MODAL: разрешит отправку любых событий за пределы окна в окна позади него, в противном случае он сам будет использовать все события указателя, независимо от того, находятся ли они внутри окна;

    • FLAG_NOT_TOUCHABLE: это окно никогда не может получать события касания.

  • В MainActivity вызвать запрос

    ActivityCompat.requestPermissions(
        MainActivity.this,
        arrayOf(Manifest.permission.READ_CONTACTS),
        REQUEST_CODE
    )
  • И сразу после этого из MainActivity запустить другую активити: PermissionActivity.

    startActivity(Intent(this, PermissionActivity::class.java))

    PermissionActivity с прозрачным фоном и отсутствием фокуса и прокидыванием событий на Activity позади себя запустится и позволит добиться перекрытия системного диалога с сохранением полного взаимодействия с ним. Результат достигнут!

Android >= 7.1.1

Хотя Runtime Permission появились в версии Android 6.0, но до версии 7.1.1 эту уязвимость использовать не получится, т.к. ранние версии Android ругаются на обнаружение перекрытия при попытке нажатия на кнопку Разрешить.

Если вы попробуете запустить мой код на версии Android 6.0, то получите это предупреждение. По неизвестной мне причине, Google отказался от этих ограничений в новых версиях.

Android Rewards Program

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

А как проще?

Для удобства эксплуатации уязвимости мною была написана библиотека

Tags:
Hubs:
+16
Comments 11
Comments Comments 11

Articles