Determinar si se ejecuta en un dispositivo rooteado


292

Mi aplicación tiene una cierta funcionalidad que solo funcionará en un dispositivo donde la raíz esté disponible. En lugar de que esta característica falle cuando se usa (y luego muestra un mensaje de error apropiado al usuario), preferiría una capacidad de verificar silenciosamente si la raíz está disponible primero, y si no, ocultar las opciones respectivas en primer lugar .

¿Hay alguna forma de hacer esto?


11
No hay una forma confiable de hacerlo; las respuestas a continuación verifican características comunes, pero un dispositivo dado puede no estar enraizado de una manera común. Si la comprobación de la raíz se vuelve frecuente, las soluciones de raíz probablemente comenzarán a esforzarse por ocultarse. Como pueden modificar el comportamiento del sistema operativo, tienen muchas opciones para hacerlo.
Chris Stratton

Podría ser mejor indicar que la función no está disponible debido a la falta de capacidad de raíz que proporciona más información al usuario en lugar de ocultar las capacidades de su aplicación, lo que agrega ambigüedad a la experiencia general.
Nick Fox

¿Las respuestas a continuación funcionan para Systemless Root ?
Piyush Kukadiya

Respuestas:


260

Aquí hay una clase que verificará para Root una de tres maneras.

/** @author Kevin Kowalewski */
public class RootUtil {
    public static boolean isDeviceRooted() {
        return checkRootMethod1() || checkRootMethod2() || checkRootMethod3();
    }

    private static boolean checkRootMethod1() {
        String buildTags = android.os.Build.TAGS;
        return buildTags != null && buildTags.contains("test-keys");
    }

    private static boolean checkRootMethod2() {
        String[] paths = { "/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
                "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su"};
        for (String path : paths) {
            if (new File(path).exists()) return true;
        }
        return false;
    }

    private static boolean checkRootMethod3() {
        Process process = null;
        try {
            process = Runtime.getRuntime().exec(new String[] { "/system/xbin/which", "su" });
            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
            if (in.readLine() != null) return true;
            return false;
        } catch (Throwable t) {
            return false;
        } finally {
            if (process != null) process.destroy();
        }
    }
}

8
Si dos preguntas justifican respuestas idénticas, son duplicadas el 99% del tiempo, por lo tanto, marque como engañadas en lugar de publicar la misma respuesta en ambas. Gracias.
Kev

2
Puede ser así, sin embargo, solo le estoy haciendo saber que la comunidad marca las respuestas exactas duplicadas . Debe adaptar sus respuestas y abordar los detalles del problema del OP. Copiar y pegar respuestas corren el riesgo de atraer votos negativos.
Kev

99
-1, este método no es viable, porque algunos teléfonos incluyen el subinario mientras no están rooteados.
neevek

12
Solo quería informarle, la aplicación Fox Digital Copy (Beta) usa su código casi al pie de la letra, incluidas las clases Root y ExecShell, así como los métodos checkRootMethod1 / 2/3. Lo encontré muy divertido.
Matt Joseph

8
¿Puedo demandarlos como Fox demandó a muchos otros?
Kevin Parker

58

Si ya está utilizando Fabric / Firebase Crashlytics, puede llamar

CommonUtils.isRooted(context)

Esta es la implementación actual de ese método:

public static boolean isRooted(Context context) {
    boolean isEmulator = isEmulator(context);
    String buildTags = Build.TAGS;
    if(!isEmulator && buildTags != null && buildTags.contains("test-keys")) {
        return true;
    } else {
        File file = new File("/system/app/Superuser.apk");
        if(file.exists()) {
            return true;
        } else {
            file = new File("/system/xbin/su");
            return !isEmulator && file.exists();
        }
    }
}

La mejor respuesta de todas. Utilice esto sobre cualquier biblioteca, hay muchos falsos positivos en los dispositivos chinos.
Pedro Paulo Amorim

¿Hay algún falso positivo en este método?
Ehsan Mashhadi

Probé esto en nexus 5 con download.chainfire.eu/363/CF-Root/CF-Auto-Root/… , este no es exacto.
Jeffrey Liu

54

La biblioteca RootTools ofrece métodos simples para verificar la raíz:

RootTools.isRootAvailable()

Referencia


10
isRootAvailable () solo verifica la existencia de su en la ruta y algunos otros directorios codificados. Escuché que algunas herramientas para desrootear dejarán su allí, así que esto dará un falso positivo.
Bob Whiteman el

13
RootTools.isAccessGiven () no solo buscará root, sino que también solicitará permiso de root; Por lo tanto, un dispositivo sin raíz siempre devolverá falso con este método.
agregado11686877

2
@ agregate1166877, tiene razón, pero no es lo suficientemente bueno, ¿qué pasa si no solicito permiso de root cuando pregunto? Solo quiero saber si está rooteado, pero no necesito permiso de root en este momento.
neevek

44
isAccessGiven () devuelve falso cuando el usuario niega el permiso a pesar de que el dispositivo estaba rooteado.
subair_a

Esta es la única respuesta que creo que vale la pena votar. Vea mi respuesta a continuación si desea algo similar a simplemente copiar y pegar, o si desea obtener más detalles
rsimp

52

En mi aplicación estaba comprobando si el dispositivo está rooteado o no ejecutando el comando "su". Pero hoy he eliminado esta parte de mi código. ¿Por qué?

Porque mi aplicación se convirtió en un asesino de la memoria. ¿Cómo? Déjame contarte mi historia.

Hubo algunas quejas de que mi aplicación estaba ralentizando dispositivos (por supuesto, pensé que eso no puede ser cierto). Traté de entender por qué. Así que utilicé MAT para obtener volcados del montón y analizar, y todo parecía perfecto. Pero después de reiniciar mi aplicación muchas veces, me di cuenta de que el dispositivo realmente se está volviendo más lento y detener mi aplicación no lo hizo más rápido (a menos que reinicie el dispositivo). Analicé los archivos de volcado nuevamente mientras el dispositivo es muy lento. Pero todo seguía siendo perfecto para el archivo de volcado. Luego hice lo que debía hacerse al principio. Enumeré los procesos.

