Los fabricantes de teléfonos inteligentes envían dispositivos Android con un conjunto estricto de permisos y sistemas de control de acceso para proteger a los usuarios de los riesgos de seguridad y evitar que dañen accidentalmente sus dispositivos. Sin embargo, estos sistemas pueden parecer restrictivos para los usuarios que deseen personalizar su dispositivo de una manera no prevista por el fabricante.
Para obtener acceso completo a un dispositivo Android, debe estar rooteado. Tener acceso root a un dispositivo proporciona los siguientes beneficios, entre otros:
- Instalación de aplicaciones móviles que se encuentran en tiendas de aplicaciones de terceros
- Aplicaciones de prueba
- Aplicación de temas/máscaras personalizados para aplicaciones y la pantalla de inicio
- Mejorar la duración de la batería
- Mejora del rendimiento
- Modificación del comportamiento de las aplicaciones móviles en el dispositivo
Tener acceso raíz a los dispositivos Android es un componente necesario de las pruebas de seguridad de aplicaciones móviles en NowSecure. Sin él, los investigadores de seguridad no podrían obtener información sobre el funcionamiento interno de las aplicaciones móviles tan fácilmente.
¿Qué es la detección de raíz?
Cuando los usuarios tienen acceso de raíz, pueden manipular cada parte del dispositivo. Si bien no todos los usuarios de un dispositivo rooteado pueden tener intenciones maliciosas, algunos desarrolladores no quieren permitir que los dispositivos rooteados usen sus aplicaciones móviles. Permitir que una aplicación móvil se ejecute en un dispositivo rooteado la abre a una amplia variedad de vulnerabilidades. Las aplicaciones móviles confidenciales, como las bancarias, médicas, comerciales y gubernamentales, a menudo implementan comprobaciones que determinan si la aplicación se ejecuta en un dispositivo con privilegios de root. En la mayoría de los casos, estas comprobaciones impedirán que la aplicación móvil funcione correctamente. A veces, los desarrolladores implementan capacidades estrictas de detección de raíces que evitan que las aplicaciones se ejecuten en dispositivos rooteados.
A menudo es posible omitir la detección de raíz, sin embargo, la cantidad de habilidad que se requiere puede variar mucho según la aplicación móvil. Hay muchas técnicas de detección de raíz que se pueden implementar. Algunas aplicaciones usan controles básicos que se pueden encontrar fácilmente en línea, mientras que otras pueden usar métodos de detección personalizados que nunca antes se habían visto. Debido a que la mayoría de la lógica de detección de raíz se ejecuta directamente en un dispositivo, estas técnicas a menudo se pueden descubrir mediante ingeniería inversa.
A través de una combinación de análisis estático y dinámico, puede descubrir qué está haciendo la detección raíz, cuándo se llama y cómo eludir las comprobaciones. Los pasos no serán exactamente los mismos para todas las aplicaciones, sin embargo, el proceso para eludir la detección de raíz es similar en la mayoría de los casos.
Tutorial para aplicaciones de Android de ingeniería inversa
Ser capaz de eludir la detección de raíz es una habilidad invaluable que todos los investigadores de seguridad móvil y los evaluadores de penetración móviles deben tener en su conjunto de herramientas.
Este tutorial cubre los pasos para aplicar ingeniería inversa a las aplicaciones de Android y omitir tres técnicas comunes de detección de raíces usando Frida. Este tutorial utiliza una aplicación de prueba rudimentaria, pero las mismas técnicas se aplican a las aplicaciones móviles del mundo real. Puedes descargar la aplicación de prueba aquí:
requisitos previos
- Un dispositivo Android rooteado
- Una aplicación con detección de raíz
- JADX-GUI
- Paquetes Frida Python (frida, frida-tools)
- Frida Server ejecutándose en el dispositivo de destino (cómo)
- Conocimientos básicos de Java y Javascript
Encontrar el código de detección con análisis estático

Al iniciar la aplicación móvil de muestra, podemos ver los tres métodos de detección de raíces que utiliza la aplicación. Ya que los tres son true
, se detectó el acceso a la raíz y el mensaje «¡Parece que esta aplicación se está ejecutando en un dispositivo pirateado!» brindis aparece en la parte inferior de la pantalla. Dado que este brindis aparece cada vez que falla la verificación de raíz, tenemos una idea de dónde comenzar a buscar en el código descompilado.
Para descompilar el código, inicie JADX-GUI, seleccione «Abrir archivo» y seleccione el APK que desea descompilar. Una vez que se descompile la aplicación de Android, comience a buscar palabras o frases que puedan estar relacionadas con la detección de raíces. Algunas frases útiles podrían ser: raíz detectada, su, magisk, verificación de raíz, está enraizado, jailbreak, etc. Dado que tenemos un aviso de advertencia que se muestra cuando se detecta la raíz, podemos buscar el contenido del brindis.

