Introduction️

There are additional protections related to the requirement for a specific TLS certificate (pinning). There is also the possibility of encrypting network communications over the HTTP protocol. In the case of symmetric encryption, the application will obtain the key from the server or obtain it from the application’s own source code, statically (stored in a variable), or dynamically, where several obfuscated methods will be executed to create the key to use.

We will disassemble an application to find out which method generates the key and with Frida we will modify the method to print the key on screen. Once we have the key, we can decipher the traffic generated by the application and intercept it with a proxy.️

HTTP Request Analysis

We obtain the following request from the proxy:

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":""}

As we can see, a text in JSON format is being sent to the /api/systemInitialization/ endpoint encoded with Base64. Upon decoding, binary data is obtained, suggesting that the parameters are encrypted.️

Searching for the source code responsible for the request

In the search for the code that makes the HTTP request we observe that the application uses Retrofit, an HTTP client and found different points in the class com.testapplication.net.retrofit.RetrofitService.️

package com.testapplication.net.retrofit;

...

@POST("/api/systemInitialization/")
Call<SystemInitResponse> initSystem(@Body SystemInitRequest systemInitRequest);

We find the class that will store the request, SystemInitRequest.️

package com.testapplication.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;

And here is the class that will store the response SystemInitResponse.️

package com.testapplication.net.response;

public class SystemInitResponse extends BasicResponse {

  @JsonProperty("param0")
  private String symmetricKey;

  @JsonProperty("param4")
  private String language;

  @JsonProperty("param3")
  private String version;
}

We observe a variable in the response, symmetricKey, which may correspond to the key we are looking for, but in this case, the field param0 in the response is empty, so we assume that the key is obtained locally. Searching for references to SystemInit, we find the method handler c of the class defpackage.cn0 from the request.️

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

We see that the parameters of the request to be sent are established and then queued. The value stored (encoded in Base64) is the value returned by the function yc.q with the first parameter being the string to encrypt and the second parameter being the key to use. In this case, the initialization vector used is 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;
}

Recovering the Key using Frida

The ro.K method, which obtains the encryption key, performs various mathematical operations that are obscured. The optimal way to print the key to the screen will be to have a function to execute it first and then print its result. We can do this with this Frida script.

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

We open the application with Frida, using the debugging option -U, indicating the file with the script with -l and specifying the name of the package of the application with -f.️

frida -U -l frida.js -f com.testapplication

In the moment in which access is made to the method a 32 character key will be printed:

Intercepted KEY -> 86qP{ap_70#"3~HiQ#*'E0pm=ws-Z);W

Also you can create a script that prints all parameters that enter in function yc.q before being encrypted.️

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

We will get the values:️

Before encryption -> 1
Before encryption -> v1
Before encryption -> android14
Before encryption -> 20240101

Conclusion️

The diverse encrypted traffic in applications will require individualized analysis for each one of them.️