$ adb shell ps

Sorpresa; hubo muchos procesos para mi aplicación (con la etiqueta de proceso de mi aplicación en el manifiesto). Algunos de ellos eran zombis, otros no.

Con una aplicación de muestra que tiene una sola Actividad y ejecuta solo el comando "su", me di cuenta de que se está creando un proceso zombie en cada lanzamiento de la aplicación. Al principio, estos zombis asignan 0 KB pero luego sucede algo y los procesos de zombis contienen casi los mismos KB que el proceso principal de mi aplicación y se convirtieron en procesos estándar.

Hay un informe de error para el mismo problema en bugs.sun.com: http://bugs.sun.com/view_bug.do?bug_id=6474073 esto explica si no se encuentra el comando, se crearán zombis con el método exec () . Pero todavía no entiendo por qué y cómo pueden convertirse en procesos estándar y tener KB significativos. (Esto no sucede todo el tiempo)

Puede probar si lo desea con el ejemplo de código a continuación;

String commandToExecute = "su";
executeShellCommand(commandToExecute);

Método de ejecución de comando simple;

private boolean executeShellCommand(String command){
    Process process = null;            
    try{
        process = Runtime.getRuntime().exec(command);
        return true;
    } catch (Exception e) {
        return false;
    } finally{
        if(process != null){
            try{
                process.destroy();
            }catch (Exception e) {
            }
        }
    }
}

Para resumir; No tengo consejos para determinar si el dispositivo está rooteado o no. Pero si fuera usted, no usaría Runtime.getRuntime (). Exec ().

Por cierto; RootTools.isRootAvailable () causa el mismo problema.


55
Eso es muy preocupante. Tenía una clase de detección de dispositivo rooteado que hacía lo mismo: después de leer esto, confirme lo que el egeo detalló anteriormente. Procesos ocasionales de zombies que se quedan atrás, ralentizaciones del dispositivo, etc ...
AWT

1
Confirmo el problema con RootTools 3.4 en un GT-S5830i android 2.3.6. A la mayoría de los zombis se les asignó memoria y el problema es sistemático. Necesito reiniciar el dispositivo después de 3-4 pruebas. Recomiendo guardar el resultado de la prueba en preferencia compartida.
Cristo

2
Google ahora recomienda usar ProcessBuilder () y el comando start ().
EntangledLoops

1
@NickS Interesante, pero ¿qué comando lanzaste? No tengo el mismo problema aquí emitiendo comandos en numerosos teléfonos Android con diferentes niveles de API del 9 al 23.
EntangledLoops

1
@EntangledLoops. Gracias. Lanzo mi propio binario e interactúo con él a través de stdin / stdout. Revisé nuevamente cómo lo detengo y descubrí que me perdí Process.destroy () en uno de los casos. Entonces, no hay zombies.
Nick S

36

Muchas de las respuestas enumeradas aquí tienen problemas inherentes:

  • La comprobación de claves de prueba está correlacionada con el acceso raíz, pero no necesariamente lo garantiza.
  • Los directorios "PATH" deben derivarse de la variable de entorno "PATH" real en lugar de estar codificados
  • La existencia del ejecutable "su" no significa necesariamente que el dispositivo haya sido rooteado
  • El ejecutable "which" puede o no estar instalado, y debe permitir que el sistema resuelva su ruta si es posible
  • El hecho de que la aplicación SuperUser esté instalada en el dispositivo no significa que el dispositivo tenga acceso root todavía

La biblioteca RootTools de Stericson parece estar buscando la raíz de manera más legítima. También tiene muchas herramientas y utilidades adicionales, así que lo recomiendo encarecidamente. Sin embargo, no hay una explicación de cómo verifica específicamente la raíz, y puede ser un poco más pesado de lo que la mayoría de las aplicaciones realmente necesitan.

He hecho un par de métodos de utilidad que se basan libremente en la biblioteca RootTools. Si simplemente desea verificar si el ejecutable "su" está en el dispositivo, puede usar el siguiente método:

public static boolean isRootAvailable(){
    for(String pathDir : System.getenv("PATH").split(":")){
        if(new File(pathDir, "su").exists()) {
            return true;
        }
    }
    return false;
}

Este método simplemente recorre los directorios listados en la variable de entorno "PATH" y verifica si existe un archivo "su" en uno de ellos.

Para verificar verdaderamente el acceso a la raíz, el comando "su" debe ejecutarse realmente. Si se instala una aplicación como SuperUser, en este punto puede solicitar acceso a la raíz, o si ya se le ha otorgado / denegado, se puede mostrar un brindis que indica si el acceso fue otorgado / denegado. Un buen comando para ejecutar es "id" para que pueda verificar que el ID de usuario es de hecho 0 (root).

Aquí hay un método de muestra para determinar si se ha otorgado acceso root:

public static boolean isRootGiven(){
    if (isRootAvailable()) {
        Process process = null;
        try {
            process = Runtime.getRuntime().exec(new String[]{"su", "-c", "id"});
            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String output = in.readLine();
            if (output != null && output.toLowerCase().contains("uid=0"))
                return true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (process != null)
                process.destroy();
        }
    }

    return false;
}

Es importante probar realmente ejecutar el comando "su" porque algunos emuladores tienen el ejecutable "su" preinstalado, pero solo permiten que ciertos usuarios accedan a él como el shell adb.

También es importante verificar la existencia del ejecutable "su" antes de intentar ejecutarlo, ya que se sabe que Android no elimina correctamente los procesos que intentan ejecutar comandos faltantes. Estos procesos fantasmas pueden aumentar el consumo de memoria con el tiempo.


El método isRootAvailable () funciona muy bien, gracias. Sin embargo, recomiendo no ejecutar esto en el hilo principal para evitar un ANR, como una llamada desde una AsyncTask
Thunderstick