Los resultados de la búsqueda nos muestran la ubicación en el código descompilado donde se crea este brindis de advertencia. Al hacer clic en el resultado de la búsqueda, podemos navegar hasta el código que se encarga de realizar la verificación y decidir si se debe mostrar este brindis.

El código fuente muestra que una función llamada checkForRoot
se llama. Si la función devuelve true
, entonces se ha detectado el acceso raíz. Según esta información, el código que necesitamos omitir debe residir dentro de esta función.
Comprender y omitir el código de detección

Dentro de checkForRoot
función, vemos que se realizan tres comprobaciones. Estos cheques regresan boolean
valores y si alguno de ellos es true
, luego se detectó la raíz. Estos tres controles corresponden a los tres valores que se muestran en la pantalla de inicio de la aplicación, por lo que los revisaremos uno por uno. Nuestro objetivo será manipular el comportamiento de la aplicación, por lo que todos estos controles regresan false
. Para manipular el comportamiento de la aplicación móvil, nos basaremos en una herramienta de análisis dinámico llamada Frida. Frida utiliza varios métodos para conectarse al tiempo de ejecución de una aplicación y proporciona una interfaz para que los investigadores vean o manipulen cómo funciona un programa mientras se ejecuta. En este tutorial, usaremos la API de JavaScript de Frida para implementar nuestros ganchos.
su Comprobación binaria
Entendiendo la comprobación binaria su
La primera comprobación que realiza la aplicación está contenida en doesSuBinaryExist
.

Esta función crea una matriz de rutas de archivos donde un su
el binario podría ser potencialmente localizado. Luego, el código inicializa un File
class para cada una de estas rutas y verifica si el archivo se puede encontrar usando un exists
llamada. Esta verificación se usa comúnmente en la detección de raíz, porque los usuarios o las aplicaciones usan un binario su para obtener sudo
o root
acceso a un dispositivo. Para eludir esta verificación, debemos engañar a la aplicación para que piense que el su
el binario no existe en el dispositivo. Como sabemos que utiliza exists
para encontrar el binario, ese debería ser nuestro objetivo.
Omitir la verificación binaria su
Como queremos modificar el exists
método, primero debemos conectarnos a la clase que llama al método. En este caso, exists
se llama desde dentro de la clase File. Mirando la lista de clases importadas a la clase MainActivity en la parte superior de la file
podemos obtener la File
nombre de la clase que nos interesa.

Para comenzar a escribir el gancho, necesitamos abrir un archivo JavaScript vacío. Podemos empezar a enganchar el File
clase con:
const File = Java.use('java.io.File');
A continuación, debemos indicar qué método queremos modificar. Podemos hacer esto para el exists
método como este:
File.exists.implementation = function ()
Esta línea de código nos permite sobrescribir el comportamiento de lo que debería suceder cuando se realiza una llamada a exists
. Cualquier código que queramos ejecutar debe colocarse dentro de la función que acabamos de definir.
Para entender cómo implementar el bypass, necesitamos entender cómo funciona este método. Podemos hacer esto echando un vistazo a la Documentación de Android. La documentación explica que el exists
el método devuelve true
si el camino existe y false
si no es así En este caso, todo lo que tenemos que hacer es verificar si la ruta del archivo que se está verificando termina con su
y si lo hace, obligamos al método a devolver false
. Si no está comprobando su
entonces podemos continuar con la llamada al método normalmente usando this.exists.
Para determinar qué camino exists
se está llamando, nos referiremos una vez más a la documentación de Android y veremos si el File
class puede proporcionar acceso a esa información. De acuerdo con la documentosel File
la clase tiene un getPath
método que devuelve la ruta del archivo como una cadena. Podemos llamar manualmente a este método y usar la salida para verificar si la llamada debe omitirse o no.
El gancho completo se ve así:
Java.perform(function())
Nota: El anzuelo está envuelto en Java.perform()
, porque esto asegura que la máquina virtual de Java se inicialice antes de que comencemos a cargar nuestros ganchos. Si esta llamada no está incluida, es posible que experimente un comportamiento inesperado.
Guarde el archivo JavaScript y genere su aplicación con Frida usando el siguiente comando:
frida -U -f com.example.rootbypass -l root_bypass.js
El comportamiento de cada argumento es el siguiente:
-U
= utilizar el dispositivo conectado a través de USB-f
= el nombre del paquete de la aplicación que está probando-l
= el archivo JavaScript que se va a cargar
Si el enlace se escribió correctamente, la aplicación de destino debería generarse y la terminal debería mostrar lo siguiente:

La salida de la consola nos muestra que las comprobaciones su se omitieron con éxito y todas las demás exists
las llamadas continuaron con normalidad. Podemos también verifique que el bypass funcionó mirando la pantalla de la aplicación.

¡Eso es uno abajo y quedan dos!
que su comprobar
Comprender el control de cuál su
Otra forma en que las aplicaciones pueden verificar si el su
binario existe en un dispositivo es mediante el uso de la which
dominio. En nuestro código descompilado, podemos ver que se ejecuta which su
para tratar de descubrir la ruta del archivo para el binario.

