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 (...){ }
}
}
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();
}