1
Creo que es la diferencia entre querer asegurarse de que la raíz no esté disponible y querer asegurarse de que sí. Si desea asegurarse de que un dispositivo no esté rooteado, las comprobaciones sugeridas son buenas. Obtendrá falsos positivos, pero está bien cuando no ejecute su código en un dispositivo comprometido es su principal preocupación.
Jeffrey Blattman

1
@ DAC84 No estoy seguro de entender su pregunta. Si ejecuta isRootGiven y rechaza en su aplicación de enrutamiento, debería devolver false ¿No es eso lo que está pasando? Si desea evitar la alerta, puede usar isRootAvailable, que también podría llamarse doesSUExist. También puede intentar configurar su aplicación raíz para que la distribuya libremente y no la administre.
rsimp

1
@BeeingJk no, no realmente, aunque eso es realmente lo máximo que puedes comprobar sin ejecutar su, que es la prueba real. Sin embargo, debe verificar su en PATH antes de intentar ejecutarlo. Sin embargo, la ejecución de su a menudo resulta en un mensaje brindis o una interacción con una aplicación de administración raíz que puede no ser lo que desea. Por su propia lógica, puede considerar la mera existencia de su suficiente. Esto aún puede dar falsos positivos en algunos emuladores que pueden contener un ejecutable su pero acceso de bloqueo.
rsimp

1
@BeeingJk isRootAvailable es probablemente todo lo que necesita, pero el punto que estoy tratando de hacer es que un nombre como ese o incluso doesSUExist proporciona una semántica mejor que un nombre de método como isDeviceRooted que no es del todo correcto. Si realmente necesita verificar el acceso completo a la raíz antes de continuar, debe intentar ejecutar un comando con su como el codificado en isRootGiven
rsimp

35

Actualización 2017

Puede hacerlo ahora con la API de Google Safetynet . SafetyNet API proporciona API de certificación que le ayuda a evaluar la seguridad y la compatibilidad de los entornos de Android en los que se ejecutan sus aplicaciones.

Esta certificación puede ayudar a determinar si el dispositivo en particular ha sido manipulado o modificado de otra manera.

La API de certificación devuelve una respuesta JWS como esta

{
  "nonce": "R2Rra24fVm5xa2Mg",
  "timestampMs": 9860437986543,
  "apkPackageName": "com.package.name.of.requesting.app",
  "apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the
                                  certificate used to sign requesting app"],
  "apkDigestSha256": "base64 encoded, SHA-256 hash of the app's APK",
  "ctsProfileMatch": true,
  "basicIntegrity": true,
}

Analizar esta respuesta puede ayudarlo a determinar si el dispositivo está rooteado o no

Los dispositivos enraizados parecen causar ctsProfileMatch = false.

Puede hacerlo en el lado del cliente, pero se recomienda analizar la respuesta en el lado del servidor. Una arquitectura básica de servidor cliente con API de red de seguridad se verá así:

ingrese la descripción de la imagen aquí


3
Excelente información, y en un contexto diferente, creo que esta sería la respuesta correcta. Desafortunadamente, la pregunta de los OP no se trata de defender su aplicación de entornos inseguros, sino de detectar la raíz para habilitar funciones solo de raíz en su aplicación. Para el propósito previsto de los OP, este proceso parece demasiado complejo.
rsimp

31

La verificación de raíz a nivel de Java no es una solución segura. Si su aplicación tiene problemas de seguridad para ejecutarse en un dispositivo rooteado, utilice esta solución.

La respuesta de Kevin funciona a menos que el teléfono también tenga una aplicación como RootCloak. Dichas aplicaciones tienen una API Handle over Java una vez que el teléfono está rooteado y se burlan de estas API para que el teléfono no esté rooteado.

He escrito un código de nivel nativo basado en la respuesta de Kevin, ¡funciona incluso con RootCloak! Además, no causa problemas de pérdida de memoria.

#include <string.h>
#include <jni.h>
#include <time.h>
#include <sys/stat.h>
#include <stdio.h>
#include "android_log.h"
#include <errno.h>
#include <unistd.h>
#include <sys/system_properties.h>

JNIEXPORT int JNICALL Java_com_test_RootUtils_checkRootAccessMethod1(
        JNIEnv* env, jobject thiz) {


    //Access function checks whether a particular file can be accessed
    int result = access("/system/app/Superuser.apk",F_OK);

    ANDROID_LOGV( "File Access Result %d\n", result);

    int len;
    char build_tags[PROP_VALUE_MAX]; // PROP_VALUE_MAX from <sys/system_properties.h>.
    len = __system_property_get(ANDROID_OS_BUILD_TAGS, build_tags); // On return, len will equal (int)strlen(model_id).
    if(strcmp(build_tags,"test-keys") == 0){
        ANDROID_LOGV( "Device has test keys\n", build_tags);
        result = 0;
    }
    ANDROID_LOGV( "File Access Result %s\n", build_tags);
    return result;

}

JNIEXPORT int JNICALL Java_com_test_RootUtils_checkRootAccessMethod2(
        JNIEnv* env, jobject thiz) {
    //which command is enabled only after Busy box is installed on a rooted device
    //Outpput of which command is the path to su file. On a non rooted device , we will get a null/ empty path
    //char* cmd = const_cast<char *>"which su";
    FILE* pipe = popen("which su", "r");
    if (!pipe) return -1;
    char buffer[128];
    std::string resultCmd = "";
    while(!feof(pipe)) {
        if(fgets(buffer, 128, pipe) != NULL)
            resultCmd += buffer;
    }
    pclose(pipe);

    const char *cstr = resultCmd.c_str();
    int result = -1;
    if(cstr == NULL || (strlen(cstr) == 0)){
        ANDROID_LOGV( "Result of Which command is Null");
    }else{
        result = 0;
        ANDROID_LOGV( "Result of Which command %s\n", cstr);
        }
    return result;

}

