
PoC для CVE-2021-39749, позволяющий запускать произвольное Activity на Android 12L Beta
Это PoC для CVE-2021-39749, который позволяет запускать активности других приложений на Android 12L Beta независимо от их настроек permission и exported
В Android 12L доступ к TaskFragmentOrganizer (намеренно) больше не требует разрешения MANAGE_ACTIVITY_TASKS
Использование приложения, представленного здесь, требует отключения проверок скрытых API, это можно сделать через adb shell settings put global hidden_api_policy 1. Это не является границей безопасности, и существуют известные обходные пути на уровне приложений
Вот коммиты, исправляющие эту ошибку (и несколько связанных, упомянутых в оригинальном отчёте):
startActivityInTaskFragment больше не полагается на Binder.getCallingUid()ResolverActivity теперь имеет включённый relinquishTaskIdentitySurfaceControl для TaskFragment больше не предоставляетсяActivityRecord#appToken в TaskFragmentOrganizer, теперь основано на uid, а не на pidВы можете переключиться на android-12.1.0_r4, откатить первые 3 коммита (или первые 2, приложение всё равно сможет выключить устройство (запустив ShutdownActivity), но флажок "Zoom and set alpha" не будет работать)
(Первый коммит из этого списка будет иметь конфликты слияния в тестах, если вы попытаетесь его откатить, но их можно игнорировать)
Binder.getCallingUid(), который всегда возвращает системный uidМетод Binder.getCallingUid() возвращает uid процесса, отправившего текущую обрабатываемую Binder-транзакцию. Этот uid хранится в переменной, локальной для потока. Код, обрабатывающий транзакцию, может вызвать Binder.clearCallingIdentity(), чтобы установить эту переменную в uid собственного процесса, указывая методам, вызываемым позже во время обработки транзакции, что проверки разрешений должны выполняться против самого себя (кода, обрабатывающего транзакцию), а не вызывающего Binder-транзакцию
Иногда встречаются вызовы Binder.getCallingUid(), которые всегда вызываются после Binder.clearCallingIdentity(), и поэтому всегда возвращают uid собственного процесса. Иногда это происходит намеренно, например, в ActivityTaskManagerService#startDreamActivity (хотя это довольно запутанный способ сделать Process.myUid() или Os.getuid())
Я написал для себя (основанный на Soot) инструмент статического анализа, который сообщает о таких вызовах Binder.getCallingUid() (и других проверках разрешений), которые могут произойти только после Binder.clearCallingIdentity(). (У меня есть собственная логика обработки Jimple/Shimple IR, предоставляемого Soot, хотя, возможно, есть лучший способ сделать это с помощью Soot, но это то, что у меня есть сейчас)
В Android 12L Beta этот инструмент нашёл один такой вызов в ActivityStartController#startActivityInTaskFragment (Примечание: исходный код тогда не был доступен, так как Beta-релизы не являются открытым исходным кодом, но Shimple в целом читаем, поэтому я использовал Soot также как декомпилятор Java)
startActivityInTaskFragmentВ рамках отчёта статического анализа я получил иерархию вызовов от реализации onTransact() (где начинается Binder-вызов) до startActivityInTaskFragment:
onTransact в сгенерированном aidl-коде IWindowOrganizerControllerWindowOrganizerController#applyTransaction (без аргумента CallerInfo)WindowOrganizerController#applyTransaction (с аргументом CallerInfo)WindowOrganizerController#applyHierarchyOpActivityStartController#startActivityInTaskFragmentЯ обнаружил, что Binder-вызовы applyTransaction присутствуют в классе TaskFragmentOrganizer, и решил использовать его как более удобную обёртку, чем выполнять все Binder-вызовы напрямую (ни то, ни другое не является публичным API, поэтому в любом случае пришлось использовать рефлексию)
Прежде всего, метод "2." вызывает enforceTaskPermission, который на Android 12.0 проверял разрешение MANAGE_ACTIVITY_TASKS только для signature, которое мы не могли получить, однако на Android 12L правила были смягчены, так что определённые транзакции можно выполнять без разрешений. Оказалось, что ни одна из операций, необходимых для выполнения startActivityInTaskFragment, не требовала разрешения (если с транзакцией был связан TaskFragmentOrganizer)
Итак, мы хотим выполнить HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT. Для этого наш TaskFragment должен быть зарегистрирован в mLaunchTaskFragments, иначе будет сообщено об исключении "Not allowed to operate with invalid fragment token"
Мы можем зарегистрировать такой TaskFragment через HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, который вызывает createTaskFragment()
(В коде PoC эти транзакции отправляются в SecondActivity: HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT отправляется initOrganizerAndFragment(), а HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT отправляется startActivityInOrganizer)
Это позволяет нам вызывать startActivityInTaskFragment, и Intent'ы активностей, запущенных здесь, считаются исходящими от системного uid, но оказывается, что само по себе это не даёт нам ничего: активности, запущенные системой, не могут выполнять URI grants, и если мы попытаемся запустить активность другого приложения, нас остановит проверка canEmbedActivity
canEmbedActivityДавайте снова посмотрим на canEmbedActivity: встраивание разрешено, если taskFragment.getTask().effectiveUid является uid системы или совпадает с uid запускаемого приложения. Нам нужно оказаться в задаче, чей effectiveUid является системным
Также вернёмся к createTaskFragment(): создание TaskFragment разрешалось только если rootActivity.getUid() != ownerActivity.getUid(). Это означает, что наша активность должна находиться внизу back-stack'а задачи, в которой она находится
Нам нужно будет запустить новую Task (через Intent.FLAG_ACTIVITY_NEW_TASK), в которой будет активность, принадлежащая системному uid (так что Task#effectiveUid будет установлен в AID_SYSTEM), а затем эта активность запустит нашу активность (в той же задаче) и finish() сама себя (так что наша активность станет корневой в этой задаче, что позволит нам использовать createTaskFragment())
Одной из таких активностей является ChooserActivity. Chooser обычно используется для выбора приложения, которое пользователь хочет использовать после выбора опции "поделиться". ChooserActivity, однако, имеет android:relinquishTaskIdentity="true" в AndroidManifest.xml, что означает, что когда она запускает другую активность, она перезаписывает Task#effectiveUid на uid только что запущенного приложения
(relinquishTaskIdentity работает только при использовании первым приложением в задаче и только для системных приложений, поэтому мы не можем сами использовать relinquishTaskIdentity и запустить системное приложение, чтобы перезаписать Task#effectiveUid нашей задачи)
Другой такой активностью (которая может запустить нашу активность и finish() сама себя) является ResolverActivity. Она используется при запуске неявного Intent, который разрешается в несколько активностей. Resolver (в отличие от Chooser) предлагает опцию запомнить выбор, что позволяет вам (как пользователю телефона) отличить эти две активности. ResolverActivity не имела установленного relinquishTaskIdentity, однако Resolver использует собственный Intent для поиска доступных опций (в то время как Chooser берёт Intent, предоставленный в Extras). Это оказывается проблемой для эксплуатации, потому что флаги Intent, используемые Resolver при запуске выбранной активности, будут такими же, как и те, что использовались для запуска Resolver, и:
Intent.FLAG_ACTIVITY_NEW_TASK, Resolver будет запущен в нашей Task, которая уже имеет постоянно установленный effectiveUidIntent.FLAG_ACTIVITY_NEW_TASK, Resolver запустит выбор в ещё одну Task, которая затем будет иметь effectiveUid, установленный на uid запущенного приложенияРешение этих проблем — использовать оба варианта:
ChooserActivity: Мы предоставляем в её Intent:
Intent.FLAG_ACTIVITY_NEW_TASK, чтобы Chooser был запущен в новой задаче (которая будет иметь effectiveUid системы, но только до следующего запуска активности)Intent.EXTRA_INTENT, установленный на Intent, который не соответствует ни одной активности, и единственные оставшиеся опции в Chooser будут из Intent.EXTRA_INITIAL_INTENTSIntent.EXTRA_INITIAL_INTENTS, содержащий массив с одним элементом: Intent, который мы хотим, чтобы Chooser запустил (когда есть только одна опция, и Chooser, и Resolver пропускают запрос и немедленно запускают единственную опцию и finish() сами себя)ResolverActivity запускается ChooserActivity:
(В PoC-приложении подготовка этих шагов выполняется в FirstActivity)
TaskFragmentOrganizer получает SurfaceControl через колбэк onTaskFragmentAppeared, и, используя этот SurfaceControl, можно масштабировать запущенную активность и делать её прозрачной, при этом она всё равно будет получать события касания и не будет считаться скрытой (поэтому элементы, защищённые от tap-jacking, всё равно можно нажимать)
Вы можете увидеть это, отметив флажок "Zoom and set alpha" в PoC-приложении
Это исправлено коммитом "3." из списка исправлений вверху
Ещё одна вещь: ActivityRecord#appToken-ы активностей, работающих внутри TaskFragment, передаются в колбэки TaskFragmentOrganizer. Этот список фильтровался, чтобы включать только токены активностей в том же процессе, однако проверка выполнялась путём сравнения pid TaskFragmentOrganizer с pid активности, чей appToken мы могли получить. Я на самом деле не проверял, но думаю, что приложение могло бы создать TaskFragmentOrganizer, выйти из процесса, использованного для его первоначального создания, и дождаться, пока его pid будет переиспользован как pid активности другого приложения, чтобы получить его appToken. Вот коммит ("4." в списке исправлений выше), который переключает проверку с pid-основанной на uid-основанную (похоже, этот коммит был сделан независимо от моего отчёта (хотя и после него))
Как только злоумышленник получает appToken активности, он может внедрять вызовы onActivityResult() (даже если целевое приложение само не вызывало startActivityForResult()) и, возможно, подделывать savedInstanceState (вызывая activityStopped(), при условии, что злоумышленник может выиграть гонку с целевым приложением, вызывающим этот метод, и дополнительный вызов не приведёт к потере состояния из-за сбоя)
Я не проверял, можно ли это сделать в данном случае, однако ранее, с CVE-2020-0001 (Да, мне достался красивый номер), я смог использовать подделку savedInstanceState и внедрение onActivityResult() для обмана системного приложения настроек, заставив его включить мой AccessibilityService без взаимодействия с пользователем, но это история для другого раза
relinquishTaskIdentityTask#effectiveUidIntent.FLAG_ACTIVITY_NEW_TASK, поэтому следующая активность запускается в той же задаче<intent-filter>, который мы объявили сами в нашем приложении, поэтому Resolver немедленно переходит к запуску нашей активностиResolverActivity запускает нашу активность
effectiveUid равен AID_SYSTEM, поэтому canEmbedActivity() разрешает всёcreateTaskFragment()