Bypassing SSL Pinning in Android Apps, Methods and Tools

20 February 2025 12 minutes Author: (Українська) George Droyd

This article takes a detailed look at modern approaches to bypassing SSL Pinning in Android apps. We explain that SSL Pinning provides an additional layer of security by verifying the server certificate, and that root privileges and specialized software are required to analyze such protected traffic.

Introduction

SSL Pinning is a technology widely used in applications to protect confidential data transmitted between a client and a server. An application is any software, regardless of platform – mobile or desktop. Due to the dominance of mobile applications in the market, Android is considered as an example.

Modern network traffic is almost entirely transmitted via the secure TLS protocol, which provides encryption and trust verification between the device and the server. The mechanism is based on checking the server certificate – if it meets the security requirements, the client device establishes a secure connection and transmits encrypted data.

The main question is why the device trusts a particular server certificate and whether a manually created certificate will be accepted.

The answer lies in trust certificates that are pre-built into the operating system and issued by authoritative certification authorities (Certificate Authority, CA). A self-created SSL certificate is not considered trustworthy because its absence from the list of trusted CAs makes it impossible to automatically verify its legitimacy.

The next logical question is: is it possible to create your own CA certificate to bypass SSL pinning? This article is the answer to this question. Therefore, let’s consider the following questions:

  • Why do you need to do this?

  • How does it even work?

  • Unlinking SSL with Frida

  • Implementing your CA

Why do you need to do this?

The article discusses mobile application API security testing and open data parsing in ethical research. Many online services, including online stores, news portals, and banking institutions, use separate APIs for mobile applications, which may differ significantly from the API for the web version.

This API simplifies work because:

  • Data can be submitted in a structured format (JSON).

  • There may be no CAPTCHA.

  • Requests may have a lower rate-limit or other features that should be studied or tested for vulnerabilities.

However, this does not mean that working with the application will be easier. It can use various protection mechanisms, such as:

  • Integrity Check – checks the integrity of the application to detect modifications.

  • Proof of Work – additional computational tasks to slow down attacks.

  • Root Checking – detects root access and blocks work on rooted devices.

  • Native Cryptography – uses encrypted native libraries that need to be decompiled or reverse engineered.

How does it even work?

There is an SSL library OpenSSL, and for Android a modified version of it is used – BoringSSL, which is a fork of OpenSSL and optimized for the specifics of the mobile platform. It is this library that performs all the logic of verifying certificates. It can be found in the process memory as the file libssl.so.

To check this, you must first determine the PID (process ID) of any process that uses the Internet (except for the browser, since it has specific features of working with the network, which is discussed in the “Advanced” section). Then run the command:

cat /proc/<pid>/maps | grep ssl

The result on desktop Linux will look something like this:

7f61cbbb8000-7f61cbbcb000 r--p 00000000 00:21 186853  /usr/lib64/libssl.so.3.2.2
7f61cbbcb000-7f61cbc5d000 r-xp 00013000 00:21 186853  /usr/lib64/libssl.so.3.2.2
7f61cbc5d000-7f61cbc80000 r--p 000a5000 00:21 186853  /usr/lib64/libssl.so.3.2.2
7f61cbc80000-7f61cbc8b000 r--p 000c8000 00:21 186853  /usr/lib64/libssl.so.3.2.2
7f61cbc8b000-7f61cbc8f000 rw-p 000d3000 00:21 186853  /usr/lib64/libssl.so.3.2.2

This means that the library libssl.so was loaded from /usr/lib64/libssl.so.3.2.2.

In the case of Android, the result will be different:

/apex/com.android.conscrypt/lib/libssl.so

This library plays a key role in implementing SSL and certificate validation in Android applications.

However, applications do not access its functions directly. Instead, high-level libraries are used to work with SSL, which are already integrated into the programming environment and simplify interaction with security mechanisms.

In the case of Java, this is:

java.net.ssl

This library provides a convenient API for working with TLS connections, and all low-level calls to libssl.so are performed in the background. Logically, the library should have a function that determines whether an SSL certificate is trusted or not.

Is it possible to change this function so that it always accepts any certificates? Yes, it is possible! This is one method that has both its advantages and disadvantages.

Pros:

  • No need to mess with your own CA certificate.

  • You can quickly unpin one application and see its traffic.

Cons:

  • Does not work with WebView and Flutter.

Therefore, if you decide to take this method, go to the paragraph “Unbinding SSL using Frida”, otherwise go to “Implementing your CA”.

Unlinking SSL using Frida