JNIEXPORT int JNICALL Java_com_test_RootUtils_checkRootAccessMethod3(
        JNIEnv* env, jobject thiz) {


    int len;
    char build_tags[PROP_VALUE_MAX]; // PROP_VALUE_MAX from <sys/system_properties.h>.
    int result = -1;
    len = __system_property_get(ANDROID_OS_BUILD_TAGS, build_tags); // On return, len will equal (int)strlen(model_id).
    if(len >0 && strstr(build_tags,"test-keys") != NULL){
        ANDROID_LOGV( "Device has test keys\n", build_tags);
        result = 0;
    }

    return result;

}

En su código Java, debe crear RootUtils de clase de contenedor para realizar las llamadas nativas

    public boolean checkRooted() {

       if( rootUtils.checkRootAccessMethod3()  == 0 || rootUtils.checkRootAccessMethod1()  == 0 || rootUtils.checkRootAccessMethod2()  == 0 )
           return true;
      return false;
     }

1
Creo que la detección de raíz se divide en dos categorías, lo que permite características dependientes de la raíz y luego medidas basadas en la seguridad para tratar de mitigar los problemas de seguridad con los teléfonos rooteados. Para las características dependientes de la raíz, encuentro que la respuesta de Kevin es bastante pobre. En el contexto de esta respuesta, estos métodos tienen más sentido. Aunque reescribiría el método 2 para no usar el cual y en su lugar iteraría sobre la variable de entorno PATH para buscar "su". "cuál" no está garantizado para estar en el teléfono.
rsimp

¿puede proporcionar un ejemplo de cómo usar este código c en java?
mrid

@mrid Compruebe cómo realizar llamadas JNI desde Java en Android.
Alok Kulkarni

Este método evita la omisión de la detección de raíz mediante la aplicación RootCloak. ¿Hay alguna técnica de derivación de raíz conocida que falle este método?
Nidhin

20

http://code.google.com/p/roottools/

Si no desea utilizar el archivo jar, simplemente use el código:

public static boolean findBinary(String binaryName) {
        boolean found = false;
        if (!found) {
            String[] places = { "/sbin/", "/system/bin/", "/system/xbin/",
                    "/data/local/xbin/", "/data/local/bin/",
                    "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/" };
            for (String where : places) {
                if (new File(where + binaryName).exists()) {
                    found = true;

                    break;
                }
            }
        }
        return found;
    }

El programa intentará encontrar su carpeta:

private static boolean isRooted() {
        return findBinary("su");
    }

Ejemplo:

if (isRooted()) {
   textView.setText("Device Rooted");

} else {
   textView.setText("Device Unrooted");
}

¡Gracias! Estoy usando esto como checkRootMethod4()con la respuesta de Kevin .
Sheharyar

1
Nunca agregue == truea un booleano, no agrega nada y no se ve bien.
minipif

2
@smoothBlue ¿Por qué lo haría? No está generando ningún Proceso como lo es la solución de DevrimTuncer.
FD_

1
Una mejor idea sería iterar sobre PATH, en lugar de codificar de manera rígida los directorios PATH típicos
rsimp

1
Use, if (isRooted())marque en lugar de escribir explícitamente verdadero. Es mejor seguir los patrones de escritura de código
blueware

13

En lugar de usar isRootAvailable (), puede usar isAccessGiven (). Directo del wiki de RootTools :

if (RootTools.isAccessGiven()) {
    // your app has been granted root access
}

RootTools.isAccessGiven () no solo verifica que un dispositivo esté rooteado, sino que también llama a su para su aplicación, solicita permiso y devuelve verdadero si su aplicación obtuvo los permisos de root con éxito. Esto se puede usar como la primera verificación en su aplicación para asegurarse de que se le otorgue acceso cuando lo necesite.

Referencia


pero el usuario tiene que dar acceso a la raíz ¿verdad? así que si mi objetivo era detener la ejecución de mi aplicación si el dispositivo está rooteado, entonces mis opciones son realmente limitadas
Nasz Njoka Sr.

11

Algunas compilaciones modificadas se utilizan para establecer la propiedad del sistemaro.modversion para este propósito. Las cosas parecen haber seguido adelante; mi versión de TheDude hace unos meses tiene esto:

