Introduction to Android Native Code Exploitation
Modern Android applications increasingly rely on native code (C/C++), primarily for performance-critical operations, cross-platform compatibility, or to protect sensitive logic from easy reverse-engineering. While native code offers these benefits, it also introduces a new attack surface for penetration testers. Exploiting vulnerabilities in native libraries, especially on ARM64 architectures, requires a deep understanding of assembly, memory management, and dynamic instrumentation tools. This article will guide you through setting up an Android pentesting lab and using Frida to dynamically analyze and exploit a custom native library compiled for ARM64.
Frida, a dynamic instrumentation toolkit, is an invaluable asset in this domain. It allows security researchers to inject custom scripts into running processes on Android, providing unparalleled control over the application’s runtime. We’ll leverage Frida to hook native functions, inspect and modify arguments, and even alter return values, demonstrating how a vulnerability might be exploited or debugged.
Setting Up Your Android Pentesting Lab
Before diving into exploitation, you need a properly configured environment:
1. Android Device or Emulator
- A rooted Android device or an emulator (e.g., Android Studio’s AVD, Genymotion) running a relatively recent Android version (e.g., Android 8.0+). Root access is crucial for deploying the Frida server.
2. ADB (Android Debug Bridge)
- Ensure ADB is installed and configured on your host machine. Test connectivity:
adb devices
3. Frida Server on Android
- Download the correct Frida server binary for your Android device’s architecture (typically
arm64for modern devices). You can find it on Frida’s GitHub releases page.
# Example for ARM64 server v16.1.1 (adjust version as needed)adb push frida-server-16.1.1-android-arm64 /data/local/tmp/frida-serverchmod 755 /data/local/tmp/frida-serveradb shell "/data/local/tmp/frida-server &"
4. Frida Tools on Host Machine
- Install
frida-toolsvia pip:
pip install frida-tools
5. Android NDK for Native Code Compilation
- If you want to compile your own vulnerable native library, you’ll need the Android NDK. Download it via Android Studio’s SDK Manager or directly from Google.
Understanding ARM64 Native Function Calls
When hooking native functions, especially on ARM64, it’s vital to understand the calling convention:
- Arguments: The first eight integer or pointer arguments (including
JNIEnv*andjobject/jclassfor JNI functions) are passed in registersx0throughx7. Additional arguments are pushed onto the stack. - Return Value: The return value is typically placed in register
x0. - Context Object: Frida’s Interceptor provides a
this.contextobject, allowing direct access to these registers (e.g.,this.context.x0,this.context.x1).
Crafting a Vulnerable Native Library
Let’s create a simple C++ native library with a function we’ll later exploit. This function will simulate a password check where a hardcoded value is compared against user input.
1. native-lib.cpp
#include #include #include #define LOG_TAG "NativeCode"#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)extern "C" JNIEXPORT jboolean JNICALL Java_com_example_nativedemo_MainActivity_checkPassword(JNIEnv* env, jobject /* this */, jstring password){ const char* nativePassword = env->GetStringUTFChars(password, 0); const char* secret = "mySuperSecret"; bool isValid = (strcmp(nativePassword, secret) == 0); LOGD("Password entered: %s, isValid: %s", nativePassword, isValid ? "true" : "false"); env->ReleaseStringUTFChars(password, nativePassword); return isValid;}extern "C" JNIEXPORT jstring JNICALL Java_com_example_nativedemo_MainActivity_getSecretKey(JNIEnv* env, jobject /* this */, jint keyId){ if (keyId == 123) { return env->NewStringUTF("HardcodedSecureKey123!"); } else { return env->NewStringUTF("InvalidKeyID"); }}
2. CMakeLists.txt
To compile this, create a CMakeLists.txt in the same directory:
cmake_minimum_required(VERSION 3.4.1)add_library(native-lib SHARED native-lib.cpp)find_library(log-lib log)target_link_libraries(native-lib ${log-lib})
3. Compile with NDK
You can integrate this into an Android Studio project or compile manually using ndk-build or CMake with NDK toolchains. For demonstration, assuming a typical Android Studio project structure, compile the app. The library will be located in app/build/intermediates/cmake/debug/obj/arm64-v8a/libnative-lib.so.
Identifying and Analyzing the Target Function
First, install the compiled Android app (APK) on your device/emulator. Run the app to ensure the native library is loaded.
1. Find the Application Process
Use frida-ps to list running applications and find your target (e.g., com.example.nativedemo):
frida-ps -Uai
Note the PID of your application.
2. Trace Native Calls (Optional but Useful)
frida-trace can give you an initial idea of functions being called. If you know the library name and function, you can trace it:
frida-trace -U -f com.example.nativedemo -i "*checkPassword*"
This will show when checkPassword is called.
Developing the Frida Hook Script
Now, let’s write a Frida script to hook checkPassword and getSecretKey to bypass the password check and extract the secret key.
1. exploit.js
Java.perform(function () { var MainActivity = Java.use('com.example.nativedemo.MainActivity'); // Get a reference to the native library var libNativeLib = Module.findBaseAddress('libnative-lib.so'); if (libNativeLib) { console.log('[+] libnative-lib.so loaded at: ' + libNativeLib); // Find the exported functions // Option 1: Find by export name var checkPasswordPtr = Module.findExportByName('libnative-lib.so', 'Java_com_example_nativedemo_MainActivity_checkPassword'); var getSecretKeyPtr = Module.findExportByName('libnative-lib.so', 'Java_com_example_nativedemo_MainActivity_getSecretKey'); if (checkPasswordPtr) { console.log('[+] Hooking checkPassword at: ' + checkPasswordPtr); Interceptor.attach(checkPasswordPtr, { onEnter: function (args) { // x0: JNIEnv*, x1: jobject (this), x2: jstring (password) this.env = args[0]; this.jobject = args[1]; this.passwordJstring = args[2]; var password = this.env.getStringUtfChars(this.passwordJstring, null).readCString(); console.log(' [checkPassword] Original password input: "' + password + '"'); // Modify the password argument to always match the secret // This requires creating a new jstring and updating args[2] var newPassword = this.env.newStringUtf("mySuperSecret"); args[2] = newPassword; // Overwrite the input password string console.log(' [checkPassword] Modified password input to: "mySuperSecret"'); }, onLeave: function (retval) { console.log(' [checkPassword] Original return value (boolean): ' + retval.toInt32()); // Force the return value to true (1) retval.replace(1); console.log(' [checkPassword] Modified return value (boolean): ' + retval.toInt32() + ' (True)'); } }); } else { console.log('[-] checkPassword function not found.'); } if (getSecretKeyPtr) { console.log('[+] Hooking getSecretKey at: ' + getSecretKeyPtr); Interceptor.attach(getSecretKeyPtr, { onEnter: function (args) { // x0: JNIEnv*, x1: jobject (this), x2: jint (keyId) this.env = args[0]; this.keyId = args[2].toInt32(); console.log(' [getSecretKey] Original keyId input: ' + this.keyId); // Modify keyId to ensure we get the secret args[2] = ptr(123); // Change the keyId to 123 console.log(' [getSecretKey] Modified keyId input to: ' + args[2].toInt32()); }, onLeave: function (retval) { // The return value is a jstring. We need to convert it to a JavaScript string. var secretKey = this.env.getStringUtfChars(retval, null).readCString(); console.log(' [getSecretKey] Original return value (secret): ' + secretKey); // Optionally, you could modify the return value here too // var newSecret = this.env.newStringUtf("NEW_EXPOSED_SECRET!"); // retval.replace(newSecret); // console.log(' [getSecretKey] Modified return value (secret): ' + this.env.getStringUtfChars(retval, null).readCString()); } }); } else { console.log('[-] getSecretKey function not found.'); } } else { console.log('[-] libnative-lib.so not found in memory.'); }});
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 →