Android App Penetration Testing & Frida Hooks

Intercepting Encrypted Communications: Frida Hooks for Android Native Network Libraries

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction

Intercepting encrypted network traffic from Android applications is a common challenge in mobile penetration testing. While tools like Burp Suite or OWASP ZAP work well for Java/Kotlin-based HTTP(S) traffic, many applications employ native libraries (e.g., C/C++ implementations of OpenSSL, BoringSSL, or custom crypto) for their network communications. These native implementations often bypass system proxy settings and can utilize certificate pinning, making traditional man-in-the-middle (MITM) techniques ineffective. This article explores how to leverage Frida, a dynamic instrumentation toolkit, to hook into these native network functions and intercept encrypted data directly from the application’s memory space.

The Challenge: Native Network Stacks and Proxy Bypass

Modern Android applications, especially those requiring high performance, security, or cross-platform compatibility, frequently link native code. This native code might use low-level socket APIs and implement its own TLS stack, independent of Android’s `HttpsURLConnection` or OkHttp. When an app uses a native library like OpenSSL or BoringSSL, it performs TLS handshake and data encryption/decryption within its own process, often ignoring system-wide proxy configurations. Furthermore, certificate pinning, a security measure where the app verifies the server’s certificate against a pre-bundled one, can prevent even successfully proxied traffic from being decrypted by a proxy’s self-signed certificate.

Why Frida is Indispensable

Frida provides a powerful JavaScript API to inject custom code into running processes. Its ability to dynamically attach to process memory, enumerate modules, find exported functions, and hook arbitrary addresses makes it perfect for bypassing complex security mechanisms. For native network interception, Frida allows us to hook critical functions within the SSL/TLS libraries (e.g., `SSL_read`, `SSL_write` from OpenSSL/BoringSSL) and extract plaintext data before it’s encrypted or after it’s decrypted.

Setting Up Your Environment

Before diving into hooking, ensure your Frida environment is ready:

  1. Rooted Android Device or Emulator: Frida requires root privileges to inject into applications.
  2. Frida Server: Download the appropriate Frida server for your device’s architecture (e.g., `frida-server-16.1.4-android-arm64`) from the Frida GitHub releases. Push it to your device and run it:
    adb push frida-server /data/local/tmp/adb shellsu -c /data/local/tmp/frida-server &

  3. Frida Python Client: Install on your host machine:
    pip install frida-tools

Identifying Target Native Functions

The first step is to identify which native library an application uses and which functions are responsible for sending and receiving data. Common candidates include:

  • OpenSSL/BoringSSL: Look for functions like `SSL_read`, `SSL_write`, `SSL_connect`, `SSL_do_handshake`, `SSL_set_fd`.
  • WolfSSL, mbedTLS, LibreSSL: Similar patterns, though function names will differ.

You can identify these using various reverse engineering techniques:

  • Static Analysis (Strings): Extract strings from the application’s native libraries (typically `.so` files in `lib/arm64-v8a`, `lib/armeabi-v7a`, etc., within the APK).
    adb pull /data/app/<package_name> <output_dir>find <output_dir> -name "*.so" -exec strings {} ; | grep SSL_

  • Dynamic Analysis (Frida): Enumerate loaded modules and their exports:
    frida -U -f com.example.app --no-pause -l -e "Process.enumerateModules().forEach(function(m){ console.log(m.name); });"

    Once you find a suspicious module (e.g., `libssl.so`, `libcrypto.so`, or an app-specific library), you can enumerate its exports:

    frida -U -f com.example.app --no-pause -l -e "Module.findExportByName('libssl.so', 'SSL_read').name)"

  • Disassembly (Ghidra/IDA Pro): Analyze the native library directly. Look for cross-references to network-related syscalls (e.g., `sendto`, `recvfrom`, `connect`) and trace back to see which high-level SSL/TLS functions are calling them.

For this tutorial, we’ll assume the application uses OpenSSL/BoringSSL and we’re targeting `SSL_read` and `SSL_write`.

Frida Hooks for Native Interception

The core idea is to attach an interceptor to `SSL_read` and `SSL_write` to log the plaintext buffers.

Example: Intercepting SSL_read and SSL_write

Create a JavaScript file, e.g., `intercept_ssl.js`:

