Android Kiosk: как купить сухарики, когда ларёк закрыт

Android Kiosk: как купить сухарики, когда ларёк закрыт — Cybersecurity | Versia.media

Несколько месяцев назад один мой коллега готовился к длительному перелёту — около 8 часов. Ему быстро стало скучно, и он обратил внимание на экран мультимедийной системы, встроенный в спинку кресла напротив. Он запустил карту полёта и обнаружил, что это Android-устройство с активированным Kiosk Mode. Данный режим призван препятствовать (или, как минимум, существенно затруднять) исследование устройства, однако на деле всё оказалось с точностью до наоборот. Коллега без каких-либо усилий обошёл Kiosk Mode на планшете и прочитал файл /etc/shadow. Именно тогда и возникла идея изучить Kiosk Mode более детально и выяснить, насколько он на самом деле безопасен.

В этой статье я попытаюсь объяснить внутреннее устройство Kiosk Mode в Android, расскажу об основных компонентах, необходимых приложению для превращения устройства в kiosk-девайс, а также приведу несколько примеров реальной эксплуатации kiosk-устройств и приложений.

Данный материал был впервые представлен в качестве доклада на zeronight.

Что такое Kiosk Mode

Начнём с простого: что же такое Kiosk Mode?

Kiosk Mode — это режим на различных платформах (Windows, Linux, Android), предназначенный для ограничения возможностей пользователя на устройстве одним или несколькими приложениями. Если говорить просто, при использовании Kiosk Mode мы пытаемся «запереть» пользователя в заранее выбранных приложениях. При этом крайне важно, чтобы пользователь оставался именно в этих приложениях. Выход из Kiosk Mode в самом безобидном случае полностью «ломает» функционал устройства, а в худшем — может привести к финансовым потерям или компрометации всей локальной сети, в которой находится kiosk-устройство.

Kiosk Mode довольно широко распространён, хотя в сообществе ИБ о нём говорят нечасто. Он используется в POS-терминалах (недавно вышла статья, где автор подробно разбирает такое применение Kiosk Mode), постаматах, кассах самообслуживания, общественном транспорте, ресторанах в качестве электронного меню, промышленных терминалах и корпоративных устройствах. Однако, несмотря на важность безопасности Kiosk Mode, ею часто пренебрегают.

В данной статье не будет рассматриваться реализация Kiosk Mode под Linux или Windows: я сосредоточусь только на реализации Kiosk Mode в Android.

Устройства с Kiosk Mode можно условно разделить на две группы: Single App и Multi App.

Как понятно из названия, Single App используется, когда возможности пользователя ограничены одним конкретным приложением. Single App обычно применяется для цифровых вывесок, мобильных POS-систем, терминалов в аэропортах, терминалов в индустрии гостеприимства, а также терминалов бронирования билетов. Несмотря на то, что режим Single App проще как в установке на устройство, так и в администрировании, он используется реже.

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

Kiosk-приложение на Android

В этой статье я не буду подробно описывать процесс разработки приложения для Kiosk Mode. Информацию для этого раздела я взял из Github-статьи mrugacz95. Здесь я расскажу лишь об основных механизмах обеспечения «изоляции» пользователя, используемых в kiosk-приложениях на Android.

В приложении, которое полностью ограничивает возможности пользователя, можно выделить три основных компонента:

Home Launcher — это главный экран Android: на нём расположены иконки приложений, виджеты и прочие объекты, которые пользователь может туда поместить. Если наше приложение не будет лаунчером по умолчанию, то после перезагрузки Kiosk Mode перестанет работать, и устройство вернётся к своим стандартным функциям.

Screen Pinning. Закрепление экрана — это функция, позволяющая отображать одно приложение до момента его открепления. Она появилась в Android 5.0 и изначально предназначалась для передачи телефона другим людям без риска утечки информации. После включения этой функции на устройстве происходят следующие изменения: статус-бар полностью скрывается, отключаются кнопки «Домой» и «Недавние приложения», а также вводится запрет на запуск новых действий другими приложениями.

Device Admin. Приложение становится администратором устройства. Если пропустить этот шаг, мы не сможем полностью ограничить пользователя в доступе к уведомлениям и строке состояния, а также не сможем менять системные настройки из приложения, что для kiosk-устройств крайне полезно.

Home Launcher

Чтобы сделать приложение лаунчером, нужно выдать ему интенты, написать соответствующий intentFilter и предоставить необходимые привилегии с помощью addPersistentPreferredActivity.

Далее необходимо отключить Keyguard (экран блокировки). Это позволит нашему приложению запускаться сразу, минуя экран блокировки. Кроме того, необходимо предотвратить выключение экрана. Для этого следует выставить флаг FLAG_KEEP_SCREEN_ON для текущего окна (window принадлежит классу Activity и указывает на текущее окно).

