androidc++reflectionjava-native-interfacejnienv

How to get fields Class.getDeclaredFields() in JNI and c++


I want to protect some part of the android app. The code protection is working fine with java reflections. But I can't compete this with JNI and c++.

Please help. How to get java.lang.Class.getDeclaredFields() in JNI?

lib.so file created with android-ndk-r13b/ndk-build. I receive next error when the app and lib.so is launched: Error: JNI DETECTED ERROR IN APPLICATION: can't call java.lang.reflect.Field[] java.lang.Class.getDeclaredFields() on instance of com.metadata.MetaData$1

Each java class has method public native Field[] getDeclaredFields(); Is this my bug or perhaps restrictions in Android? https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces

Example class:

package com.metadata;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;

class MetaData implements Parcelable {
   public String data;

   private MetaData(Parcel in) {
      data = in.readString();
   }

   @Override
   public int describeContents() {
      return 0;
   }

   @Override
   public void writeToParcel(@NonNull Parcel dest, int flags) {
      dest.writeString(data);
   }

   public static final Creator<MetaData> CREATOR = new Creator<MetaData>() {
      @Override
      public MetaData createFromParcel(Parcel in) {
         return new MetaData(in);
      }

      @Override
      public MetaData[] newArray(int size) {
         return new MetaData[size];
      }
   };
}

Attack the class and modify it's CREATOR object with ability to change the data:

public static void modify(String newData) {
        MetaData.Creator<MetaData> original = MetaData.CREATOR;
        MetaData.Creator<MetaData> fake = new Parcelable.Creator<MetaData>() {
            @Override
            public MetaData createFromParcel(Parcel source) {
                MetaData metaData = original.createFromParcel(source);
                metaData.data = newData;
                return metaData;
            }

            @Override
            public MetaData[] newArray(int size) {
                return original.newArray(size);
            }
        };

        try {
            Field field = MetaData.class.getDeclaredField("CREATOR");
            field.setAccessible(true);
            field.set(null, fake);
        } catch (Throwable e) {
            // ignore
        }
    }

Protect the class by rolling back previous field CREATOR:

public static void rollbackModification() {
        try {
            MetaData.Creator<MetaData> metaDataCreator = MetaData.CREATOR;
            Class<?> metaDataCreatorClass = metaDataCreator.getClass();
            Field[] fields = metaDataCreatorClass.getDeclaredFields();
            if (fields == null || fields.length == 0) return;
            Object originalCreator = null;
            for (Field field : fields) {
                // Search for original CREATOR
                Class clazz = field.getType();
                if (clazz == Parcelable.Creator.class) {
                    field.setAccessible(true);
                    originalCreator = field.get(metaDataCreator);
                    break;
                }
            }
            if (originalCreator != null) {
                Field field = MetaData.class.getDeclaredField("CREATOR");
                field.setAccessible(true);;
                field.set(null, originalCreator);
            }
        } catch (Throwable e) {
            // ignore
        }
    }

File metadata-jni.cpp with implemented method:

#include <string.h>
#include <jni.h>

#include <android/log.h>

// Android log function wrappers
static const char* kTAG = "native_log-jniCallback";
#define LOGI(...) \
  ((void)__android_log_print(ANDROID_LOG_INFO, kTAG, __VA_ARGS__))
#define LOGW(...) \
  ((void)__android_log_print(ANDROID_LOG_WARN, kTAG, __VA_ARGS__))
#define LOGE(...) \
  ((void)__android_log_print(ANDROID_LOG_ERROR, kTAG, __VA_ARGS__))

