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
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>®istrationID=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 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
Veamos en qué parte se utiliza esta variable que contiene la string "hash" ICON_HASH_KEY
.
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:
Parece que
a(Map<String, String> map)
ordena el mapa por orden
alfabético de sus claves.generarHash(Map<String, String> map)
genera una string que luego
es convertida a hash MD5 a través dea(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:
Se han ordenado los ítems del mapa según las claves: clave1, clave2,
clave3Se ha creado una string separando los valores (con el nuevo orden)
por el carácter '~':valor1~valor2~valor3
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:
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:
- 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>
- 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.
Top comments (0)