Screen Pinning

Для функционирования Screen Pinning в Android существует специальный механизм Lock Task Mode (режим закрепления задач). Он позволяет закрепить Activity поверх экрана. Для закрепления приложения используются две функции: setLockTaskPackages() в DevicePolicyManager и startLockTask() в Activity.

setLockTaskPackages разрешает приложению закрепляться, а startLockTask обеспечивает закрепление. Финальным шагом закрепления приложения является добавление кода, обеспечивающего включение полноэкранного режима для нашего приложения.

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

Kiosk Mode изнутри

### setLockTaskPackages

Как уже было сказано в предыдущем разделе, функция setLockTaskPackages нужна для того, чтобы система позволила приложению закрепиться. Для этого используется механизм политик, а точнее DevicePolicyEngine (DPE) — внутренний механизм Android, который регламентирует управление политиками.

Суть работы DPE довольно проста:

- Каждый администратор по-своему настраивает систему (например, разрешает или запрещает включение камеры). - DPE на основе заранее заданных правил определяет, какая политика будет принята, после чего формирует итоговую политику, которая будет применена системой.

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

setLockTaskPackages как раз и задаёт такую политику. Сама функция реализована в DevicePolicyManagerService и используется для создания политики «это приложение (или эти приложения) могут быть закреплены». Ниже представлен исходный код функции.

Проанализировав эту функцию, можно заметить два основных вызова: getLocalPolicySetByAdmin и setLocalPolicy.

getLocalPolicySetByAdmin используется для чтения политики, заданной администратором для текущего пользователя. Использование этой функции позволяет обновлять политики, не заменяя собой «вклад» других администраторов: мы меняем только своё локальное значение, а затем DPE объединяет все источники и пересчитывает итоговую политику, которая будет использована далее. Заодно по этому значению удобно понять, было ли фактическое изменение: если изменения не было, можно избежать лишних действий и уведомлений.

setLocalPolicy — центральная точка записи локальной политики от конкретного администратора. Метод добавляет «вклад» администратора в политику, пересчитывает итоговое значение с учётом «вклада» других администраторов и при необходимости применяет изменения к системе, уведомляет администратора и распространяет политику на наследуемые профили.

### Запуск Lock Task Mode

Запуск Lock Task Mode происходит в функции startLockTask из класса Activity, которая, в свою очередь, обращается к startLockTaskModeByToken. Это приводит к вызову функции startLockTaskMode в классе LockTaskController. Именно её и стоит рассмотреть подробнее.

Сперва следует обратить внимание на проверку параметра isSystemCaller. Он определяет, кто пытается закрепить приложение. При вызове из startLockTask этот параметр всегда равен false. Функционал внутри блока if (!isSystemCaller) отвечает за всплывающее окно, в котором пользователь устройства должен подтвердить закрепление (функция showScreenPinningRequest).

После подтверждения закрепления со стороны пользователя функция startLockTaskMode вызывается снова, но параметр isSystemCaller уже будет передан как true, и будет вызвана основная функция закрепления приложения — setLockTaskMode в классе LockTaskController.

При анализе setLockTaskMode основное внимание стоит уделить трём функциям: findTaskToMoveToFront, resumeFocusedTasksTopActivities и executeAppTransition.

Немного об окнах в Android

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

Task — это контейнер стека Activity. Этот класс хранит упорядоченный набор ActivityRecord. Когда пользователь переключается между приложениями или между экранами внутри одного приложения, система выбирает верхний (последний) ActivityRecord в этом списке. Этот верхний ActivityRecord считается текущим: его Activity должна оказаться на экране в состоянии RESUMED, а записи ниже в этом же списке переводятся в состояния PAUSED или STOPPED.

При обычном вызове startActivity() в список ActivityRecord текущего Task добавляется новая запись сверху, и тем самым верх стека меняется. При нажатии Back верхняя запись удаляется из списка, и её место занимает предыдущая запись. Если включены специальные правила (например, singleTop), новый элемент может не добавляться: вместо этого система использует существующий верхний ActivityRecord и обновляет его Intent. Если используется CLEAR_TOP или singleTask, система находит нужный ActivityRecord внутри списка этого Task, удаляет все записи выше него и делает найденную запись верхней. Если пользователь открывает другое приложение, стек не перестраивается: просто активным становится верхний Task.

Также Task является узлом в дереве WindowManager. Это означает, что Task существует как оконный контейнер (WindowContainer), который участвует в компоновке, отрисовке и Z-порядке — очерёдности наложения элементов по оси глубины экрана (что выводится на передний план, а что остаётся на заднем). Если два окна или контейнера перекрываются, элемент с более высоким Z-порядком будет находиться на переднем плане и обычно именно он получает фокус ввода.

