/dev/posts/

Disable certificate verification on Android with Frida

Published:

Updated:

Some notes about how to write a Frida script with the (somewhat classic) example of disabling certificate verification for TLS communications on Android applications.

What you could learn here:

Too long; didn't read

You can use objection (android sslpinning disable) if you want to disable TLS certificate verification on Android Java applications.

If you are interested in my code:

Java.perform(function (){
  const ArrayList = Java.use("java.util.ArrayList");

  // See https://developer.android.com/reference/android/net/http/X509TrustManagerExtensions
  const X509TrustManagerExtensions = Java.use("android.net.http.X509TrustManagerExtensions");

  function checkServerTrusted(chain, authType, host) {
    console.log(host);
    const res = ArrayList.$new();
    for (let cert of chain)
      res.add(cert);
    return res;
  }
  X509TrustManagerExtensions.checkServerTrusted.overload(
    "[Ljava.security.cert.X509Certificate;", "java.lang.String", "java.lang.String"
    ).implementation = checkServerTrusted;
  console.log("Ready");
});

Motivation

I wanted to disable certificate verification in an Android application in order to inspect (and possibly modify) its traffic using a meddler-in-the-middle (MITM) proxy such as mitmproxy or Open Web Application Security Project (OWASP) Zed Attack Proxy (ZAP).

Timeline:

  1. I first tried to install a custom certificate authority (CA) in the Android device but it did not work (see Network security configuration for why it did not work);
  2. I found a first Frida script which did not work for this application;
  3. I wrote my Frida script;
  4. then I found another Frida script which actually works;
  5. actually objection can be used for this (see appendix).

Note: nitpicking

These scripts are actually described as disabling certificate pinning. However, they actually disable all TLS certificate verifications (certificate chain, host name, certificate validity, etc.). The application I was trying to MITM was not using certificate pinning.

So there is nothing really new here. There was already some Fridra script out there to do the job. However, this can be used an introduction for writing Frida scripts (especially targeting Java applications on Android).

Trying to MITM the application

In this example, I am going to use an Android virtual machine (VM) using the QEMU/KVM-based emulator included in Android Studio. You can get root access on these VMs with adb root which comes handy when trying to use Frida[1].

The first step is to start a virtual device:

~/Android/Sdk/emulator/emulator -avd API_29 -writable-system -selinux disabled -qemu -enable-kvm

where API_29 is the name I gave to a virtual device I created beforehand in the Android Virtual Device (AVD) manager of Android Studio.

Now we can install the application either through the graphical user interface (GUI) of the virtual device or through the Android debug bridge (ADB):

adb install foo.apk

We are going to use mitmproxy which is a nice Python-based MITM program (see installation instructions). In addition to the mitmproxy commands, it ships with a mitmdump command. The latter is nicer when setting things up because it displays TLS errors:

mitmdump

By default, mitmdump and mitmproxy listen on TCP port 8080 in regular proxy mode: they act as an HTTP proxy which intercepts (decrypts and re-encrypts)[2] TLS communications initiated through it.

[ HTTP ]<->[ HTTP¹   ]<->[ HTTP ]  ¹: mitmproxy logs and can modify HTTP messages on the fly
[ TLS  ]<->[ TLS²    ]<->[ TLS  ]  ²: mitmproxy intercepts TLS communications
[ HTTP ]<->[ HTTP³]                ³: mitmproxy serves as an HTTP proxy (HTTP CONNECT)
[ TCP  ]<->[ TCP     ]<->[ TCP  ]
[ IP   ]<->[ IP      ]<->[ IP   ]
  App.      mitmproxy     Server

Now we need to ask the Android device to use our proxy. Using the emulator included in Android Studio, this can be done in the menu of the emulator:

in the "Settings" sections,
under the "Proxy" tab,
enable "Manual proxy configuration" with 127.0.0.1 and port 8080

When trying to use our application, we now probably find TLS errors[3] in the output of mitmdump such as:

<< Cannot establish TLS with client (sni: foo.example.com): TlsException("SSL handshake error: Error([('SSL routines', 'ssl3_read_bytes', 'sslv3 alert certificate unknown')])")

where foo.example.com is the name of one server the application is trying to communicate with.

