En 2016, aproximadamente el 86% de todas las vulnerabilidades en Android estaban relacionadas con la seguridad de la memoria. La mayoría de las vulnerabilidades son explotadas por atacantes que cambian el flujo de control normal de una app para realizar actividades maliciosas arbitrarias con todos los privilegios de la app explotada. La integridad del flujo de control (CFI) es un mecanismo de seguridad que no permite cambios en el gráfico de flujo de control original de un objeto binario compilado, lo que dificulta considerablemente la realización de este tipo de ataques.
En Android 8.1, habilitamos la implementación de CFI de LLVM en la pila de medios. En Android 9, habilitamos el CFI en más componentes y también en el kernel. El CFI del sistema está activado de forma predeterminada, pero debes habilitar el CFI del kernel.
La CFI de LLVM requiere la compilación con optimización en tiempo de vinculación (LTO). La LTO conserva la representación de código de bits de LLVM de los archivos de objeto hasta el tiempo de vinculación, lo que permite que el compilador razone mejor sobre qué optimizaciones se pueden realizar. Habilitar la LTO reduce el tamaño del objeto binario final y mejora el rendimiento, pero aumenta el tiempo de compilación. En las pruebas en Android, la combinación de LTO y CFI genera una sobrecarga insignificante en el tamaño y el rendimiento del código. En algunos casos, ambos mejoraron.
Para obtener más detalles técnicos sobre el CFI y cómo se controlan otras verificaciones de control hacia adelante, consulta la documentación de diseño de LLVM.
Ejemplos y fuente
El CFI lo proporciona el compilador y agrega instrumentación al objeto binario durante el tiempo de compilación. Admitimos CFI en la cadena de herramientas de Clang y el sistema de compilación de Android en AOSP.
La CFI está habilitada de forma predeterminada para los dispositivos Arm64 en el conjunto de componentes de /platform/build/target/product/cfi-common.mk
.
También se habilita directamente en un conjunto de archivos makefile o de blueprint de componentes multimedia, como /platform/frameworks/av/media/libmedia/Android.bp
y /platform/frameworks/av/cmds/stagefright/Android.mk
.
Implementación del CFI del sistema
La CFI está habilitada de forma predeterminada si usas Clang y el sistema de compilación de Android. Debido a que la CFI ayuda a mantener seguros a los usuarios de Android, no debes inhabilitarla.
De hecho, te recomendamos que habilites el CFI para componentes adicionales. Los candidatos ideales son el código nativo privilegiado o el código nativo que procesa la entrada del usuario no confiable. Si usas clang y el sistema de compilación de Android, puedes habilitar la CFI en componentes nuevos agregando algunas líneas a tus archivos makefiles o blueprint.
Compatibilidad con CFI en archivos makefile
Para habilitar CFI en un archivo de compilación, como /platform/frameworks/av/cmds/stagefright/Android.mk
, agrega lo siguiente:
LOCAL_SANITIZE := cfi # Optional features LOCAL_SANITIZE_DIAG := cfi LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
LOCAL_SANITIZE
especifica CFI como el verificador durante la compilación.LOCAL_SANITIZE_DIAG
activa el modo de diagnóstico para el CFI. El modo de diagnóstico imprime información de depuración adicional en logcat durante las fallas, lo que resulta útil mientras desarrollas y pruebas tus compilaciones. Sin embargo, asegúrate de quitar el modo de diagnóstico en las compilaciones de producción.LOCAL_SANITIZE_BLACKLIST
permite que los componentes inhabiliten de forma selectiva la instrumentación de CFI para funciones o archivos fuente individuales. Puedes usar una lista negra como último recurso para solucionar cualquier problema que pueda existir para los usuarios. Para obtener más detalles, consulta Cómo inhabilitar la CFI.
Compatibilidad con CFI en archivos de esquema
Para habilitar la CFI en un archivo de esquema, como /platform/frameworks/av/media/libmedia/Android.bp
, agrega lo siguiente:
sanitize: { cfi: true, diag: { cfi: true, }, blacklist: "cfi_blacklist.txt", },
Solución de problemas
Si habilitas CFI en componentes nuevos, es posible que encuentres algunos problemas con errores de discrepancia de tipos de funciones y errores de discrepancia de tipos de código de ensamblaje.
Los errores de discrepancia de tipo de función se producen porque la CFI restringe las llamadas indirectas para que solo salten a funciones que tienen el mismo tipo dinámico que el tipo estático que se usa en la llamada. El CFI restringe las llamadas a funciones miembro virtuales y no virtuales para que solo se salte a objetos que sean una clase derivada del tipo estático del objeto que se usa para realizar la llamada. Esto significa que, cuando tengas código que incumpla cualquiera de estas suposiciones, se anulará la instrumentación que agrega CFI. Por ejemplo, el registro de seguimiento de pila muestra un SIGABRT y logcat contiene una línea sobre la integridad del flujo de control que encuentra una discrepancia.
Para corregir este problema, asegúrate de que la función llamada tenga el mismo tipo que se declaró de forma estática. Estos son dos ejemplos de CL:
- Bluetooth: /c/platform/system/bt/+/532377
- NFC: /c/platform/system/nfc/+/527858
Otro posible problema es intentar habilitar la CFI en código que contiene llamadas indirectas a ensamblador. Dado que el código de ensamblaje no está escrito, esto genera un error de tipo.
Para corregir esto, crea wrappers de código nativo para cada llamada de ensamblado y asígnale a los wrappers la misma firma de función que el puntero de llamada. Luego, el wrapper puede llamar directamente al código de ensamblaje. Como las bifurcaciones directas no se instrumentan con CFI (no se pueden reorientar en el tiempo de ejecución y, por lo tanto, no representan un riesgo de seguridad), esto solucionará el problema.
Si hay demasiadas funciones de ensamblaje y no se pueden corregir todas, también puedes incluir en la lista negra todas las funciones que contengan llamadas indirectas al ensamblaje. No se recomienda esta opción, ya que inhabilita las verificaciones de CFI en estas funciones y, por lo tanto, abre la superficie de ataque.
Cómo inhabilitar la CFI
No observamos ninguna sobrecarga de rendimiento, por lo que no deberías inhabilitar el CFI. Sin embargo, si hay un impacto para el usuario, puedes inhabilitar la CFI de forma selectiva para funciones o archivos fuente individuales. Para ello, proporciona un archivo de lista negra del sanitizador en el momento de la compilación. La lista negra le indica al compilador que inhabilite la instrumentación de CFI en las ubicaciones especificadas.
El sistema de compilación de Android admite listas negras por componente (lo que te permite elegir archivos fuente o funciones individuales que no recibirán la instrumentación de CFI) para Make y Soong. Para obtener más detalles sobre el formato de un archivo de lista negra, consulta la documentación de Clang upstream.
Validación
Actualmente, no hay pruebas del CTS específicas para la CFI. En su lugar, asegúrate de que las pruebas de CTS se aprueben con o sin la CFI habilitada para verificar que la CFI no afecte el dispositivo.