Creating and using JVM instances in Android C/C++ applications

+Ch0pin🕷️
5 min readAug 30, 2023

Considering the reader’s interest in this post, it’s reasonable to assume a certain level of familiarity with JNI and its usage. For those who stumbled upon this content by chance, a brief introduction to the subject is recommended. I invite you to explore the topic further by reading THIS article which provides a foundational understanding.

While the typical perception of JNI involves utilising native code in Java applications, this post explores exactly the oposite. We’re about to dive into crafting a pure native Android app and we will try to use Java features, that normally a native app wouldn’t support.

Why we might want to do such a thing ?

In addition to the advantages related to software development, when trying to test, fuzz, or broadly speaking, reverse-engineer native code that interacts with Java objects, you’ll inevitably encounter a juncture where you need to isolate particular segments of code, and this is what this post is all about.

So, what are we going to do ?

Our task is to call a java method from an apk using a C/C++ Android application. I chose com.whatsapp version 2.23.16.76, from which we are going to call the following method (which can be found in the X.2ts class):

public static X.2ts A01(byte[] bArr)

This method gets as an argument a byte array returned by the native method:

WebpUtils.fetchWebpMetadata(file.getAbsolutePath())

This functionality is natively implemented in the libwhatsapp.so library. Given a file path that points to a webp file, this function returns the file's metadata as a byte array. Subsequently, the A01 method utilises this data to initialize an object of the X.2ts class, encapsulating the metadata information. Finally, we invoke the toString method of the X.2ts class to display the metadata of the webp file as interpreted by WhatsApp.

Our final “deliverable” is an Android binary (lets call it caller) which gets a file path to a webp file from the command line and prints its metadata.

Calling WhatsApps get webp metadata java method

The Invocation API

The first step towards achieving our goal is to create a Java Virtual Machine (JVM) and utilize it within our native application to execute our Java compiled code. The Invocation API enables software vendors to integrate the Java VM into any native application [1]. This API offers a range of functions, including the creation, attachment, detachment, and destruction of the JVM. Among these functions, the JNI_CreateJavaVM stands out as one of the most important. It initializes a Java VM and provides a pointer to the JNI interface pointer (JNIEnv):

JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);

The third parameter contains a set of arguments which are used during the VM’s initialisation. Conveniently, the java native interface provides a structure called JavaVMInitArgs which can be used for this reason:

typedef struct JavaVMInitArgs {
jint version;
jint nOptions;
JavaVMOption *options;
jboolean ignoreUnrecognized;
} JavaVMInitArgs;

typedef struct JavaVMOption {
char *optionString;
void *extraInfo;
} JavaVMOption;

We are going to use theJavaVMOption in order to define the path where our java compiled code relies.

Implementation

While I was writing this post, I came across various implementations of the JVM creation process, including:

  • One used in THIS post by Quarkslab
  • Celeb Fenton’s post: Calling JNI Functions with Java Object Arguments from the Command Line
  • This gist by tewilove

Unfortunately, none of these worked for me (for various reasons in each case), so I decided to use the libnativehelper approach which did the trick for me. Further than that, the code is pretty much similar with the one from Quarkslab and you can find it here.

Our project consists of the following files:

  • caller.c (which corresponds to our native app)
  • jnihelper.c (a library that we are going to use to create the JVM)
  • include/jenv.h (header file for our jnihelper library)
  • lib/libwhatsapp.so (the whatsapp library extracted from the whatsapp apk)

The jnihelper library (jnihelper.c)

Before we proceed with compiling the library as well as the native app that uses it, let’s take a look on a few things. First of all, the method that we are going to use to create JVMs is the following (jnihelper.c):

int initialize_java_environment(JavaCTX *ctx, char **jvm_options, uint8_t jvm_nb_options)

This method returns JNI_OK on success or JNI_ERR otherwise and takes the three following parameters:

  • JavaCTX *ctx, is a pointer to a structure of type JavaCTX which holds context and configuration information related to the Java environment.
  • char **jvm_options, is a pointer to an array of pointers to characters (strings). We will use it to pass an array of Java Virtual Machine (JVM) options or/and configuration settings to the function.
  • uint8_t jvm_nb_options represents an unsigned 8-bit integer which we will use to indicate the number of JVM options provided in the jvm_options array.

