Introducción
Además de las protecciones relacionadas con la exigencia de un certificado TLS específico (pinning) existe la posibilidad de cifrar las comunicaciones en red sobre el protocolo HTTP. En el caso del cifrado simétrico la aplicación obtendrá la clave del servidor o la obtendrá del propio código fuente de la aplicación, de forma estática (guardada en una variable), o de forma dinámica, en la que varios métodos ofuscados se ejecutarán para crear la clave a utilizar.
En este caso descompilaremos una aplicación para buscar el método encargado de generar la clave y con Frida modificaremos el método para imprimir la clave por pantalla. Al obtener la clave podremos descifrar el tráfico generado por la aplicación e interceptados con un proxy.
Análisis de la petición HTTP
Al interceptar la petición con el proxy obtenemos lo siguiente:
POST /api/systemInitialization/ HTTP/1.1
Content-Type: application/json; charset=UTF-8
Content-Length: 157
Host: api.amdjrptjgs.com:1111
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/4.9.0
{"param5":"XEy2oQt8bzxqeBtwBpwoJg==","param2":"0fMCWKI7yt40z2a1KZpb/w==","param4":"bOh76UF9jK1O3m86bwiaxQ==","param3":"tOLlDpAUH8PBPvliNVi6vQ==","param1":""}
Como observamos se está enviando al punto /api/systemInitialization/ un texto JSON con diferentes parámetros codificados con Base64. Al decodificarlos obtenemos datos en binario por lo que suponemos que los parámetros se encuentran cifrados.
Búsqueda del código fuente responsable de la petición
En la búsqueda del código que realiza la petición HTTP observamos que la aplicación utiliza Retrofit, un cliente HTTP y encontramos los diferentes puntos en la clase com.aplicaciondeprueba.net.retrofit.RetrofitService.
package com.aplicaciondeprueba.net.retrofit;
...
@POST("/api/systemInitialization/")
Call<SystemInitResponse> initSystem(@Body SystemInitRequest systemInitRequest);
Con ello encontramos la clase que almacenará la petición, SystemInitRequest.
package com.aplicaciondeprueba.net.request;
public class SystemInitRequest extends NewBasicRequest {
@JsonProperty("param5")
private String lastUpdate;
@JsonProperty("param2")
private String language;
@JsonProperty("param4")
private String os;
@JsonProperty("param3")
private String version;
Y la clase que almacenará la respuesta SystemInitResponse.
package com.aplicaciondeprueba.net.response;
public class SystemInitResponse extends BasicResponse {
@JsonProperty("param0")
private String symmetricKey;
@JsonProperty("param4")
private String language;
@JsonProperty("param3")
private String version;
}
Observamos una variable en la respuesta, symmetricKey, que se puede corresponder a la clave que estamos buscando, pero en este caso, este campo param0 en la respuesta aparece vacío, por lo que suponemos que la clave se obtiene localmente. Buscando referencias a SystemInit, encontramos el método manejador c de la clase defpackage.cn0 de la petición.
public final void c(Boolean bool, bn0 bn0Var) {
SystemInitRequest systemInitRequest = new SystemInitRequest();
systemInitRequest.setLang(yc.q(code, ro.K()));
systemInitRequest.setVersion(yc.q("v1", ro.K()));
systemInitRequest.setOS(yc.q("android14", ro.K()));
systemInitRequest.setLastUpdate(yc.q(kr0.o(c).B(), ro.K()));
this.a.initSystem(systemInitRequest).enqueue(new an0(this, bool, bn0Var));
}
Vemos que se establecen los parámetros de la petición a enviar y a continuación se encola. El valor que se guarda (codificado con Base64) es el valor devuelto por la función yc.q con el primer parámetro la cadena a cifrar y el segundo parámetro la clave a utilizar. En este caso el vector de inicialización utilizado son 16 bytes 0x00.
public static String q(String str, String str2) {
if (str != null && !"".equals(str)) {
SecretKeySpec secretKeySpec = new SecretKeySpec(str2.getBytes(), "AES");
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(1, secretKeySpec, new IvParameterSpec(new byte[16]));
return new String(Base64.encode(cipher.doFinal(str.getBytes("UTF-8")), 2));
} catch (Exception e2) {
e2.toString();
}
}
return null;
}
Recuperación de la clave utilizando Frida
El método ro.K con el que se obtiene la clave de cifrado realiza diversas operaciones matemáticas ofuscadas por lo que el método óptimo para imprimir la clave por pantalla será poseer la función para ejecutarla en primer lugar y posteriormente imprimir su resultado. Podemos realizar esta opción con este script de Frida.
Java.perform(function () {
var Class = Java.use('ro');
Class.K.implementation = function() {
var key = Class.K.call(this);
console.log('Intercepted KEY -> ' + key)
return key;
};
});
A continuación abrimos la aplicación con Frida, utilizando la opción de depuración -U, indicando el archivo con el script con -l y especificando el nombre del paquete de la aplicación con -f.
frida -U -l frida.js -f com.aplicaciondeprueba
En el momento en el que se acceda al método se imprimirá la clave de 32 caracteres:
Intercepted KEY -> 86qP{ap_70#"3~HiQ#*'E0pm=ws-Z);W
También se puede crear un script que imprima todos los parámetros que entran en la función yc.q antes de ser cifrados.
Java.perform(function () {
var Class = Java.use('yc');
Class.q.implementation = function(string, key) {
var key = Class.q.call(this, string, key);
console.log('Before encryption -> ' + string)
return key;
};
});
Y obtendremos los valores:
Before encryption -> 1
Before encryption -> v1
Before encryption -> android14
Before encryption -> 20240101
Conclusión
El diverso tráfico cifrado en las aplicaciones requerirá un análisis individualizado en cada uno de ellas.