Este código ejecuta un which su
usando el Runtime
clase. Si se encuentra el binario, el comando envía la ruta del archivo a la salida estándar, pero si no encuentra nada, no se imprimirá nada en la pantalla. El método de detección de raíz luego regresa true
si which su
envía cualquier cosa a stdout, de lo contrario regresa false
.
Debido a que la mayoría de la lógica de detección de raíz se ejecuta directamente en un dispositivo, estas técnicas a menudo se pueden descubrir mediante ingeniería inversa.
Pasando por alto el control what su
Una vez más, debemos consultar la documentación de Android, pero esta vez debemos ver cómo funciona el exec
opera el método. De acuerdo con la documentosparece que hay varias versiones de exec
que se puede llamar, por lo que debemos asegurarnos de que estamos conectando la versión que usa nuestra aplicación de destino. En este caso, el which su
El comando está representado por una matriz de cadenas, por lo que debemos crear un gancho utilizando la documentación para la sobrecarga que toma una única matriz de cadenas como parámetro.
Debido a que también necesitamos acceso al argumento en nuestro gancho, podemos enganchar el argumento agregándolo a la firma de la función que creamos. La configuración para el nuevo gancho se ve así:
const Runtime = Java.use('java.lang.Runtime'); Runtime.exec.overload('[Ljava.lang.String;').implementation = function(commandArray)
Now that the skeleton for our hook has been created, we need to give it functionality. Because we want to bypass commands that reference the su
binary, we should loop over all the words in the command and see if we find su
. The command is stored as an array of strings, so this can be done with a simple for loop. If su
is found, we need to swap that word for a string that does not exist on the device and exec
the substituted command. The code to should look like this:
Java.perform(function())
Este nuevo código se puede colocar dentro del Java.perform()
función del otro gancho. Cuando la aplicación se inicia con frida, la aplicación ahora debería mostrar dos controles omitidos:

Comprobación de la aplicación raíz
Comprender la verificación de la aplicación raíz
Algunas aplicaciones como Magisk se utilizan comúnmente para ayudar en el proceso de enraizamiento. Tener cualquiera de estas aplicaciones instaladas en un dispositivo indica que probablemente se haya rooteado.

Este método comprueba si alguno de los tres nombres de paquetes de aplicaciones raíz proporcionados está instalado en el dispositivo y, si se detecta un paquete, devuelve true
.
Omitir la verificación de la aplicación raíz
A primera vista, parece que debería ser un gancho fácil de escribir. Todo lo que uno debe hacer es enganchar la llamada a getPackageInfo
desde el PackageManager
clase y vuelta false
. Sin embargo, si escribes un gancho para el PackageManager
clase como esta, su enlace nunca se ejecutará:
const PackageManager = Java.use("android.content.pm.PackageManager");
PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function (packageName, flags)
Esto se debe a que PackageManager es una clase abstracta. Esto significa que debe buscar una clase que amplíe el PackageManger
clase y escribir un gancho para eso. Afortunadamente, Android es de código abierto, por lo que podemos ver fácilmente todas las clases que amplían el PackageManager
¡clase! Si levantamos el código fuente de Androidhaga clic en PackageManager
y luego haga clic en «Referencias» en la parte inferior, ¡podemos ver una lista de clases que lo amplían!

La primera clase que se extiende PackageManager
se llama ApplicationPackageManager
. Usando el conocimiento obtenido de las omisiones anteriores, necesitamos buscar los argumentos y devolver el valor para getPackageInfo
en los documentos de Android. Una vez que entendemos cómo funciona el método y hemos escrito el esqueleto, el bypass debe verificar si el paquete de destino coincide con lo que hemos instalado en nuestro dispositivo e insertar un nombre de paquete falso. El gancho terminado debería verse así:
Java.perform(function())
Asegúrese de que el gancho tenga en cuenta la sobrecarga correcta de getPackageInfo
y la firma de la función acepta ambos parámetros. Cuando este gancho se combina con los dos anteriores, deberíamos ver que la terminal debería verse así:

Y si todas las omisiones funcionan como se esperaba, ¡nuestra aplicación no podrá detectar que se está ejecutando en un dispositivo rooteado!

Conclusión
Si bien este tutorial solo muestra tres métodos de detección de raíz, esta técnica de combinar análisis estático con instrumentación dinámica se puede aplicar a todos los métodos. A medida que encuentre más técnicas de detección de raíz y las agregue a su secuencia de comandos de omisión de Frida, puede crear una herramienta confiable para omitir las técnicas de detección de raíz en una variedad de aplicaciones móviles.
Para ahorrar tiempo en el análisis de aplicaciones móviles, los evaluadores de penetración y otros pueden aprovechar el hardware y el software preconfigurados de NowSecure Workstation que comprime las evaluaciones de vulnerabilidad de las aplicaciones móviles en solo unas horas y permite realizar pruebas repetibles. También puede conocer a los expertos de NowSecure en nuestra serie de Tech Talks: registro hoy.