After successful invocation of the JNI_CreateJVM the ctx->vm and and ctx->env will point to the JVM and JNIenv respectivelly:

...

jint status = JNI_CreateJVM(&ctx->vm, &ctx->env, &args);

if (status == JNI_ERR){
printf("[!] Can't create java vm/env \n");
return JNI_ERR;
}
printf("[+] Initialization completed successfully.\n \
[+]Java VM pointer: %p\n \
[+]Java env pointer: %p\n",ctx->vm, ctx->env);
....
....

We are going to use the initialize_java_environment from our caller native program, in order to be able to call java methods from our DEX/APK file.

The native caller (caller.c)

Starting with the main, we have the following:

JavaCTX ctx;

int main(int argc, char **argv)
{
int status;
if(argc < 2){
printf("Usage: ./caller webp_file.webp");
return 1;
}
char *jvmoptions = "-Djava.class.path=/data/local/tmp/JNIhelper/base.apk";
if((status = initialize_java_environment(&ctx,&jvmoptions,1)) != 0)
return status;

wrapper(argv[1]);
if(cleanup_java_env(&ctx)!=0)
return -1;
return 0;
}

While the code is pretty much self-explanatory a few points worth to mention are:

  • The jvmoptions points to the whatsapp apk, which we push under the: /data/local/tmp/JNIhelper/base.apk
  • The call to the initialize_java_environment in order to create our JVM and initialise our java context (JavaCTX).
  • The wrapper depicted below:
int wrapper(const char *path){

jclass X_2ts = (*ctx.env)->FindClass(ctx.env, "X/2ts");
if (X_2ts == NULL) {
printf("Can't find class X/2ts\n");
return -1;
}
jmethodID A01 = (*ctx.env)->GetStaticMethodID(ctx.env, X_2ts, "A01", "([B)LX/2ts;");
if (A01 == NULL) {
printf("Can't find method A01\n");
return -1;
}

jobject X_2ts_obj = (*ctx.env)->CallStaticObjectMethod(ctx.env,X_2ts,A01,Java_com_whatsapp_stickers_WebpUtils_fetchWebpMetadata(ctx.env,NULL,(*ctx.env)->NewStringUTF(ctx.env,path)));
if(X_2ts_obj==NULL) {
printf("Can't create X_2ts_obj object\n");
return -1;
}

jmethodID toString = (*ctx.env)->GetMethodID(ctx.env,X_2ts,"toString","()Ljava/lang/String;");
if(toString==NULL){
printf("Can't find toString method id\n");
return -1;
}
jstring describe = (*ctx.env)->CallObjectMethod(ctx.env,X_2ts_obj,toString);
if(describe==NULL){
return -1;
}
const char *descr = (*ctx.env)->GetStringUTFChars(ctx.env, describe, NULL);
if(descr!=NULL)
printf("%s",descr);
return 0;
(*ctx.env)->DeleteLocalRef(ctx.env, X_2ts_obj);
(*ctx.env)->DeleteLocalRef(ctx.env, describe);
return -1;
}

Notice how we use our JVM in order to call the JNI methods, having loaded what we need from the whatsapp apk.

Compile

Assuming that you have download and install the Android NDK, make sure to modify the the build.sh in order to point to your toolchain file:

mkdir build && cd build
cmake -DANDROID_PLATFORM=31 \
-DCMAKE_TOOLCHAIN_FILE=$HOME/Library/Android/sdk/ndk/25.2.9519653/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a ..
make

Running

  1. Push the compiled binaries (build/caller and build/libjenv.so), the whatsapp apk (as base.apk) and the a.webp under /data/local/tmp.
  2. Chmod the caller to +x
  3. Install the whatsapp apk (we need a couple more dependencies to resolve)
  4. Set the LD_LIBRARY_PATH to ./:/data/data/com.whatsapp/files/decompressed/libs.spk.zst/

Run with ./caller a.webpReferences:

Project git directory: https://github.com/Ch0pin/JNIInvocation

References:

  1. https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html
  2. https://blog.quarkslab.com/android-greybox-fuzzing-with-afl-frida-mode.html
  3. https://calebfenton.github.io/2017/04/14/calling_jni_functions_with_java_object_arguments_from_the_command_line/
  4. https://gist.github.com/tewilove/b65b0b15557c770739d6

--

--