This tells us that:

  1. the application tried to use our proxy;
  2. but our attempt to MITM the application was prevented by the certificate verifications done by the client.

In order to fix this, one solution could be to install the CA certificate generated by mitmproxy as a trusted certificate in Android: this should convince the application that the certficicates generated by the MITM proxy are trustworthy.

This approach did not work for the application I was working on. This is because (quoting NCC group, emphasis mine):

if the application targets an SDK higher or equal to 24, only the system certificates are trusted.

If the HTTPS traffic needs to be intercepted, then a proxy certificate must be installed, but it is going to be installed in the ‘user certificates’ container, which is not trusted by default.

The Android documentation explains (emphasis mine):

By default, secure connections (using protocols like TLS and HTTPS) from all apps trust the pre-installed system CAs, and apps targeting Android 6.0 (API level 23) and lower also trust the user-added CA store by default.

In other words, user certificates are not trusted by Android applications targeting API level 24 and higher.

Installing Frida

We are going to use Frida to disable TLS verifications in the target application. Frida is a toolkit for dynamic instrumentation of programs: it can be used to instrument running processes in order to change (or trace) their behavior. It works by injecting a JavaScript runtime (either v8 or QuickJS) in the target process.

The first step is to install the Frida comand-line interface (CLI) tools on our computer:

python3 -m venv ./frida
. ./frida/bin/activate
pip install frida-toools

In addition, we need to install Frida Server on the Android device:

# Get root access
adb root

# See https://github.com/frida/frida/releases,
# choose binary depending on the architecture of the Android device:
wget https://github.com/frida/frida/releases/download/14.0.5/frida-server-14.0.3-android-x86_64.xz
unxz frida-server-14.0.3-android-x86_64.xz
chmod +x frida-server-14.0.3-android-x86_64
adb push frida-server-14.0.3-android-x86_64 /data/local/tmp/frida-server

We need keep the Frida Server running on the Android device while we are using it:

adb shell /data/local/tmp/frida-server

In order to check everything is working correctly this far, we test a simple Frida command:

frida-ps -U

This command lists the processes on the Android device.

The -U flags is used for connection over USB. This is used for interacting with a real Android device over USB or for interacting with an Android virtual device.

Finding which Java method to override

Our goal is to use Frida to override some Java methods in order to disable certificate verification in the application. In order to do that, we need to find which Java methods the application is using to validate the certificates. The checkServerTrusted() method of the javax.net.ssl.X509TrustManager interface in a good candidate. Its signature is:

void checkServerTrusted(X509Certificate[] chain, String authType)
                        throws CertificateException;

We are going to check if this method is actually called by the application using frida-trace:

frida-trace -U -f com.example.foo -j '*!checkServerTrusted'

We get some trace of calls such as:

/* TID 0x2f24 */
11658 ms  X509TrustManagerExtensions.checkServerTrusted(["",""], "RSA", "foo.example.com")
11674 ms     | RootTrustManager.checkServerTrusted(["",""], "RSA", "foo.example.com")
11783 ms     |    | NetworkSecurityTrustManager.checkServerTrusted(["",""], "RSA", "foo.example.com")
11785 ms     |    |    | TrustManagerImpl.checkServerTrusted(["",""], "RSA", "foo.example.com")
12266 ms     |    |    |    | X509TrustManagerExtensions.checkServerTrusted(["",""], "RSA", "foo.example.com")
12267 ms     |    |    |    |    | RootTrustManager.checkServerTrusted(["",""], "RSA", "foo.example.com")
12278 ms     |    |    |    |    |    | NetworkSecurityTrustManager.checkServerTrusted(["",""], "RSA", "foo.example.com")
12281 ms     |    |    |    |    |    |    | TrustManagerImpl.checkServerTrusted(["",""], "RSA", "foo.example.com")

We don't have calls to the method we were expecting but we find calls of method with the same name. We have calls to the checkServerTrusted() methods:

The last parameter of these method calls is the name of the backend server the application is talking to.

By searching the name of the first class, we find that it is android.net.http.X509TrustManagerExtensions. The method we are interested in is:

public List<X509Certificate> checkServerTrusted(X509Certificate[] chain,
                                                String authType,
                                                String host);

The other class is android.security.net.config.RootTrustManager. However, this class is not part of the public API so we are going to focus on X509TrustManagerExtensions instead.

Instrumentation

We have found the methods (checkServerTrusted()) our target application is using to verify the certificates of the servers it is communicating to. Now all we have to do is override these methods in order to disable the certificate verification.

We are going to use Frida to replace the checkServerTrusted() method in X509TrustManagerExtensions. The code is quite straightforward:

Java.perform(function (){
  const ArrayList = Java.use("java.util.ArrayList");

  // See https://developer.android.com/reference/android/net/http/X509TrustManagerExtensions
  const X509TrustManagerExtensions = Java.use("android.net.http.X509TrustManagerExtensions");

  function checkServerTrusted(chain, authType, host) {
    console.log(host);  // (2)
    const res = ArrayList.$new();
    for (let cert of chain)
      res.add(cert);
    return res;
  }
  X509TrustManagerExtensions.checkServerTrusted.overload(
    "[Ljava.security.cert.X509Certificate;", "java.lang.String", "java.lang.String"
    ).implementation = checkServerTrusted; // (1)
  console.log("Ready");
});

In (1), we override the method with our implementation in JavasScript. We need to specify the signature (i.e. argument types) of the method. The "[Ljava.security.cert.X509Certificate;" string designates the java.security.cert.X509Certificate[] type.

In (2), we log the name of the server the application is trying to check. This can be used to check that our code is actually called for the servers we are interested in.

We can now use our trust.js script with:

frida -U -f com.example.foo -l trust.js --no-pause

This asks Frida to spawn the target application and inject our JavaScript code in it. The injected JavaScript script replaces the code of checkServerTrusted() method with our code.

This fixes the Cannot establish TLS with client error for our application and we can now see the requests in mitmdump or mitmproxy.

Discussion

This approach won't work for all applications. Depending on which API and classes are used for certificate verification in the application and depending on the version of the Android framework, it might be necessary to instrument other methods. Other Frida scripts instrument several different classes in order to have a higher compatibility.

For native applications, it might be necessary to hook directly into native code (such as OpenSSL) instead.

References

Frida scripts:

Posts:

Documentation:

Appendix, using objection

objection can be used to disable TLS verifications:

objection --gadget com.example.foo explore
Checking for a newer version of objection...
Using USB device `Android Emulator 5554`
Agent injected and responds ok!

     _   _         _   _
 ___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_|  _|  _| | . |   |
|___|___| |___|___|_| |_|___|_|_|
      |___|(object)inject(ion) v1.9.6

     Runtime Mobile Exploration
        by: @leonjza from @sensepost

[tab] for command suggestions
...com.example.foo on (google: 10) [usb] # android sslpinning disable
(agent) Custom TrustManager ready, overriding SSLContext.init()
(agent) Found com.android.org.conscrypt.TrustManagerImpl, overriding TrustManagerImpl.verifyChain()
(agent) Found com.android.org.conscrypt.TrustManagerImpl, overriding TrustManagerImpl.checkTrustedRecursive()
(agent) Registering job 2793269006910. Type: android-sslpinning-disable
...com.example.foo on (google: 10) [usb] # (agent) [2793269006910] Called (Android 7+) TrustManagerImpl.checkTrustedRecursive(), not throwing an exception.
(agent) [2793269006910] Called (Android 7+) TrustManagerImpl.checkTrustedRecursive(), not throwing an exception.

  1. It is not necessary to get root access in order to use Frida. Another possibility is to repack the target application in order to include a Frida Gadget. I did not try this approach however. ↩︎

  2. TLS protects against this type of attacks. The client is expected to authenticate the server it is talking to. The first time it is launched, mitmproxy creates a CA (i.e. a private/public key pair and a self-signed root certificate for this key pair). We must instruct the application to trust this root CA if we want it to accept to establish TLS sessions with the MITM proxy instead of the legit server. ↩︎

  3. If all the HTTPS traffic is successfully intercepted at this step without error, the targeted application has probably disabled verification of the TLS certificates hosts which is very bad. ↩︎

  4. It is possible to include the method signature (i.e. the types of the arguments) in the search by prepending the /s modifier. For example, -j '*!*X509Certificate*/s'. ↩︎