Ingeniería inversa APK: Deusto App
Published: 2024-05-11 10:44:28

### 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](https://www.charlesproxy.com/), 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

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>

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](https://github.com/skylot/jadx) vamos a descompilar la aplicación para su posterior análisis.

``` bash
❯ jadx -d AcademicMobileDEUSTO_4_0_19 AcademicMobileDEUSTO_4_0_19.apk
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l2045u11e9o1sgxvhous.png)

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
Descripción generada automáticamente](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3sl4t5zg0yhlegb99ou4.png)


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:

``` java
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);
}
```

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

``` java
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;
    }
}
```

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

``` java
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();
}
```

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:

``` java
Map<String, String> map = new HashMap<>();
map.put("clave2", "valor2");
map.put("clave1", "valor1");
map.put("clave3", "valor3");
```

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

``` java
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
}
```

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:

``` java
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));
}
```

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
automáticamente](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/orrafwk62wxwt2hxww23.png)

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

``` java
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;
}
```

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

``` typescript
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>
```

2.  Formamos el contenido definitivo de la petición añadiéndole el hash
    MD5 calculado

``` json
{
  "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>"
}
```

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.