Bypass Reflection restrictions
Since API level 28 Android forbids access to some hidden API functions (see: https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces). Lots of required functions used by the POC are black listed and threw exceptions while trying to access them via the Reflection API.
Bypass prior to API 30
With API level 30 this feature got more hardened by the Android Team so that the Workaround using the Reflection API to call the Reflection API functions for accessing the forbidden functions and fields does not work anymore. However prior to API 30 we still can use this solution.
Bypass since API 30 (UPDATE: THIS WORKS WITH API 31, 32, 33 too)
As seen in the FirstExternalCallerVisitor (https://android.googlesource.com/platform/art/+/refs/heads/master/runtime/native/java_lang_Class.cc) visitor the caller of the reflection call is identified by walk through the call stack and stop when functions were found that don’t match a specific filter (package filters for example). If the visitor stops at a function of the App code the VM assumes the reflection call was done by the App and not by the System. The VM will check if the callee is a restricted API function and will deny the access.
A possible way to bypass this solution is to break the call stack in a way that the VM is not able to identify the caller. This could be done via a new Thread crafted via the JniEnv::AttachCurrentThread(…) function (see: https://developer.android.com/training/articles/perf-jni). The Listing 2 is called by Java code and converts all necessary java objects into global references in order to be able to use them within other JNI environments. After that it creates and starts a new thread by calling the std::async(…) function and waits for the result by calling the std::async::get(..) function. Meanwhile the getDeclaredMethod_internal() function (Listing 1) attaches the new crafted native thread to the VM using the JavaVM::attackCurrentThread() function. At this point we’re inside a new VM callstack without any history of Java calls. Next step is to retrieve the getDeclaredMethod() function and call it with the given arguments. Finally we have to convert the result into a global reference. At this point our Exploit is done.
01: static jobject getDeclaredMethod_internal(
02: jobject clazz,
03: jstring method_name,
04: jobjectArray params) {
05: JNIEnv *env = attachCurrentThread();
06: printClassName(clazz, env);
07: jclass clazz_class = env->GetObjectClass(clazz);
08: jmethodID get_declared_method_id = env->GetMethodID(clazz_class, "getDeclaredMethod",
09: "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
10:
11: jobject res = env->CallObjectMethod(clazz, get_declared_method_id,
12: method_name, params);
13: if (env->ExceptionCheck()) {
14: env->ExceptionDescribe();
15: env->ExceptionClear();
16: }
17: jobject global_res = nullptr;
18: if (res != nullptr) {
19: global_res = env->NewGlobalRef(res);
20: }
21: detachCurrentThread();
22: return global_res;
23: }
Listing 1: getDeclaredMethod_internal(…) is called within a new native thread and attaches to the VM by calling attachCurrentThread(). The \emph{getDeclaredMethod} call happens within a new call stack so the VM cannot identify that the call was made by App code (see implementation at: gleipnir/src/main/cpp/native-lib.cpp:50).
01: static jobject Java_getDeclaredMethod(
02: JNIEnv *env,
03: jclass interface,
04: jobject clazz,
05: jstring method_name,
06: jobjectArray params) {
07: __android_log_print(ANDROID_LOG_DEBUG, "native", "Create globals");
08: auto global_clazz = env->NewGlobalRef(clazz);
09: jstring global_method_name = (jstring) env->NewGlobalRef(method_name);
10: int arg_length = env->GetArrayLength(params);
11: jobjectArray global_params = nullptr;
12: if (params != nullptr) {
13: for (int i = 0; i < arg_length; i++) {
14: jobject element = (jobject) env->GetObjectArrayElement(params, i);
15: jobject global_element = env->NewGlobalRef(element);
16: env->SetObjectArrayElement(params, i, global_element);
17: __android_log_print(ANDROID_LOG_DEBUG, "native", "Element %p", global_element);
18: }
19: global_params = (jobjectArray) env->NewGlobalRef(params);
20: }
21:
22:
23: __android_log_print(ANDROID_LOG_DEBUG, "native", "Start async");
24:
25: auto future = std::async(&getDeclaredMethod_internal, global_clazz,
26: global_method_name,
27: global_params);
28: auto result = future.get();
29: if (env->ExceptionCheck()) {
30: env->ExceptionDescribe();
31: env->ExceptionClear();
32: }
33: return result;
34: }
Listing 2: Java_getDeclaredMethod will be called by App’s java code and creates global references of all JNI objects in order to ship them to another thread. The std::async will return a future we use for block and gather the result of the getDeclaredMethod_internal call.
API Restrictions BYPASS Library
In order to provide this solution as easy as possible I created an OpenSource library that combines all api restriction bypasses into one easy to use library.
Greetings! Very useful advice in this particular article! Its the little changes that will make the biggest changes. Thanks for sharing!