Java.perform(function() {    var module_name = 'libssl.so'; // Or libboringssl.so, libcrypto.so, etc.    var ssl_read_ptr = Module.findExportByName(module_name, 'SSL_read');    var ssl_write_ptr = Module.findExportByName(module_name, 'SSL_write');    if (ssl_read_ptr) {        console.log('[+] Found SSL_read at ' + ssl_read_ptr);        Interceptor.attach(ssl_read_ptr, {            onEnter: function(args) {                // args[0] is the SSL* context                // args[1] is the buffer                // args[2] is the number of bytes to read                this.buf = args[1];                this.len = args[2].toInt32(); // Cast NativePointer to int            },            onLeave: function(retval) {                var bytesRead = retval.toInt32();                if (bytesRead > 0) {                    var buffer = this.buf.readByteArray(bytesRead);                    console.log('==== SSL_read (Decrypted Inbound) ====');                    console.log(hexdump(buffer, { offset: 0, length: bytesRead, header: true, ansi: false }));                    // Try to decode as string if it looks like text                    try {                        var decoded = new TextDecoder().decode(buffer);                        console.log('Decoded String:');                        console.log(decoded);                    } catch (e) { /* Not text */ }                    console.log('=======================================');                }            }        });    } else {        console.log('[-] SSL_read not found in ' + module_name);    }    if (ssl_write_ptr) {        console.log('[+] Found SSL_write at ' + ssl_write_ptr);        Interceptor.attach(ssl_write_ptr, {            onEnter: function(args) {                // args[0] is the SSL* context                // args[1] is the buffer                // args[2] is the number of bytes to write                this.buf = args[1];                this.len = args[2].toInt32();            },            onLeave: function(retval) {                var bytesWritten = retval.toInt32();                if (bytesWritten > 0) {                    var buffer = this.buf.readByteArray(bytesWritten);                    console.log('==== SSL_write (Plaintext Outbound) ====');                    console.log(hexdump(buffer, { offset: 0, length: bytesWritten, header: true, ansi: false }));                    try {                        var decoded = new TextDecoder().decode(buffer);                        console.log('Decoded String:');                        console.log(decoded);                    } catch (e) { /* Not text */ }                    console.log('========================================');                }            }        });    } else {        console.log('[-] SSL_write not found in ' + module_name);    }});

Running the Script

Execute the Frida script against your target application:

frida -U -f com.example.targetapp -l intercept_ssl.js --no-pause

Now, as the application communicates, you should see hex dumps and potentially decoded strings of the encrypted traffic in your console.

Understanding the Code

  • Java.perform(function() { ... });: Ensures the script runs within the context of the Dalvik/ART VM, necessary for some Frida APIs, though not strictly for native hooking.
  • Module.findExportByName(module_name, 'SSL_read');: This is crucial. It locates the memory address of the exported `SSL_read` function within the specified native library (e.g., `libssl.so`). If the function is not exported or has a different name (e.g., an internal symbol or a custom wrapper), you might need `Module.findBaseAddress(module_name).add(offset)` after determining the offset via static analysis.
  • Interceptor.attach(function_pointer, { onEnter: ..., onLeave: ... });: This is Frida’s core hooking mechanism. It allows you to execute JavaScript code before (`onEnter`) and after (`onLeave`) the target function is called.
  • `args[0]`, `args[1]`, `args[2]`: These represent the arguments passed to the hooked native function. You need to know the function signature to correctly interpret them. For `SSL_read(SSL *s, void *buf, int num)`, `args[0]` is the `SSL*` context, `args[1]` is the buffer pointer (`void *buf`), and `args[2]` is the length (`int num`).
  • `this.buf`, `this.len`: `onEnter` and `onLeave` share the same `this` context, allowing you to store arguments from `onEnter` and use them in `onLeave`.
  • `retval.toInt32()`: `onLeave` receives the function’s return value. For `SSL_read` and `SSL_write`, this is typically the number of bytes read/written.
  • `this.buf.readByteArray(bytesRead)`: This reads the content of the buffer pointed to by `this.buf` for the specified `bytesRead` length, returning a JavaScript `ArrayBuffer`.
  • `hexdump(buffer, { … });` and `TextDecoder().decode(buffer);`: These are utility functions to display the buffer content in a human-readable format. `hexdump` shows raw bytes, while `TextDecoder` attempts to convert them to a string.

Advanced Considerations

  • Different SSL Libraries: The `module_name` might vary (`libssl.so`, `libcrypto.so`, `libboringssl.so`, or even app-specific names). You might need to iterate through common names or perform static analysis to find the correct module.
  • Obfuscated Function Names: Some applications might obfuscate their native symbols. In such cases, `Module.findExportByName` won’t work. You’ll need to rely on static analysis (Ghidra/IDA Pro) to find the function’s address relative to its module base and use `Module.findBaseAddress(‘module_name’).add(offset)`.
  • Filtering Traffic: The output can be very verbose. You might want to add logic within your `onLeave` functions to filter based on content, length, or other criteria.
  • Bypassing Certificate Pinning: While this article focuses on interception, Frida can also be used to bypass certificate pinning by hooking functions like `SSL_CTX_set_verify` or modifying certificate verification logic.

Conclusion

Frida is an incredibly powerful tool for understanding and manipulating Android applications at a deep, native level. By hooking into fundamental network functions like `SSL_read` and `SSL_write` within native SSL/TLS libraries, penetration testers can effectively bypass common challenges like proxy-aware applications and certificate pinning. This technique provides unparalleled visibility into the actual plaintext data exchanged by an application, revealing sensitive information that would otherwise remain hidden.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner