
PoC para CVE-2021-39749, que permite iniciar una Activity arbitraria en Android 12L Beta
Este es un PoC para CVE-2021-39749, que permite iniciar actividades de otras aplicaciones en Android 12L Beta independientemente de su configuración de permission y exported
En Android 12L, el acceso a TaskFragmentOrganizer (intencionalmente) ya no requiere el permiso MANAGE_ACTIVITY_TASKS
Usar la aplicación proporcionada aquí requiere deshabilitar Hidden API Checks, puedes hacerlo mediante adb shell settings put global hidden_api_policy 1. Estos no son un límite de seguridad y existen bypasses conocidos basados en aplicaciones
Aquí están los commits que corrigen este error (y algunos relacionados mencionados en el informe original):
startActivityInTaskFragment ya no depende de Binder.getCallingUid()ResolverActivity ahora tiene relinquishTaskIdentity habilitadoSurfaceControl de TaskFragment ya no se proporcionaActivityRecord#appToken a TaskFragmentOrganizer ahora se basa en uid en lugar de pidPuedes hacer checkout de android-12.1.0_r4, revertir los primeros 3 commits (o los primeros 2, la aplicación aún podrá apagar el dispositivo (iniciando ShutdownActivity), pero la casilla "Zoom and set alpha" no funcionará)
(El primer commit de esa lista tendrá conflictos de fusión en las pruebas si intentas revertirlo, pero puedes ignorarlos)
Binder.getCallingUid() que siempre devuelve el uid del sistemaEl método Binder.getCallingUid() devuelve el uid del proceso que envió la transacción Binder que se está procesando actualmente. Ese uid se almacena en una variable local al hilo. El código que maneja la transacción puede llamar a Binder.clearCallingIdentity() para establecer esa variable al uid de su propio proceso, indicando así a los métodos llamados posteriormente durante el manejo de la transacción que las comprobaciones de permisos deben realizarse contra sí mismo (el código que maneja la transacción) y no contra el llamador de la transacción Binder
A veces hay llamadas a Binder.getCallingUid() que siempre se ejecutan después de Binder.clearCallingIdentity(), por lo tanto siempre devuelven el uid del propio proceso. A veces esto ocurre intencionalmente, por ejemplo en ActivityTaskManagerService#startDreamActivity (aunque esa es una forma bastante enrevesada de hacer Process.myUid() o Os.getuid())
He escrito para mí mismo una herramienta de análisis estático (basada en Soot) que reporta dichas llamadas a Binder.getCallingUid() (y otras comprobaciones de permisos) que solo pueden ocurrir después de Binder.clearCallingIdentity(). (Tengo lógica personalizada para manejar el IR Jimple/Shimple proporcionado por Soot, aunque podría haber una mejor manera de hacerlo con Soot, pero es lo que tengo ahora)
En Android 12L Beta, esa herramienta encontró una en ActivityStartController#startActivityInTaskFragment (Nota: el código fuente no estaba disponible entonces ya que las versiones Beta no son de código abierto, pero Shimple es generalmente legible, así que he estado usando Soot también como descompilador de Java)
startActivityInTaskFragmentComo parte del informe de análisis estático, obtuve la jerarquía de llamadas desde la implementación de onTransact() (donde comienza la llamada Binder) hasta startActivityInTaskFragment:
onTransact en el código generado por aidl de IWindowOrganizerControllerWindowOrganizerController#applyTransaction (sin el argumento CallerInfo)WindowOrganizerController#applyTransaction (con el argumento CallerInfo)WindowOrganizerController#applyHierarchyOpActivityStartController#startActivityInTaskFragmentDescubrí que las llamadas Binder a applyTransaction están presentes en la clase TaskFragmentOrganizer y decidí usarla como un envoltorio más conveniente que hacer todas las llamadas Binder directamente (ninguna es API pública, así que tuve que usar reflexión de todos modos)
En primer lugar, el método "2." llama a enforceTaskPermission, que en Android 12.0 comprobaba el permiso MANAGE_ACTIVITY_TASKS solo de signature, que no podíamos obtener. Sin embargo, en Android 12L las reglas se relajaron para que ciertas transacciones puedan realizarse sin permisos. Resultó que ninguna de las operaciones necesarias para realizar startActivityInTaskFragment requería un permiso (si la transacción tenía un TaskFragmentOrganizer asociado)
Así que queremos realizar HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT. Para hacerlo, debemos tener nuestro TaskFragment registrado en mLaunchTaskFragments; de lo contrario, se reportará la excepción "Not allowed to operate with invalid fragment token"
Podemos registrar dicho TaskFragment mediante HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, que llama a createTaskFragment()
(En el código del PoC, estas transacciones se envían en SecondActivity: HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT se envía mediante initOrganizerAndFragment() y HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT se envía mediante startActivityInOrganizer)
Eso nos permite llamar a startActivityInTaskFragment y los Intents de las Actividades iniciadas aquí se consideran provenientes del uid del sistema, pero resulta que por sí solo no nos permite hacer nada: las actividades iniciadas por el sistema no pueden realizar concesiones de URI y si intentamos lanzar una actividad de otra aplicación seremos detenidos por la comprobación canEmbedActivity
canEmbedActivityEchemos otro vistazo a canEmbedActivity: la incrustación se permite si taskFragment.getTask().effectiveUid es el uid del sistema o coincide con el uid de la aplicación lanzada. Necesitaremos estar en una tarea cuyo effectiveUid sea el del sistema
También retrocedamos a createTaskFragment(): la creación de TaskFragment solo se permitía si rootActivity.getUid() != ownerActivity.getUid(). Esto significa que nuestra actividad deberá estar en la parte inferior de la pila de retroceso de la Task en la que se encuentra
Necesitaremos lanzar una nueva Task (mediante Intent.FLAG_ACTIVITY_NEW_TASK) que tendrá una Actividad perteneciente al uid del sistema (para que Task#effectiveUid se establezca en AID_SYSTEM) y luego esa Actividad iniciará nuestra Actividad (dentro de la misma Task) y se finish() a sí misma (para que nuestra Actividad se convierta en la raíz de esa tarea, permitiéndonos usar createTaskFragment())
Una de esas Actividades es ChooserActivity. El Chooser se usa generalmente para elegir a qué aplicación quiere usar el usuario después de seleccionar la opción "compartir". Sin embargo, ChooserActivity tiene android:relinquishTaskIdentity="true" establecido en AndroidManifest.xml, lo que significa que cuando lanza otra Actividad sobrescribirá Task#effectiveUid con el uid de la aplicación recién lanzada
(relinquishTaskIdentity solo funciona cuando lo usa la primera aplicación en la Task y solo para aplicaciones del sistema, por lo que no podemos usar relinquishTaskIdentity nosotros mismos y lanzar una aplicación del sistema para sobrescribir Task#effectiveUid de nuestra Task)
Otra de esas Actividades (que puede iniciar nuestra Actividad y finish() a sí misma) es ResolverActivity. Se usa al iniciar un Intent implícito que se resuelve a múltiples Actividades. Resolver (a diferencia de Chooser) sí ofrece la opción de recordar la elección, que es como tú (como usuario del teléfono) puedes distinguir entre ambas. ResolverActivity no tenía relinquishTaskIdentity establecido; sin embargo, Resolver usa su propio Intent para encontrar qué opciones están disponibles (mientras que Chooser toma el Intent proporcionado en Extras). Esto resulta ser un problema para la explotación porque los flags de Intent que usa Resolver al lanzar la Actividad seleccionada serán los mismos que los usados para lanzar Resolver y:
Intent.FLAG_ACTIVITY_NEW_TASK, Resolver se lanzará dentro de nuestra Task, que ya tiene effectiveUid establecido permanentementeIntent.FLAG_ACTIVITY_NEW_TASK, Resolver lanzará la selección en otra Task, que entonces tendrá effectiveUid establecido al de la aplicación lanzadaLa solución a estos problemas es usar ambos:
ChooserActivity: Proporcionamos a su Intent:
Intent.FLAG_ACTIVITY_NEW_TASK, para que Chooser se lance en una nueva Task (que tendrá effectiveUid del sistema pero solo hasta el siguiente lanzamiento de Actividad)Intent.EXTRA_INTENT establecido a un Intent que no coincide con ninguna Actividad y las únicas opciones que queden en Chooser provendrán de Intent.EXTRA_INITIAL_INTENTSIntent.EXTRA_INITIAL_INTENTS que contenga un array con un solo elemento: El Intent que queremos que Chooser lance (cuando solo hay una opción, tanto Chooser como Resolver omiten el aviso y lanzan inmediatamente la única opción y se finish() a sí mismos)ResolverActivity es lanzada por ChooserActivity:
(En la aplicación del PoC, la preparación de estos pasos se realiza en FirstActivity)
TaskFragmentOrganizer recibe un SurfaceControl a través del callback onTaskFragmentAppeared y usando ese SurfaceControl se puede escalar la Actividad lanzada y hacerla transparente, mientras que aún recibirá eventos táctiles y no se considerará oscurecida (por lo que los elementos protegidos contra tap-jacking aún pueden tocarse)
Puedes verlo marcando la casilla "Zoom and set alpha" en la aplicación del PoC
Esto se corrige con el commit "3." de la lista de correcciones al principio
Otra cosa es que los ActivityRecord#appToken-s de las Actividades que se ejecutan dentro de TaskFragment se pasan a los callbacks de TaskFragmentOrganizer. Esta lista se filtraba para incluir solo los tokens de Actividades dentro del mismo proceso; sin embargo, la comprobación se hacía comparando el pid de TaskFragmentOrganizer con el pid de la Actividad cuyo appToken podíamos obtener. No lo he comprobado realmente, pero creo que una aplicación podría crear TaskFragmentOrganizer, salir del proceso usado para crearlo inicialmente y hacer que su pid se reutilice como pid de una Actividad de otra aplicación para obtener su appToken. Aquí está el commit ("4." en la lista de correcciones anterior) que cambia la verificación de basada en pid a basada en uid (parece que este commit se hizo independientemente de mi informe, aunque después de él)
Una vez que el atacante obtiene el appToken de una Actividad, puede inyectar llamadas a onActivityResult() (incluso si la aplicación objetivo no llamó a startActivityForResult() por sí misma) y posiblemente manipular savedInstanceState (llamando a activityStopped(), asumiendo que el atacante puede ganar la carrera contra la aplicación objetivo que llama a ese método y que la llamada adicional no causará pérdida de estado debido a un bloqueo)
No he comprobado si eso se puede hacer en este caso; sin embargo, anteriormente, con CVE-2020-0001 (Sí, tengo un número elegante), pude usar la manipulación de savedInstanceState y la inyección de onActivityResult() para engañar a la aplicación de configuración del sistema y hacer que habilitara mi AccessibilityService sin interacción del usuario, pero esa es una historia para otro momento
relinquishTaskIdentityTask#effectiveUidIntent.FLAG_ACTIVITY_NEW_TASK, así que la siguiente Actividad se lanza dentro de la misma Task<intent-filter> que declaramos nosotros mismos en nuestra aplicación, así que Resolver procede inmediatamente a lanzar nuestra ActividadResolverActivity lanza nuestra Actividad
effectiveUid es AID_SYSTEM, así que canEmbedActivity() permite cualquier cosafinish() a sí mismos, así que somos la Actividad raíz en la Task y se nos permite usar createTaskFragment()