DEV Community

NULLX
NULLX

Posted on • Updated on

Ingeniería inversa APK: Deusto App

Sobre Deusto App

Deusto App es una aplicación de la Universidad de Deusto con la que además de otras diversas funciones, puedes consultar tus calificaciones finales como estudiante.

Se entiende que esta aplicación hace uso de las comunicaciones para obtener la información de un repositorio securizado.

Queremos llegar a saber cómo se comunica la aplicación con dicho repositorio a la hora de obtener las calificaciones de un usuario. Para hacer esto inicialmente tenemos dos opciones:

  • Analizar el tráfico de red
  • Investigar el propio código

Se empieza por el primero, ya que es el más sencillo. De no obtener información suficiente pasaremos al segundo método.

Análisis del tráfico de red

Utilizando una herramienta de análisis de tráfico http, como es Charles, podemos ver que las peticiones http se realizan de la siguiente manera:

Identificación:

POST /AdvancedProtocolRedirector/resources/identificacion HTTP/1.1
Host: <HOST>
Content-Type: application/x-www-form-urlencoded
Content-Length: <TAMAÑO DEL CONTENIDO DE LA PETICIÓN>

av=3.6.10&hash=00841f1b43tcc4dce4y7f26e2039827f&idioma=es&multi=true&os=iOS&pais=ES&password=<CONTRASEÑA>&time=<STAMPA DE TIEMPO>&usuario=<CORREO ELECTRÓNICO>&token=&sv=15.2
Enter fullscreen mode Exit fullscreen mode

Obtención de calificaciones:

POST /academic-rest/resources-ext/obtenerCalificacionesExt HTTP/1.1
Host: <HOST>
Content-Type: application/x-www-form-urlencoded
Content-Length: <TAMAÑO DEL CONTENIDO DE LA PETICIÓN>

av=3.6.10&hash=7f99544ebfdb531bfd8586f7af09b592&idioma=es&multi=true&multicliente=N&os=iOS&pais=ES&perfilActivo=<PERFIL ACTIVO>&registrationID=18AB34A93C05AF04E54128EF3FD859DA97783D81107E98C881245E2DFAC566E9&sv=15.2&time=1640618821566&token=<TOKEN>
Enter fullscreen mode Exit fullscreen mode

Las peticiones necesitan ciertos parámetros, de los que destacan:

  • CORREO ELECTRÓNICO
  • CONTRASEÑA
  • HASH
  • TOKEN (se entiende que será una forma de autenticarse tras el inicio de sesión)

De los campos anteriores, disponemos de todos ellos menos del hash. En un principio se puede intuir lo que significa el hash, pero no conocemos la manera de recrear su valor.

Si probamos a cambiar el hash por otro cualquiera la API devuelve un error, por lo que vemos que es necesario para poder realizar tanto el inicio de sesión como las siguientes peticiones.

Tras distintos intentos fallidos de adivinar cómo obtener dicho hash se decide inspeccionar el código para obtener el algoritmo que revela cómo calcular dicho hash.

Análisis del código fuente

No es nada trivial inspeccionar el código de una aplicación ya compilada puesta en producción ya que estas suelen estar preparadas para dificultar lo máximo posible su entendimiento. Dentro del proceso de compilación existe una fase denominada ofuscación, la cual se encarga de enrevesar de diversas formas el código (renombramiento de variables y funciones, alteración de la estructura, aparición de código muerto, uso de reflexión...) de forma que si alguien intenta analizarlo le sea muy difícil.

Haciendo uso de una herramienta de ingeniería inversa llamada jadx vamos a descompilar la aplicación para su posterior análisis.

❯ jadx -d AcademicMobileDEUSTO_4_0_19 AcademicMobileDEUSTO_4_0_19.apk
Enter fullscreen mode Exit fullscreen mode

Ahora debemos buscar la zona del código donde se añade esa clave "hash" al contenido de la petición http. Para ello buscamos dentro del código java compilado por "hash". Parece que el código principal de la aplicación se encuentra dentro del paquete org.sigmaaie.mobile.* ya que el resto de los paquetes son librerías externas. Es dentro de este paquete donde buscaremos por "hash".

El único lugar de todo el código fuente donde se define "hash" es en siguiente nodo

Texto Descripción generada automáticamente

Veamos en qué parte se utiliza esta variable que contiene la string "hash" ICON_HASH_KEY.

Interfaz de usuario gráfica, Texto, Aplicación, Correo electrónico<br>
Descripción generada automáticamente

