Подменяем Runtime permissions в Android
Здравствуйте, меня зовут Виталий.
Мне 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_group.CAMERA
android.permission_group.CONTACTS
android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS
android.permission_group.LOCATION
android.permission_group.MICROPHONE
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_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 есть 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
Создать стиль
К сожалению, невозможно настроить эти параметры иначе как из стилей
Прописать в манифесте Activity со стилем
...
Создать 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
Я подал заявку и приложил все объяснительные и демонстрационные документы по данной уязвимости. В данный момент заявка находится в стадии рассмотрения, поэтому я не могу разглашать подробности, потому что подписал соответствующее соглашение.
А как проще?
Для удобства эксплуатации уязвимости мною была написана библиотека