Android apps using HTTPS/TLS over the network can be decrypted using a specially crafted shim library that hooks the SSL Context to enable keylogging. Such implementation requires using the LD_PRELOAD technique and modifying the Android bootup scripts to load this library during the Zygote startup process.
Pen testers and security analysts often need to capture and decrypt network traffic as part of mobile application security testing. Network flow analysis on a TCP/IP packet capture is a fundamental methodology for examining dynamic application content on a network. Prior to HTTPS adoption, performing network traffic analysis was simple because all web content was transmitted in clear text. Ever-growing cyberattacks around the globe spawned techniques and technologies to maintain the cybersecurity pillars of confidentiality, data Integrity and availability.
The TLS cryptographic protocol provides information security by encrypting application data over network communication endpoints. TLS works by generating secret session keys that enable a client and server to communicate over a secure, encrypted channel for the duration of their session. Various tools and techniques have been developed for PCs to decrypt TLS, but solutions for Android are not as prevalent.
Crypto Library APIs for TLS
The implementation basis of all TLS decryption software tools is nearly the same across the board. They hook into the initial TLS session establishment, typically via the “SSL_new” API, and dynamically call the “SSL_CTX_set_keylog_callback“ API to enable keylogging. Many crypto libraries such as OpenSSL implement this API to enable application debugging for developers. Android uses the BoringSSL crypto library, which is forked from OpenSSL and developed/maintained by Google. BoringSSL also implements the “SSL_CTX_set_keylog_callback” API needed to dump the secret TLS session keys.
The Frida dynamic instrumentation tool can intercept and modify functions during runtime. With Frida, we can hook into an app after the TLS session has been initiated to grab the SSL Context of the session using the “SSL_get_SSL_CTX” API. After we hook into this API, we can call the “SSL_CTX_set_keylog_callback” API to pass it a callback function that will perform the logging.
This is a great solution because it doesn’t require a rooted device and supports Frida-gadget. However, the biggest drawback in this technique is that it does not persist across application restarts or reboots. Therefore, if an app closes, keylogging would no longer be enabled for that app and one would have to redeploy the Frida script for that app over a server that is constantly monitoring every app’s state over USB. Additionally, this method requires starting every app during a preparation phase to hook each app’s SSL Context for keylogging. As such, starting every app may inadvertently alter the behavior of the apps due to the increased interprocess communication (IPC) activity and changed interaction of the app’s state on the system. In addition, it is difficult to scale the solution to 20+ devices concurrently because the devices need to be plugged into the machine.
Native C Library Implementation using LD_PRELOAD
LD_PRELOAD is an environment variable in Linux systems that enables a specified shared library to be loaded, before all other shared libraries using a specifically hooked API, when a dynamically linked program is initialized. LD_PRELOAD can be used to intercept library APIs at runtime and replace them with implementations that are defined in a custom shared library.
While Android is a Linux system, it also incorporates the Android Open Source Project (AOSP) software stack on top of the kernel among other device drivers and security protections. Therefore, the LD_PRELOAD environment variable on Android should be able to hook the instantiation of a TLS session and proxy it to the “SSL_CTX_set_keylog_callback” API. The trick here is being able to set the LD_PRELOAD path at the exact moment in time needed during the Android bootup process so that the custom library loads in memory of all apps.
When Android devices boot up, the general boot sequence is:
Boot Rom → Bootloader → Kernel → Init Process → ART → Zygote → Android Applications and Services.
During the Init Process, the init.rc scripts execute, file systems mount and the Java Virtual Machine on Android known as ART (Android Runtime) launches the Zygote process. Every Android app launched after this point will be forked from the Zygote process. Zygote uses shared memory for ART and all of the Java dependency libraries including the AOSP Java packages and Native Development Kit (NDK) libraries to achieve faster load times and better memory optimization when running Android apps.
AOSP Java System Libraries
AOSP creates a framework of Java libraries for third-party app developers to create Android apps using these Java library packages. These Java libraries facilitate all of the Android NDK implementation of native C libraries, including the BoringSSL crypto libraries. In other words, when a third-party developer uses the “java.net.HttpURLConnection” package to make a HTTPS connection to a web server, they inherently use the native C library in the backend. The BoringSSL library handles all crypto TLS negotiation and encryption because it is mapped by NDK in the Android Java framework libraries.
BoringSSL Shim Library
This is a snippet of a C library that will hook the “SSL_new” API that is provided by BoringSSL. This library will proxy this API to the real “SSL_new” in the BoringSSL library and will also call “SSL_CTX_set_keylog_callback” API to enable keylogging of the session keys. This technique is also known as a “shim library”. This shim library is then compiled using the Android NDK compiler. The LD_PRELOAD environment variable is set to the filepath where the shim library is stored on the device.
The challenge here is that LD_PRELOAD needs to be set before Zygote is initialized because every app is forked from Zygote. Ideally, LD_PRELOAD needs to be set by the process that initializes Zygote, during the Init Process. If LD_PRELOAD is set after Zygote, the Linux system loader will not even attempt to load the shim library because all Java packages are loaded only once during Zygote’s initialization.
This can be bypassed by modifying the zygote.rc script located within the init.rc script. This step requires rooting the device with Magisk and loading it with a specially-crafted custom boot.img. This custom boot.img will need to contain the modifications to the zygote.rc script, which sets the LD_PRELOAD environment variable. Magisk provides a convenient way to add custom init.rc scripts using the overlay.d folder within the ramdisk stored in the boot.img.
Security-Enhanced Linux (SELinux) is built into the Android OS in the same fashion that CentOS and Red Hat Enterprise Linux integrate SELinux by default. SELinux enforces mandatory access control (MAC) for processes, despite process privileges (root/superuser). SELinux has two modes of global operation: enforcing and permissive. Enforcing mode will block and log the processes that violate the SELinux policy configurations. Whereas permissive mode logs processes that would otherwise violate SELinux policy configurations but does not block them. By default, SELinux operates in enforcing mode, on the principle of default denial on the Android OS (Android 5 and higher). As a result, LD_PRELOAD environment variables are blocked.
This is a snippet of a small C library that will hook the “SSL_new” API that is provided by BoringSSL.
This can be bypassed by disabling SELinux on the device. With a rooted device, we can put SELinux into permissive mode using the “setenforce 0” command. However, if we want to have a persistent keylogger that can survive reboots, we can create another bootup script within Magisk’s /data/adb/post-fs-data.d/. This script can run the “setenforce 0” command every time the device is started.
The keylogging shim library was created to store all of the master keys associated with an app, within that app’s application data space. All Android apps, by default, only have permissions to read/write from their app directory space in the filesystem. If we tried to write the master keys to /sdcard or any other filepath outside of the app’s data space, it would fail unless the app had explicit read/write permissions authorized by the user.
Capturing TLS session keys with a Native C library implementation that uses LD_PRELOAD provides an ideal solution because it does not need to load every single app, it persists across app restarts and system reboots, can be set and forgotten, and is scalable. Once the custom boot.img is flashed onto the device, it never has to be plugged into a computer to initiate keylogging. The device is always logging.
Note: This solution will not work on any Android apps that are dynamically linked against a proxy library that statically links against BoringSSL, such as Cronet. This implementation was tested on Google Pixel 6 and Google Pixel 5a devices running Android OS 12 and 12.1.
Learn from NowSecure experts by registering for the NowSecure Tech Talk series. And to save time and boost efficiency when analyzing mobile apps, pen testers and security analysts can streamline the testing process with NowSecure Workstation preconfigured hardware and software.