Frida is a powerful reverse engineering tool that allows you to change the behavior of applications in real time. It is widely used for analyzing and modifying applications because it allows you to make changes to the code without having to recompile it.

With Frida, you can use JavaScript to adjust the logic of the application, which is ideal for disabling SSL Pinning and analyzing encrypted traffic.

The OpenSSL library has functions that are responsible for certificate validation, such as:

SSL_CTX_set_verify та SSL_set_verify.

Let’s focus on SSL_set_verify, as it is responsible for verifying the server certificate. Its signature looks like this:

void SSL_set_verify(SSL *ssl, int mode, SSL_verify_cb verify_callback);
  • The second argument (mode) specifies the certificate verification mode. For example, the value SSL_VERIFY_PEER means that the server certificate must be verified.

  • The third argument (verify_callback) is a pointer to a callback function that will be called after the certificate is verified. This function determines whether the certificate should be trusted.

However, this approach is typically used for native code or desktop applications. In the case of Android, the validation mechanism works differently.

Android and BoringSSL

Android uses the BoringSSL library, which is a fork of OpenSSL adapted to the needs of the mobile platform.

One of the key differences of BoringSSL is the additional function SSL_set_custom_verify, which is used in the Java library conscrypt for certificate verification. It allows developers to define their own rules for validating SSL connections, which can be useful for analyzing and modifying the operation of applications.

This function allows you to specify your own certificate verification logic, which is executed by native code. Below is an example from the conscrypt library, where SSL_set_custom_verify is called:

static void NativeCrypto_SSL_set_verify(JNIEnv* env, jclass, jlong ssl_address,
                                        CONSCRYPT_UNUSED jobject ssl_holder, jint mode) {
    CHECK_ERROR_QUEUE_ON_RETURN;
    SSL* ssl = to_SSL(env, ssl_address, true);
    JNI_TRACE("ssl=%p NativeCrypto_SSL_set_verify mode=%x", ssl, mode);
    if (ssl == nullptr) {
        return;
    }
    SSL_set_custom_verify(ssl, static_cast<int>(mode), cert_verify_callback);
}

Hook SSL_set_custom_verifySSL_set_custom_verify

Now we can intercept the SSL_set_custom_verify function using Frida and replace its logic so that certificates are always considered valid. Here is the JavaScript code:

setTimeout(() => {
    // Спершу знаходимо модуль libssl.so
    const libssl = Process.getModuleByName("libssl.so");

    // Шукаємо функцію SSL_set_custom_verify.
    // getExportByName() повертає вказівник на неї
    const SSL_set_custom_verify_ptr = libssl.getExportByName("SSL_set_custom_verify");
    
    // Створюємо нативну функцію для цієї адреси, щоб мати
    // можливість викликати її
    // https://frida.re/docs/javascript-api/#nativefunction
    // https://android.googlesource.com/platform/external/boringssl/+/refs/heads/master/src/include/openssl/ssl.h#2648
    const SSL_set_custom_verify = new NativeFunction(SSL_set_custom_verify_ptr, 'void', ['pointer', 'int', 'pointer']);
    
    // Нативний callback, який завжди повертає 0
    // Це означає, що сертифікат вважається валідним
    // https://frida.re/docs/javascript-api/#nativecallback
    const alwaysTrue = new NativeCallback(() => {
        return 0;
    }, 'int', ['pointer', 'pointer']);
    
    // Замінюємо оригінальну SSL_set_custom_verify на нашу функцію,
    // яка викликає наш alwaysTrue callback
    Interceptor.replace(SSL_set_custom_verify_ptr, new NativeCallback((ctx, mode, cb) => {
        console.log("[+] SSL_set_custom_verify викликано");
        SSL_set_custom_verify(ctx, 1, alwaysTrue);
    }, 'void', ['pointer', 'int', 'pointer']));
}, 0);

Code explanation:

  1. Finding the function. SSL_set_custom_verifyWe find it in the libssl.so module using getExportByName.

  2. Creating a NativeFunction. We create a NativeFunction for SSL_set_custom_verify so that we can call it manually.

  3. Native callback (alwaysTrue). This callback always returns 0, which means the certificate was successfully verified.

  4. Intercepting and replacing the function. We use Interceptor.replace to replace the original function with our own, which calls SSL_set_custom_verify with our callback.

To run the script, run the command:

frida -U -l назва_вашого_файла.js -f назва.адроїд.пакета

Implementing our CA

A feature of WebView and Flutter is that they do not interact directly with libssl.so, but instead perform CA certificate validation on their own.

For example, while testing WebView, an application was loaded and an attempt was made to open a Google page. As shown in the screenshot, the site did not load, and an error appeared in the mitmproxy terminal indicating that the client did not trust the certificate. This confirms that WebView has its own mechanisms for validating SSL connections, which require alternative workarounds.

In Android, trust certificates are stored in the /system/etc/security/cacerts/ directory in hash.0 format. To intercept SSL traffic in WebView or Flutter, you need to add your own certificate to this directory.

The main problem is that starting with Android 10+, it is impossible to mount /system without disabling dm-verity. However, there is an alternative approach – mount tmpfs in /system/etc/security/cacerts/. This allows you to temporarily change the contents of the directory and work with your own certificates, but only until the next device reboot.

Advantages of this method:

  • Works for all traffic on the device, including WebView and Flutter.

  • No need to interfere with libraries or applications directly.

Cons:

  • There may be noise due to packets we don’t need, as this will affect all traffic on the device.

For this method, it is recommended to use transparent proxies in your application to view traffic. Personally, I use mitmproxy in wireguard mode.

Add your own CA certificate to /system/etc/security/cacerts

Step 1. Preparing the CA certificate

Make sure you have a CA certificate. In our example, it is the certificate from mitmproxy:

➜ ls
mitmproxy-ca-cert.cer
➜ export CA_CERT=mitmproxy-ca-cert.cer

Step 2. Convert the certificate to PEM format

Android uses certificates in PEM format. Let’s convert our .cer file to .pem:

➜ openssl x509 -in $CA_CERT -out certificate.pem
➜ ls
certificate.pem  mitmproxy-ca-cert.cer

Step 3. Calculating the certificate hash

Android stores certificates in the /system/etc/security/cacerts/ directory in the format hash.0. Calculate the hash and rename the file:

➜ mv certificate.pem $(openssl x509 -inform PEM -subject_hash_old -in certificate.pem | head -1).0
➜ ls
c8750f0d.0  mitmproxy-ca-cert.cer

Step 4: Move the certificate to the device

Use adb to copy the certificate to the device’s temporary directory:

➜ adb push c8750f0d.0 /data/local/tmp
c8750f0d.0: 1 file pushed, 0 skipped. 3.4 MB/s (1172 bytes in 0.000s)

Adding a certificate to Android

1. Get root access on your device:

➜ adb shell
generic_x86_arm:/ $ su 
generic_x86_arm:/ # whoami
root

2. Create a temporary directory for the certificates:

mkdir -m 700 /data/local/tmp/certs

3. Copy the existing certificates to a temporary directory:

cp /system/etc/security/cacerts/* /data/local/tmp/certs/

4. Create a tmpfs mount for the cacerts directory:

mount -t tmpfs tmpfs /system/etc/security/cacerts/

5. Return the original certificates to tmpfs:

mv /data/local/tmp/certs/* /system/etc/security/cacerts/

6. Copy your new certificate to cacerts:

mv /data/local/tmp/c8750f0d.0 /system/etc/security/cacerts/

7. Update permissions and SELinux context:

chown root:root /system/etc/security/cacerts/*
chmod 644 /system/etc/security/cacerts/*
chcon u:object_r:system_file:s0 /system/etc/security/cacerts/*

Important! After rebooting the device, all changes will be reset, so the procedure must be repeated again.

After adding the certificate to /system/etc/security/cacerts/, it will be recognized by the system as trusted, which will allow WebView to work correctly with intercepted traffic.

Additionally

Browsers are not amenable to the standard method of disabling SSL Pinning via Frida, as they have their own implementation of the certificate verification logic.

Although WebView is based on Chromium, its implementation can be viewed in the Chromium Repository, which allows you to find potential workarounds.

Some applications may not trust the installed CA certificate, even if it is added to the system. This can be a result of Certificate Transparency, which causes the application to ignore unverified certificates. In this case, the application must be reverse-engineered to discover the verification logic and modify it.

If none of the methods work, but the application uses libssl.so, you can change the approach: disable traffic interception and instead intercept calls to the SSL_read and SSL_write functions, which will allow you to get the decrypted data directly as it is processed.

Conclusion

SSL Pinning is a key security mechanism widely used in modern mobile applications to protect transmitted data. The article discussed several ways to bypass this mechanism for application security testing, including:

  • Intercepting and modifying certificate verification functions with Frida

  • Adding your own CA certificate to work with WebView and Flutter

Each method has its own advantages and limitations, so choosing the best approach depends on the specific scenario.

It is important to remember the ethical aspects of security testing and use this knowledge exclusively to strengthen the security of mobile applications.

Other related articles
Found an error?
If you find an error, take a screenshot and send it to the bot.