В иерархии WMS (WindowManagerService) Task располагается ниже RootTask. Общая иерархия внутри WMS выглядит так: DisplayContent → TaskDisplayArea → RootTask → Task → ActivityRecord → окна/Surface.

RootTask — это верхнеуровневый контейнер задач в WindowManager. Он группирует обычные Task по режиму и политике отображения (например, fullscreen, split-screen, PiP, freeform). Именно RootTask определяет общий оконный режим и границы для своих дочерних задач, а также управляет их порядком отображения на дисплее.

Как узел WM-дерева Task задаёт границы для всего содержимого, наследует или переопределяет оконный режим (fullscreen, split-screen, freeform) и является единицей, которую можно целиком перемещать в Z-порядке (например, move-to-front — поднять задачу над соседями в том же RootTask). Кроме того, на уровне Task удобно применять групповые операции для отрисовки: показывать временную поверхность (starting window или snapshot), выполнять анимации и трансформации, а также делать reparenting — переносить задачу в другой RootTask или даже в другой DisplayContent (перенос на другой дисплей).

Возвращение в Kiosk Mode

Теперь, когда мы понимаем, как работает показ Activity в Android, можно вернуться к Kiosk Mode, а точнее к функции findTaskToMoveToFront.

findTaskToMoveToFront реализует фазу move-to-front для Task: она переводит выбранный Task на передний план и гарантирует, что его верхняя ActivityRecord будет показана на нужном дисплее, в нужном оконном режиме (обычно fullscreen), а также станет первой по фокусу и вводу. Этапы, которые выполняются в findTaskToMoveToFront, можно описать так:

- Reparenting — если Task находится в неподходящем оконном режиме (split-screen, PiP) или на другом дисплее, его переносят в целевой полноэкранный root, чтобы получить единое фокусное окно. - Подготовка перехода в WM — перед фактическим поднятием на передний план нужные ActivityRecord помечаются для корректной анимации и правильных Surface-транзакций. - Move-to-front и focus — Task поднимается на вершину в иерархии задач, а фокус ввода переводится на его верхнюю Activity. - Starting window (при необходимости) — если верхняя Activity ещё не готова к отрисовке, открывается стартовое окно, чтобы избежать «чёрного экрана». - Передача управления фазе RESUME — после того как Task стал первым в очереди и получил фокус ввода, управление передаётся логике, которая переводит верхнюю Activity в состояние RESUMED.

resumeFocusedTasksTopActivities проходит по всем зонам дисплея (если их несколько) и для каждой области проверяет, какой RootTask сейчас считается главным и должен получать фокус.

После этого метод не пытается напрямую перевести в состояние RESUMED конкретную Activity, а просто делегирует работу на нижний уровень: выбранному RootTask передаётся команда «переведи в состояние RESUMED верхний ActivityRecord», и дальше уже внутри этого RootTask выбирается нужный Task, а внутри него — верхний ActivityRecord.

Внутри корня resumeTopActivityUncheckedLocked выполняет несколько проверок (состояние дисплея, состояние контейнера), после чего выбирается конкретная верхняя ActivityRecord и инициируется её переход в стадию RESUME.

Ключевая работа выполняется в TaskFragment.resumeTopActivity. Если Activity уже находится в состоянии RESUMED и привязана к процессу, метод просто актуализирует её видимость и завершает выполнение. В противном случае он переводит предыдущую Activity в состояние PAUSED и инициирует вызов onResume для текущей верхней Activity. Для этого в процесс предыдущей Activity передаётся PauseActivityItem, а в процесс новой Activity — ResumeActivityItem.

После того как Activity приложения будет подготовлена и перейдёт в состояние RESUMED, WindowManager переходит к стадии визуального применения изменений. Для этого вызывается executeAppTransition. Сначала метод сообщает TransitionController, что текущий DisplayContent готов запускать переходы, затем проверяет, задан ли вообще переход в mAppTransition. Если переход задан, он помечается как готовый к выполнению, после чего вызывается mWindowPlacerLocked.requestTraversal.

Вызов requestTraversal запускает следующий этап расстановки окон и поверхностей (layout и surface placement): на этом шаге изменения отображаются на экране и воспроизводится анимация (при её наличии).

Популярные ошибки при создании kiosk-устройств

Теперь, когда мы в общих чертах разобрались, как работает Kiosk Mode, пора поговорить о том, как сделать свои kiosk-приложения безопаснее (и на что обращать внимание в чужих решениях). В этом разделе будут рассмотрены часто встречающиеся ошибки в конфигурации приложений и устройств, работающих в режиме Kiosk Mode.

### Лишние приложения в списке разрешённых приложений

Как было показано выше, при конфигурации kiosk-устройств необходимо явно задавать список прилож

← Cybersecurity