Hay varios resultados que pueden resultar interesantes, sin embargo, vamos a probar con la remarcada en la anterior imagen.

El método UtilRest.generarHash es el siguiente:

public static String generarHash(Map<String, String> map) {
    List<String> a = a(map);
    String str = "";
    int i = 0;
    while (i < a.size()) {
        if (i > 0) {
            str = str + "~";
        }
        String str2 = str + ((Object) a.get(i));
        i++;
        str = str2;
    }
    return a(str);
}
Enter fullscreen mode Exit fullscreen mode

Parece que esta función hace uso de otra llamada
UtilRest.a(Map<String, String>):

private static List<String> a(Map<String, String> map) {
    ArrayList arrayList = new ArrayList();
    ArrayList arrayList2 = new ArrayList();
    for (String str : map.keySet()) {
        arrayList2.add(str);
    }
    Collections.sort(arrayList2);
    int i = 0;
    while (true) {
        int i2 = i;
        if (i2 >= arrayList2.size()) {
            return arrayList;
        }
        arrayList.add(map.get(arrayList2.get(i2)));
        i = i2 + 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Y además necesita de otra función UtilRest.a(String), que a su vez
llama a otra UtilRest.a(byte[] bArr).

private static String a(String str) {
    try {
        MessageDigest messageDigest = MessageDigest.getInstance(CommonUtils.MD5_INSTANCE);
        messageDigest.reset;
        messageDigest.update(str.getBytes());
        return a(messageDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}

private static String a(byte[] bArr) {
    StringBuilder sb = new StringBuilder(bArr.length * 2);
    for (byte b : bArr) {
        int i = b & 255;
        if (i < 16) {
            sb.append('0');
        }
        sb.append(Integer.toHexString(i));
    }
    return sb.toString().toLowerCase();
}
Enter fullscreen mode Exit fullscreen mode

Después de analizar su comportamiento y ejecutar el código de forma independiente y se concluye lo siguiente:

  1. Parece que a(Map<String, String> map) ordena el mapa por orden
    alfabético de sus claves.

  2. generarHash(Map<String, String> map) genera una string que luego
    es convertida a hash MD5 a través de a(String str).

Se necesita saber qué algoritmo es utilizado para formar dicha string previa a la conversión MD5 (a(String str)). Para esto podríamos detenernos a observar el código o directamente ejecutarlo para ver cómo reacciona:

Dado el siguiente HashMap:

Map<String, String> map = new HashMap<>();
map.put("clave2", "valor2");
map.put("clave1", "valor1");
map.put("clave3", "valor3");
Enter fullscreen mode Exit fullscreen mode

Vamos a llamar a la función generarHash(map) para luego ver el valor de str:

public static String generarHash(Map<String, String> map) {
    List<String> a = a(map);
    String str = "";
    int i = 0;
    while (i < a.size()) {
        if (i > 0) {
            str = str + "~";
        }
        String str2 = str + ((Object) a.get(i));
        i++;
        str = str2;
    }

    System.out.println(str); // valor1~valor2~valor3
    return a(str); // e0a66defa6eb59967ead9d70868f7261
}
Enter fullscreen mode Exit fullscreen mode

Esto es lo que sale por consola: valor1~valor2~valor3.

Según el análisis del código de generarHash(Map<String, String>) y el ejemplo de ejecución se concluye en una primera instancia:

  1. Se han ordenado los ítems del mapa según las claves: clave1, clave2,
    clave3

  2. Se ha creado una string separando los valores (con el nuevo orden)
    por el carácter '~': valor1~valor2~valor3

  3. Se ha hecho la conversión de la string obtenida a un hash utilizando
    el algoritmo de reducción criptográfico MD5:
    e0a66defa6eb59967ead9d70868f7261

Ahora toca saber qué mapa inicial debe de ir en la llamada a la función padre generarHash(Map<String, String>).

La función que llama a la mencionada es la siguiente:

public static void b(Map<String, String> map, List<String> list, String str) {
    HashMap hashMap = new HashMap(map);
    if (list != null && !list.isEmpty()) {
        for (String str2 : list) {
            hashMap.remove(str2);
        }
    }
    hashMap.put("metodo", str);
    hashMap.put("secreto", ConnectionConfig.getInstance().getSecret());
    map.put(SettingsJsonConstants.ICON_HASH_KEY, UtilRest.generarHash(hashMap));
}
Enter fullscreen mode Exit fullscreen mode

Esto nos sugiere que hay un mapa del que ciertas entradas son eliminadas dada una lista de claves. También vemos que a este mapa se le añade dos claves: "método" (que viene como parámetro) y "secreto", que si buscamos de nuevo encontramos:

Interfaz de usuario gráfica, Texto Descripción generada<br>
automáticamente

Sigamos con el mapa, ¿de dónde viene? Encontramos una función create, que si tiramos del hilo...

public Map<String, String> create() {
    if (this.c == null) {
        throw new IllegalStateException("missing mainService");
    }
    if (this.e) {
        this.a.putAll(this.b.tokenDefaults());
        BaseClient.b(this.a, this.d, this.c);
        this.e = false;
        if (BaseClient.buildInfo.DEBUG) {
            Log.v("ParamBuilder", this.c + StringUtils.SPACE + this.a);
        }
        return this.a;
    }
    throw new IllegalStateException("already built");
}

protected final Map<String, String> tokenDefaults() {
    Map<String, String> defaults = this.b.getDefaults();
    String userToken = ConnectionConfig.getInstance().getUserToken();
    if (userToken != null) {
        defaults.put(UserContract.Users.COLUMN_TOKEN, userToken);
    }
    return defaults;
}

@Override // org.sigmaaie.mobile.sigmacore.rest.ParamProvider
@SuppressLint({"HardwareIds"})
public Map<String, String> getDefaults() {
    Locale locale = Locale.getDefault();
    HashMap hashMap = new HashMap();
    hashMap.put("multi", "true");
    hashMap.put("av", this.a);
    hashMap.put("avc", this.b);
    hashMap.put("sv", Build.VERSION.RELEASE);
    hashMap.put("os", "ANDROID");
    hashMap.put(PerfilIdCampos.PAIS, locale.getCountry());
    hashMap.put("idioma", locale.getLanguage());
    hashMap.put("nid", Build.SERIAL);
    String lastProfile = this.preferences.getLastProfile();
    if (lastProfile != null && !lastProfile.isEmpty()) {
        hashMap.put("perfilActivo", lastProfile);
    }
    ConnectionConfig connectionConfig = ConnectionConfig.getInstance();
    if (connectionConfig.EXTERNAL) {
        hashMap.put("pocket", "true");
        hashMap.put("pocket_id", connectionConfig.getUserID());
        hashMap.put("pocket_token", connectionConfig.getUserToken());
    }
    return hashMap;
}
Enter fullscreen mode Exit fullscreen mode

Nos damos cuenta de que el mapa que va a pasar por el generarHash(Map<String, String>) es el contenido de la petición, previamente observada, sin la clave "hash".

Conclusiones

Para replicar el funcionamiento de inicio de sesión:

  1. Generamos el hash con el contenido inicial de la petición
generarHash({
    "multi": true,
    "av": "...", // opcional
    "avc": "...", // opcional
    "sv": "...", // opcional
    "os": "...", // opcional: sistema operativo
    "idioma": "...", // opcional: idioma
    "nid": "...", // opcional: serial build
    "perfilActivo": "...", // opcional en login: ultimo perfil activo: nos servirá para las calificaciones
    "token": "...", // opcional en login: token de autenticación que nos devolverá el login
}) => <String: HASH MD5>
Enter fullscreen mode Exit fullscreen mode
  1. Formamos el contenido definitivo de la petición añadiéndole el hash MD5 calculado
{
  "multi": true,
  "av": "...", // opcional
  "avc": "...", // opcional
  "sv": "...", // opcional
  "os": "...", // opcional: sistema operativo
  "idioma": "...", // opcional: idioma
  "nid": "...", // opcional: serial build
  "perfilActivo": "...", // opcional en login: ultimo perfil activo: nos servirá para las calificaciones
  "token": "...", // opcional en login: token de autenticación que nos devolverá el login
  "hash": "<String: HASH MD5>"
}
Enter fullscreen mode Exit fullscreen mode

Hemos descubierto cómo se comunica la aplicación con el servidor para obtener las calificaciones. Este proceso ha demostrado la importancia de un componente específico, el hash, cuyo valor correcto es crucial para la autenticación y la obtención de datos.

El análisis detallado ha permitido replicar la comunicación exitosamente, abriendo la puerta a la posibilidad de desarrollar una función de notificaciones que mejore la experiencia del usuario sin comprometer la seguridad ni la integridad del sistema original.

Top comments (0)