cmb@apollo:~$ adb -d shell getprop |grep build
[ro.build.id]: [CUPCAKE]
[ro.build.display.id]: [htc_dream-eng 1.5 CUPCAKE eng.TheDudeAbides.20090427.235325 test-keys]
[ro.build.version.incremental]: [eng.TheDude.2009027.235325]
[ro.build.version.sdk]: [3]
[ro.build.version.release]: [1.5]
[ro.build.date]: [Mon Apr 20 01:42:32 CDT 2009]
[ro.build.date.utc]: [1240209752]
[ro.build.type]: [eng]
[ro.build.user]: [TheDude]
[ro.build.host]: [ender]
[ro.build.tags]: [test-keys]
[ro.build.product]: [dream]
[ro.build.description]: [kila-user 1.1 PLAT-RC33 126986 ota-rel-keys,release-keys]
[ro.build.fingerprint]: [tmobile/kila/dream/trout:1.1/PLAT-RC33/126986:user/ota-rel-keys,release-keys]
[ro.build.changelist]: [17615# end build properties]

El emulador del SDK 1.5, por otro lado, que ejecuta la imagen 1.5, también tiene root, es probablemente similar al Android Dev Phone 1 (que presumiblemente desea permitir) y tiene esto:

cmb@apollo:~$ adb -e shell getprop |grep build
[ro.build.id]: [CUPCAKE]
[ro.build.display.id]: [sdk-eng 1.5 CUPCAKE 148875 test-keys]
[ro.build.version.incremental]: [148875]
[ro.build.version.sdk]: [3]
[ro.build.version.release]: [1.5]
[ro.build.date]: [Thu May 14 18:09:10 PDT 2009]
[ro.build.date.utc]: [1242349750]
[ro.build.type]: [eng]
[ro.build.user]: [android-build]
[ro.build.host]: [undroid16.mtv.corp.google.com]
[ro.build.tags]: [test-keys]
[ro.build.product]: [generic]
[ro.build.description]: [sdk-eng 1.5 CUPCAKE 148875 test-keys]
[ro.build.fingerprint]: [generic/sdk/generic/:1.5/CUPCAKE/148875:eng/test-keys]

En cuanto a las compilaciones minoristas, no tengo una a mano, pero varias búsquedas a continuación site:xda-developers.comson informativas. Aquí hay un G1 en los Países Bajos , puede ver que ro.build.tagsno tiene test-keys, y creo que esa es probablemente la propiedad más confiable para usar.


Eso parece interesante, pero: si bien el emulador (y ADP) permiten la raíz per se, no permiten que las aplicaciones lo usen, es decir: $ su app_29 $ su su: uid 10029 no está permitido su
miracle2k

Ah, supongo que no ... podría combinarlo con un cheque para ro.build.host (no) que termina en google.com, si son los únicos que tienen claves de prueba pero bloquean su sin preguntando al usuario. Depende de cuál sea el host de compilación para dispositivos más nuevos, cosas que no son teléfonos ... no es fácil.
Chris Boyle

11

RootBeer es una biblioteca de Android que verifica la raíz de Scott y Matthew. Utiliza varias comprobaciones para indicar si el dispositivo está rooteado o no.

Comprobaciones de Java

  • CheckRootManagementApps

  • CheckPotentiallyDangerousAppss

  • CheckRootCloakingApps

  • CheckTestKeys

  • checkForDangerousProps

  • checkForBusyBoxBinary

  • checkForSuBinary

  • checkSuExists

  • checkForRWSystem

Cheques nativos

Llamamos a nuestro verificador de raíz nativo para ejecutar algunas de sus propias verificaciones. Las comprobaciones nativas suelen ser más difíciles de ocultar, por lo que algunas aplicaciones de ocultación simplemente bloquean la carga de bibliotecas nativas que contienen ciertas palabras clave.

  • checkForSuBinary

8

Sugiero usar código nativo para la detección de raíz. Aquí hay un ejemplo de trabajo completo .

ingrese la descripción de la imagen aquí

Envoltura de JAVA :

package com.kozhevin.rootchecks.util;


import android.support.annotation.NonNull;

import com.kozhevin.rootchecks.BuildConfig;

public class MeatGrinder {
    private final static String LIB_NAME = "native-lib";
    private static boolean isLoaded;
    private static boolean isUnderTest = false;

    private MeatGrinder() {

    }

    public boolean isLibraryLoaded() {
        if (isLoaded) {
            return true;
        }
        try {
            if(isUnderTest) {
                throw new UnsatisfiedLinkError("under test");
            }
            System.loadLibrary(LIB_NAME);
            isLoaded = true;
        } catch (UnsatisfiedLinkError e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }
        }
        return isLoaded;
    }

    public native boolean isDetectedDevKeys();

    public native boolean isDetectedTestKeys();

    public native boolean isNotFoundReleaseKeys();

    public native boolean isFoundDangerousProps();

    public native boolean isPermissiveSelinux();

    public native boolean isSuExists();

    public native boolean isAccessedSuperuserApk();

    public native boolean isFoundSuBinary();

    public native boolean isFoundBusyboxBinary();

    public native boolean isFoundXposed();

    public native boolean isFoundResetprop();

    public native boolean isFoundWrongPathPermission();

    public native boolean isFoundHooks();

    @NonNull
    public static MeatGrinder getInstance() {
        return InstanceHolder.INSTANCE;
    }

    private static class InstanceHolder {
        private static final MeatGrinder INSTANCE = new MeatGrinder();
    }
}

