article

Protect Secrets in your app - Android NDK - Part 2

Protect Secrets using Android NDK

Configuration

To configure the project to support native code we require a Android.mk and Application.mk.

Create a file app/src/main/jni/Application.mk and add this to it:

APP_ABI := all

The Application.mk specifies project-wide settings for ndk-build. By default, it is located at jni/Application.mk, in your application's project directory.

Application.mk

Then create a file app/src/main/jni/Android.mk and add:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

# specify the name of the local module
LOCAL_MODULE    := protected

# specify the src files to include in the native lib
LOCAL_SRC_FILES := protected.c

include $(BUILD_SHARED_LIBRARY)

The Android.mk file resides in a subdirectory of your project's jni/ directory, and describes your sources and shared libraries to the build system. It is really a tiny GNU makefile fragment that the build system parses once or more. The Android.mk file is useful for defining project-wide settings that Application.mk, the build system, and your environment variables leave undefined. It can also override project-wide settings for specific modules.

Android.mk

Let's also directly create an empty *.c file with the jni headers, which will specify our native code.

Create it as app/src/main/jni/protected.c and include the header

#include "jni.h"

For the last configuration part, define the path to the Android.mk in the externalNativeBuild block via the ndkBuild. This will connect the native build with the android build, and ensures the native library will be built and included in your app.

android {
    externalNativeBuild {
        ndkBuild {
            path 'src/main/jni/Android.mk'
        }
    }
}

Sync the project and build. It should compile and install without issues.

Sample code for this part

Implement JNI Access

Head over to the CustomApplication.kt and include the native library

init {
      System.loadLibrary("protected")
}

After that specify the external specification for the native functions we want to call.

private external fun getSdkKey(): String

private external fun getSdkSecret(): String

Now we can use those from our init call.

FunSdk.init(getSdkKey(), getSdkSecret())

The last bit is now to specify our native JNI functions, this was a tedious step in the past, but thanks to Android Studio most of it happens automatically.

Open the protected.c class. It only should contain the jni header include.

Start typing JN and Android Studio should propose the auto completion.

Android Studio JNI Autocomplete
Android Studio JNI Autocomplete

Press enter and do the same for the secret.

JNIEXPORT jstring JNICALL
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkKey(JNIEnv *env, jobject thiz) {
    // TODO: implement getSdkKey()
}

JNIEXPORT jstring JNICALL
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkSecret(JNIEnv *env, jobject thiz) {
    // TODO: implement getSdkSecret()
}

Android Studio will define the correct name, parameters and return value according to the specification in the CustomApplication.

Now complete the implementation of the getSdkKey and getSdkSecret functions.

JNIEXPORT jstring JNICALL
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkKey(JNIEnv *env, jobject thiz) {
    return (*env)->NewStringUTF(env, "amazing-key");
}

JNIEXPORT jstring JNICALL
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkSecret(JNIEnv *env, jobject thiz) {
    return (*env)->NewStringUTF(env, "super-secure-secret");
}

Now build and install the app. If you log the values for key and secret, they will contain the correct values.

Check SDK Credential security strength

Building the app in release mode again and looking into the bytecode will reveal that the source is no longer containing the key or secret.

Android Studio APK Analyzer - JNI - Bytecode
Android Studio APK Analyzer - JNI - Bytecode

What it exposes though is the external specification of the native library.

This theoretically allows a potential attacker to rebuild a sample app including the native lib by copying over the *.so file, and defining a class in the same package and class name. And then just call those 2 functions, retrieving the values.

Compared to the original steps to reverse engineer this is significantly more time consuming and complicated though, and can be hardly automated.

As we built a native library the apk now includes a file libprotected.so. So let's have a look if we can extract some details from that one.

readelf

First lets try using a tool called readelf.

readelf -a libprotected.so

It gives some pointers but won't expose the string constants yet

Symbol table '.dynsym' contains 15 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
   7: 000000000000067c    20 FUNC    GLOBAL DEFAULT   10 Java_com_mikepen[...]

So let's try a different approach.

strings

strings libprotected.so

And success, beside all other string constants it includes our key and secret:

...
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkKey
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkSecret
libprotected.so
amazing-key
super-secure-secret

Even though requiring additional effort to extract the key and secret from the *.so file, it's pretty much doable to retrieve the string and secret.

Our sample makes it super obvious to identify key and secret, but also for real keys and secrets, they usually follow a specific pattern. E.g. specific length or characters, making it possible to guess. And with minimal time the limited possibilities are quickly tried through.

Sample code for this part

Feedback

Got thoughts, feedback, improvements, suggestions, or comments?

Let me know @mike_penz

Attribution

Link preview photo by Dan-Cristian Pădureț / Unsplash