extern "C" {

Java_com_metadata_Test_test(JNIEnv *env, jobject obj, jobject cnt) {
    rollbackModification(env, obj, cnt);
}

rollbackModification(JNIEnv *env, jobject obj, jobject cnt) {
    try {
 
    jclass metaDataClass = env->FindClass("com/metadata/MetaData");
    jclass parcelableCreatorClass = env->FindClass("android/os/Parcelable$Creator");

    // Get MetaData.CREATOR
    jfieldID fakeCreatorField = env->GetStaticFieldID(metaDataClass, "CREATOR", "Landroid/os/Parcelable$Creator;");
    jobject metaDataCreator = env->GetStaticObjectField(metaDataClass, fakeCreatorField);

    // Check MetaData.CREATOR for additional fields.
    if (metaDataCreator != NULL) {

    jclass fakeMetaDataCreatorClass = env->GetObjectClass(metaDataCreator);

    jmethodID fakeMetaDataCreatorClassMethod = env->GetMethodID(fakeMetaDataCreatorClass, "getClass", "()Ljava/lang/Class;");
    jobject fakeMetaDataCreatorClassObject = env->CallObjectMethod(fakeMetaDataCreatorClass, fakeMetaDataCreatorClassMethod);
    jclass fakeMetaDataCreatorClassObjectClass = env->GetObjectClass(fakeMetaDataCreatorClassObject);

    // Try to get Fields like in java.
    // Error: JNI DETECTED ERROR IN APPLICATION: can't call java.lang.reflect.Field[] java.lang.Class.getDeclaredFields() on instance of com.metadata.MetaData$1
    jmethodID declaredFieldsMethod = env->GetMethodID(fakeMetaDataCreatorClassObjectClass, "getDeclaredFields", "()[Ljava/lang/reflect/Field;");
    jobjectArray fields = (jobjectArray)env->CallObjectMethod(metaDataCreator, declaredFieldsMethod);

    jsize numFields = env->GetArrayLength(fields);
    jobject originalCreator = NULL;

    // Search for original field
    for (jsize i = 0; i < numFields; i++) {
            jobject field = env->GetObjectArrayElement(fields, i);
            jclass fieldClass = env->GetObjectClass(field);
            jmethodID getTypeMethod = env->GetMethodID(fieldClass, "getType", "()Ljava/lang/Class;");
            jobject fieldType = env->CallObjectMethod(field, getTypeMethod);
            // Find original value of metaData.CREATOR
            if (env->IsSameObject(fieldType, parcelableCreatorClass)) {
                jmethodID setAccessibleMethod = env->GetMethodID(fieldClass, "setAccessible", "(Z)V");
                env->CallVoidMethod(field, setAccessibleMethod, JNI_TRUE);

                jmethodID getMethod = env->GetMethodID(fieldClass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
                originalCreator = env->CallObjectMethod(field, getMethod, metaDataCreator);
                break;
            }
        }
        // Replace metaData.CREATOR from fake to original
        if (originalCreator != NULL) {
            jmethodID setMethod = env->GetMethodID(fakeMetaDataCreatorClass, "set", "(Ljava/lang/Object;Ljava/lang/Object;)V");
            env->CallVoidMethod(metaDataCreator, setMethod, NULL, originalCreator);
        }
    }

   } catch (...){ }
}
}

Solution

  • You should find the method id using the class and then call the method on the instance. So instead of:

    jmethodID declaredFieldsMethod = env->GetMethodID(fakeMetaDataCreatorClassObjectClass, "getDeclaredFields", "()[Ljava/lang/reflect/Field;");
    jobjectArray fields = (jobjectArray)env->CallObjectMethod(metaDataCreator, declaredFieldsMethod);
    

    Do:

    jclass classClass = env->FindClass("java/lang/Class");
    jmethodID declaredFieldsMethod = env->GetMethodID(classClass, "getDeclaredFields", "()[Ljava/lang/reflect/Field;");
    jobjectArray fields = (jobjectArray)env->CallObjectMethod(fakeMetaDataCreatorClassObject, declaredFieldsMethod);
    

    So, you get the method id from the class where the method is actually declared : Class.

    Also, in JNI you should check for any exceptions after JNI calls using:

    if (env->ExceptionCheck()) {
        env->ExceptionDescribe();
        env->ExceptionClear();
    }