JNI wrapper (native-lib.c) :

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedTestKeys(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isDetectedTestKeys();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedDevKeys(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isDetectedDevKeys();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isNotFoundReleaseKeys(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isNotFoundReleaseKeys();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundDangerousProps(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundDangerousProps();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isPermissiveSelinux(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isPermissiveSelinux();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isSuExists(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isSuExists();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isAccessedSuperuserApk(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isAccessedSuperuserApk();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundSuBinary(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundSuBinary();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundBusyboxBinary(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundBusyboxBinary();
}


JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundXposed(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundXposed();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundResetprop(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundResetprop();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundWrongPathPermission(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundWrongPathPermission();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundHooks(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundHooks();
}

constantes:

// Comma-separated tags describing the build, like= "unsigned,debug".
const char *const ANDROID_OS_BUILD_TAGS = "ro.build.tags";

// A string that uniquely identifies this build. 'BRAND/PRODUCT/DEVICE:RELEASE/ID/VERSION.INCREMENTAL:TYPE/TAGS'.
const char *const ANDROID_OS_BUILD_FINGERPRINT = "ro.build.fingerprint";

const char *const ANDROID_OS_SECURE = "ro.secure";

const char *const ANDROID_OS_DEBUGGABLE = "ro.debuggable";
const char *const ANDROID_OS_SYS_INITD = "sys.initd";
const char *const ANDROID_OS_BUILD_SELINUX = "ro.build.selinux";
//see https://android.googlesource.com/platform/system/core/+/master/adb/services.cpp#86
const char *const SERVICE_ADB_ROOT = "service.adb.root";

const char * const MG_SU_PATH[] = {
        "/data/local/",
        "/data/local/bin/",
        "/data/local/xbin/",
        "/sbin/",
        "/system/bin/",
        "/system/bin/.ext/",
        "/system/bin/failsafe/",
        "/system/sd/xbin/",
        "/su/xbin/",
        "/su/bin/",
        "/magisk/.core/bin/",
        "/system/usr/we-need-root/",
        "/system/xbin/",
        0
};

const char * const MG_EXPOSED_FILES[] = {
        "/system/lib/libxposed_art.so",
        "/system/lib64/libxposed_art.so",
        "/system/xposed.prop",
        "/cache/recovery/xposed.zip",
        "/system/framework/XposedBridge.jar",
        "/system/bin/app_process64_xposed",
        "/system/bin/app_process32_xposed",
        "/magisk/xposed/system/lib/libsigchain.so",
        "/magisk/xposed/system/lib/libart.so",
        "/magisk/xposed/system/lib/libart-disassembler.so",
        "/magisk/xposed/system/lib/libart-compiler.so",
        "/system/bin/app_process32_orig",
        "/system/bin/app_process64_orig",
        0
};

const char * const MG_READ_ONLY_PATH[] = {
        "/system",
        "/system/bin",
        "/system/sbin",
        "/system/xbin",
        "/vendor/bin",
        "/sbin",
        "/etc",
        0
};

detecciones de raíz del código nativo:

struct mntent *getMntent(FILE *fp, struct mntent *e, char *buf, int buf_len) {

    while (fgets(buf, buf_len, fp) != NULL) {
        // Entries look like "/dev/block/vda /system ext4 ro,seclabel,relatime,data=ordered 0 0".
        // That is: mnt_fsname mnt_dir mnt_type mnt_opts mnt_freq mnt_passno.
        int fsname0, fsname1, dir0, dir1, type0, type1, opts0, opts1;
        if (sscanf(buf, " %n%*s%n %n%*s%n %n%*s%n %n%*s%n %d %d",
                   &fsname0, &fsname1, &dir0, &dir1, &type0, &type1, &opts0, &opts1,
                   &e->mnt_freq, &e->mnt_passno) == 2) {
            e->mnt_fsname = &buf[fsname0];
            buf[fsname1] = '\0';
            e->mnt_dir = &buf[dir0];
            buf[dir1] = '\0';
            e->mnt_type = &buf[type0];
            buf[type1] = '\0';
            e->mnt_opts = &buf[opts0];
            buf[opts1] = '\0';
            return e;
        }
    }
    return NULL;
}


bool isPresentMntOpt(const struct mntent *pMnt, const char *pOpt) {
    char *token = pMnt->mnt_opts;
    const char *end = pMnt->mnt_opts + strlen(pMnt->mnt_opts);
    const size_t optLen = strlen(pOpt);
    while (token != NULL) {
        const char *tokenEnd = token + optLen;
        if (tokenEnd > end) break;
        if (memcmp(token, pOpt, optLen) == 0 &&
            (*tokenEnd == '\0' || *tokenEnd == ',' || *tokenEnd == '=')) {
            return true;
        }
        token = strchr(token, ',');
        if (token != NULL) {
            token++;
        }
    }
    return false;
}

static char *concat2str(const char *pString1, const char *pString2) {
    char *result;
    size_t lengthBuffer = 0;

    lengthBuffer = strlen(pString1) +
                   strlen(pString2) + 1;
    result = malloc(lengthBuffer);
    if (result == NULL) {
        GR_LOGW("malloc failed\n");
        return NULL;
    }
    memset(result, 0, lengthBuffer);
    strcpy(result, pString1);
    strcat(result, pString2);
    return result;
}

static bool
isBadPropertyState(const char *key, const char *badValue, bool isObligatoryProperty, bool isExact) {
    if (badValue == NULL) {
        GR_LOGE("badValue may not be NULL");
        return false;
    }
    if (key == NULL) {
        GR_LOGE("key may not be NULL");
        return false;
    }
    char value[PROP_VALUE_MAX + 1];
    int length = __system_property_get(key, value);
    bool result = false;
    /* A length 0 value indicates that the property is not defined */
    if (length > 0) {
        GR_LOGI("property:[%s]==[%s]", key, value);
        if (isExact) {
            if (strcmp(value, badValue) == 0) {
                GR_LOGW("bad value[%s] equals to [%s] in the property [%s]", value, badValue, key);
                result = true;
            }
        } else {
            if (strlen(value) >= strlen(badValue) && strstr(value, badValue) != NULL) {
                GR_LOGW("bad value[%s] found in [%s] in the property [%s]", value, badValue, key);
                result = true;
            }
        }
    } else {
        GR_LOGI("[%s] property not found", key);
        if (isObligatoryProperty) {
            result = true;
        }
    }
    return result;
}

bool isDetectedTestKeys() {
    const char *TEST_KEYS_VALUE = "test-keys";
    return isBadPropertyState(ANDROID_OS_BUILD_TAGS, TEST_KEYS_VALUE, true, false);
}

bool isDetectedDevKeys() {
    const char *DEV_KEYS_VALUE = "dev-keys";
    return isBadPropertyState(ANDROID_OS_BUILD_TAGS, DEV_KEYS_VALUE, true, false);
}

bool isNotFoundReleaseKeys() {
    const char *RELEASE_KEYS_VALUE = "release-keys";
    return !isBadPropertyState(ANDROID_OS_BUILD_TAGS, RELEASE_KEYS_VALUE, false, true);
}

bool isFoundWrongPathPermission() {

    bool result = false;
    FILE *file = fopen("/proc/mounts", "r");
    char mntent_strings[BUFSIZ];
    if (file == NULL) {
        GR_LOGE("setmntent");
        return result;
    }

    struct mntent ent = {0};
    while (NULL != getMntent(file, &ent, mntent_strings, sizeof(mntent_strings))) {
        for (size_t i = 0; MG_READ_ONLY_PATH[i]; i++) {
            if (strcmp((&ent)->mnt_dir, MG_READ_ONLY_PATH[i]) == 0 &&
                isPresentMntOpt(&ent, "rw")) {
                GR_LOGI("%s %s %s %s\n", (&ent)->mnt_fsname, (&ent)->mnt_dir, (&ent)->mnt_opts,
                        (&ent)->mnt_type);
                result = true;
                break;
            }
        }
        memset(&ent, 0, sizeof(ent));
    }
    fclose(file);
    return result;
}


bool isFoundDangerousProps() {
    const char *BAD_DEBUGGABLE_VALUE = "1";
    const char *BAD_SECURE_VALUE = "0";
    const char *BAD_SYS_INITD_VALUE = "1";
    const char *BAD_SERVICE_ADB_ROOT_VALUE = "1";

    bool result = isBadPropertyState(ANDROID_OS_DEBUGGABLE, BAD_DEBUGGABLE_VALUE, true, true) ||
                  isBadPropertyState(SERVICE_ADB_ROOT, BAD_SERVICE_ADB_ROOT_VALUE, false, true) ||
                  isBadPropertyState(ANDROID_OS_SECURE, BAD_SECURE_VALUE, true, true) ||
                  isBadPropertyState(ANDROID_OS_SYS_INITD, BAD_SYS_INITD_VALUE, false, true);

    return result;
}

bool isPermissiveSelinux() {
    const char *BAD_VALUE = "0";
    return isBadPropertyState(ANDROID_OS_BUILD_SELINUX, BAD_VALUE, false, false);
}

bool isSuExists() {
    char buf[BUFSIZ];
    char *str = NULL;
    char *temp = NULL;
    size_t size = 1;  // start with size of 1 to make room for null terminator
    size_t strlength;

    FILE *pipe = popen("which su", "r");
    if (pipe == NULL) {
        GR_LOGI("pipe is null");
        return false;
    }

    while (fgets(buf, sizeof(buf), pipe) != NULL) {
        strlength = strlen(buf);
        temp = realloc(str, size + strlength);  // allocate room for the buf that gets appended
        if (temp == NULL) {
            // allocation error
            GR_LOGE("Error (re)allocating memory");
            pclose(pipe);
            if (str != NULL) {
                free(str);
            }
            return false;
        } else {
            str = temp;
        }
        strcpy(str + size - 1, buf);
        size += strlength;
    }
    pclose(pipe);
    GR_LOGW("A size of the result from pipe is [%zu], result:\n [%s] ", size, str);
    if (str != NULL) {
        free(str);
    }
    return size > 1 ? true : false;
}

static bool isAccessedFile(const char *path) {
    int result = access(path, F_OK);
    GR_LOGV("[%s] has been accessed with result: [%d]", path, result);
    return result == 0 ? true : false;
}

static bool isFoundBinaryFromArray(const char *const *array, const char *binary) {
    for (size_t i = 0; array[i]; ++i) {
        char *checkedPath = concat2str(array[i], binary);
        if (checkedPath == NULL) { // malloc failed
            return false;
        }
        bool result = isAccessedFile(checkedPath);
        free(checkedPath);
        if (result) {
            return result;
        }
    }
    return false;
}

bool isAccessedSuperuserApk() {
    return isAccessedFile("/system/app/Superuser.apk");
}

bool isFoundResetprop() {
    return isAccessedFile("/data/magisk/resetprop");
}

bool isFoundSuBinary() {
    return isFoundBinaryFromArray(MG_SU_PATH, "su");
}

bool isFoundBusyboxBinary() {
    return isFoundBinaryFromArray(MG_SU_PATH, "busybox");
}

bool isFoundXposed() {
    for (size_t i = 0; MG_EXPOSED_FILES[i]; ++i) {
        bool result = isAccessedFile(MG_EXPOSED_FILES[i]);
        if (result) {
            return result;
        }
    }
    return false;
}

bool isFoundHooks() {
    bool result = false;
    pid_t pid = getpid();
    char maps_file_name[512];
    sprintf(maps_file_name, "/proc/%d/maps", pid);
    GR_LOGI("try to open [%s]", maps_file_name);
    const size_t line_size = BUFSIZ;
    char *line = malloc(line_size);
    if (line == NULL) {
        return result;
    }
    FILE *fp = fopen(maps_file_name, "r");
    if (fp == NULL) {
        free(line);
        return result;
    }
    memset(line, 0, line_size);
    const char *substrate = "com.saurik.substrate";
    const char *xposed = "XposedBridge.jar";
    while (fgets(line, line_size, fp) != NULL) {
        const size_t real_line_size = strlen(line);
        if ((real_line_size >= strlen(substrate) && strstr(line, substrate) != NULL) ||
            (real_line_size >= strlen(xposed) && strstr(line, xposed) != NULL)) {
            GR_LOGI("found in [%s]: [%s]", maps_file_name, line);
            result = true;
            break;
        }
    }
    free(line);
    fclose(fp);
    return result;
}

44
Impresionante herramienta, Dima. Muchas gracias. Incluso atrapa magisk.
experto

Este es el verdadero negocio.
Vahid Amiri

@klutch hay un enlace al ejemplo de trabajo (github) en la primera línea de mi publicación
Dima Kozhevin

7

Aquí está mi código basado en algunas respuestas aquí:

 /**
   * Checks if the phone is rooted.
   * 
   * @return <code>true</code> if the phone is rooted, <code>false</code>
   * otherwise.
   */
  public static boolean isPhoneRooted() {

    // get from build info
    String buildTags = android.os.Build.TAGS;
    if (buildTags != null && buildTags.contains("test-keys")) {
      return true;
    }

    // check if /system/app/Superuser.apk is present
    try {
      File file = new File("/system/app/Superuser.apk");
      if (file.exists()) {
        return true;
      }
    } catch (Throwable e1) {
      // ignore
    }

    return false;
  }

7

Además de la respuesta de @Kevins, recientemente descubrí que mientras usaba su sistema, el Nexus 7.1 regresaba falsepara los tres métodos: sin whichcomando, no test-keysy SuperSUno estaba instalado /system/app.

Agregué esto:

public static boolean checkRootMethod4(Context context) {
    return isPackageInstalled("eu.chainfire.supersu", context);     
}

private static boolean isPackageInstalled(String packagename, Context context) {
    PackageManager pm = context.getPackageManager();
    try {
        pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES);
        return true;
    } catch (NameNotFoundException e) {
        return false;
    }
}

Esto es un poco menos útil en algunas situaciones (si necesita acceso root garantizado), ya que es completamente posible que SuperSU se instale en dispositivos que no tienen acceso SU.

Sin embargo, dado que es posible tener SuperSU instalado y funcionando pero no en el /system/appdirectorio, este caso adicional eliminará (jaja) tales casos.


Esta no es una buena respuesta, ya que tiene otros paquetes raíz que se pueden instalar en su dispositivo. La codificación dura de otros paquetes de aplicaciones sería difícil, como no se puede esperar, y enumerarlos a todos
blueware

5
    public static boolean isRootAvailable(){
            Process p = null;
            try{
               p = Runtime.getRuntime().exec(new String[] {"su"});
               writeCommandToConsole(p,"exit 0");
               int result = p.waitFor();
               if(result != 0)
                   throw new Exception("Root check result with exit command " + result);
               return true;
            } catch (IOException e) {
                Log.e(LOG_TAG, "Su executable is not available ", e);
            } catch (Exception e) {
                Log.e(LOG_TAG, "Root is unavailable ", e);
            }finally {
                if(p != null)
                    p.destroy();
            }
            return false;
        }
 private static String writeCommandToConsole(Process proc, String command, boolean ignoreError) throws Exception{
            byte[] tmpArray = new byte[1024];
            proc.getOutputStream().write((command + "\n").getBytes());
            proc.getOutputStream().flush();
            int bytesRead = 0;
            if(proc.getErrorStream().available() > 0){
                if((bytesRead = proc.getErrorStream().read(tmpArray)) > 1){
                    Log.e(LOG_TAG,new String(tmpArray,0,bytesRead));
                    if(!ignoreError)
                        throw new Exception(new String(tmpArray,0,bytesRead));
                }
            }
            if(proc.getInputStream().available() > 0){
                bytesRead = proc.getInputStream().read(tmpArray);
                Log.i(LOG_TAG, new String(tmpArray,0,bytesRead));
            }
            return new String(tmpArray);
        }

4

Dos ideas adicionales, si desea verificar si un dispositivo es capaz de rootear desde su aplicación:

  1. Verifique la existencia del binario 'su': ejecute "which su" desde Runtime.getRuntime().exec()
  2. Busque el SuperUser.apk en /system/app/Superuser.apkubicación

3

Usar C ++ con el ndk es el mejor enfoque para detectar root incluso si el usuario está usando aplicaciones que ocultan su root, como RootCloak. Probé este código con RootCloak y pude detectar la raíz incluso si el usuario está tratando de ocultarlo. Entonces su archivo cpp quisiera:

#include <jni.h>
#include <string>


/**
 *
 * function that checks for the su binary files and operates even if 
 * root cloak is installed
 * @return integer 1: device is rooted, 0: device is not 
 *rooted
*/
extern "C"
JNIEXPORT int JNICALL


Java_com_example_user_root_1native_rootFunction(JNIEnv *env,jobject thiz){
const char *paths[] ={"/system/app/Superuser.apk", "/sbin/su", "/system/bin/su",
                      "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
                      "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su"};

int counter =0;
while (counter<9){
    if(FILE *file = fopen(paths[counter],"r")){
        fclose(file);
        return 1;
    }
    counter++;
}
return 0;
}

Y llamará a la función desde su código java de la siguiente manera

public class Root_detect {



   /**
    *
    * function that calls a native function to check if the device is 
    *rooted or not
    * @return boolean: true if the device is rooted, false if the 
    *device is not rooted
   */
   public boolean check_rooted(){

        int checker = rootFunction();

        if(checker==1){
           return true;
        }else {
           return false;
        }
   }
   static {
    System.loadLibrary("cpp-root-lib");//name of your cpp file
   }

   public native int rootFunction();
}

1
if [[ "`adb shell which su | grep -io "permission denied"`" != "permission denied" ]]; then
   echo "Yes. Rooted device."
 else
   echo "No. Device not rooted. Only limited tasks can be performed. Done."
    zenity --warning --title="Device Not Rooted" --text="The connected Android Device is <b>NOT ROOTED</b>. Only limited tasks can be performed." --no-wrap
fi


1

Olvídate de todo lo que detecta aplicaciones raíz y binarios su. Verifique el proceso del demonio raíz. Esto se puede hacer desde el terminal y puede ejecutar comandos de terminal dentro de una aplicación. Prueba este one-liner.

if [ ! -z "$(/system/bin/ps -A | grep -v grep | grep -c daemonsu)" ]; then echo "device is rooted"; else echo "device is not rooted"; fi

Tampoco necesita permiso de root para lograr esto.


0

De hecho, es una pregunta interesante y hasta ahora nadie ha merecido un premio. Yo uso el siguiente código:

  boolean isRooted() {
      try {
                ServerSocket ss = new ServerSocket(81);
                ss.close();
                                    return true;
            } catch (Exception e) {
                // not sure
            }
    return false;
  }

El código ciertamente no es a prueba de balas, porque la red puede no estar disponible, por lo que obtendrá una excepción. Si este método devuelve verdadero, entonces el 99% puede estar seguro, de lo contrario solo el 50% no. El permiso de red también puede estropear la solución.


Probé esto y no devuelve verdadero con mi dispositivo rooteado.
Tricknology

Es interesante ver qué tipo de excepción obtienes. Puede obtener una excepción de puerto ya vinculado, sin embargo, si no puede crear un puerto de servidor en un rango inferior a 1024, disminuye el valor de enraizamiento, ya que aún tiene ciertas limitaciones.
Singagirl

-1

Usar mi biblioteca en rootbox es bastante fácil. Verifique el código requerido a continuación:

    //Pass true to <Shell>.start(...) call to run as superuser
    Shell shell = null;
    try {
            shell = Shell.start(true);
    } catch (IOException exception) {
            exception.printStackTrace();
    }
    if (shell == null)
            // We failed to execute su binary
            return;
    if (shell.isRoot()) {
            // Verified running as uid 0 (root), can continue with commands
            ...
    } else
            throw Exception("Unable to gain root access. Make sure you pressed Allow/Grant in superuser prompt.");
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.