炒冷饭,再聊聊大家都知晓的躲藏接口的约束免除。

说明

由于咱们容器产品的特性,需要将使用完好的运转起来,所以必须涉及一些躲藏接口的反射调用,而打破反射约束则成为咱们实现的基础。现将咱们的解决计划分享给大家,一同学习。

Android 9.0 → 首次启用

这个大家都知道原理了,简略巴拉巴拉下,从下往上溯源。

1、找到API判别规则豁免点。

// source code: art/runtime/hidden_api.cc
template<typename T>
bool ShouldDenyAccessToMemberImpl(T* member, ApiList api_list, AccessMethod access_method) {
 
  // ......// Check for an exemption first. Exempted APIs are treated as SDK.
 if (member_signature.DoesPrefixMatchAny(runtime->GetHiddenApiExemptions())) {
  // Avoid re-examining the exemption list next time.
  // Note this results in no warning for the member, which seems like what one would expect.
  // Exemptions effectively adds new members to the public API list.
  MaybeUpdateAccessFlags(runtime, member, kAccPublicApi);
  return false;
  }
  // ......return deny_access;
}

2、找到成员特点位置。

// source code /art/runtime/runtime.hclass Runtime {
 public:
    // ......
 
  void SetHiddenApiExemptions(const std::vector<std::string>& exemptions) {
   hidden_api_exemptions_ = exemptions;
   }
​
  const std::vector<std::string>& GetHiddenApiExemptions() {
   return hidden_api_exemptions_;
   }
  // ......
};

3、找到设置办法

// source code: /art/runtime/native/dalvik_system_VMRuntime.cc// ......
​
static void VMRuntime_setHiddenApiAccessLogSamplingRate(JNIEnv*, jclass, jint rate) {
 Runtime::Current()->SetHiddenApiEventLogSampleRate(rate);
}
​
// ......
​
static JNINativeMethod gMethods[] = {
    // ......
    NATIVE_METHOD(VMRuntime, setHiddenApiExemptions, "([Ljava/lang/String;)V"),
  // ......
};
​
void register_dalvik_system_VMRuntime(JNIEnv* env) {
    REGISTER_NATIVE_METHODS("dalvik/system/VMRuntime");
}

4、找到上层调用入口。

// source code /libcore/libart/src/main/java/dalvik/system/VMRuntime.java
package dalvik.system;
​
public final class VMRuntime {
  /**
   * Sets the list of exemptions from hidden API access enforcement.
   *
   * @param signaturePrefixes
   *     A list of signature prefixes. Each item in the list is a prefix match on the type
   *     signature of a blacklisted API. All matching APIs are treated as if they were on
   *     the whitelist: access permitted, and no logging..
   *
   * @hide
   */
  @SystemApi(client = MODULE_LIBRARIES)
  @libcore.api.CorePlatformApi(status = libcore.api.CorePlatformApi.Status.STABLE)
  public native void setHiddenApiExemptions(String[] signaturePrefixes);
}

5、形成解决计划。

try {
  Method mm = Class.class.getDeclaredMethod("forName", String.class);
  Class<?> cls = (Class)mm.invoke((Object)null, "dalvik.system.VMRuntime");
  mm = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
  Method m = (Method)mm.invoke(cls, "getRuntime", null);
  Object vr = m.invoke((Object)null);
  m = (Method)mm.invoke(cls, "setHiddenApiExemptions", new Class[]{String[].class});
  String[] args = new String[]{"L"};
  m.invoke(vr, args);
} catch (Throwable e) {
  e.printStackTrace();
}

Android 11.0 → 约束晋级

从此版别开始,体系晋级了上层接口的拜访约束,直接将VMRuntime的类接口约束晋级,因此只能经过native层进行拜访。原理不变,使用体系加载lib库时JNI_OnLoad经过反射调用setHiddenApiExemptions,此刻callerjava.lang.Systemdomain级别为libcore.api.CorePlatformApi,就能够拜访hiddenapi了。

方法1:反射调用

static int setApiBlacklistExemptions(JNIEnv* env) {
  jclass jcls = env->FindClass("dalvik/system/VMRuntime");
  if (env->ExceptionCheck()) {
    env->ExceptionDescribe();
    env->ExceptionClear();
    return -1;
   }
​
  jmethodID jm = env->GetStaticMethodID(jcls, "setHiddenApiExemptions", "([Ljava/lang/String;)V");
  if (env->ExceptionCheck()) {
    env->ExceptionDescribe();
    env->ExceptionClear();
    return -2;
   }
​
  jclass stringCLass = env->FindClass("java/lang/String");
  jstring fakeStr = env->NewStringUTF("L");
  jobjectArray fakeArray = env->NewObjectArray(1, stringCLass, NULL);
  env->SetObjectArrayElement(fakeArray, 0, fakeStr);
  env->CallStaticVoidMethod(jcls, jm, fakeArray);
​
  env->DeleteLocalRef(fakeStr);
  env->DeleteLocalRef(fakeArray);
  return 0;
}
​
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  //......
  JNIEnv * env = NULL;// got env from JavaVM// make sure call here
  setApiBlacklistExemptions(env);
​
  //......
  return 0;
}

方法2:直接函数调用。

将体系的libart.so导出来,在IDA中检查导出的c函数名为:_ZN3artL32VMRuntime_setHiddenApiExemptionsEP7_JNIEnvP7_jclassP13_jobjectArray

void* utils_dlsym_global(const char* libName, const char* funcName) {
  void* funcPtr = NULL;
  void* handle = dlopen(libName, RTLD_LAZY|RTLD_GLOBAL);
  if (__LIKELY(handle)) {
    funcPtr = dlsym(handle, funcName);
   } else {
    LOGE("dlsym: %s, %s, %d, %s", libName, funcName, errno, strerror(errno))
    __ASSERT(0)
   }
  return funcPtr;
}
​
typedef void *(*setHiddenApiExemptions_Func)(JNIEnv* env, jclass, jobjectArray exemptions);
int fixHiddenApi(JNIEnv* env) {
  setHiddenApiExemptions_Func func = (setHiddenApiExemptions_Func)utils_dlsym_global("libart.so", "_ZN3artL32VMRuntime_setHiddenApiExemptionsEP7_JNIEnvP7_jclassP13_jobjectArray");
  __ASSERT(func)
  if (__UNLIKELY(!func)) return -1;
 
  jclass stringCLass = env->FindClass("java/lang/String");
  jstring fakeStr = env->NewStringUTF("L");
  jobjectArray fakeArray = env->NewObjectArray(1, stringCLass, NULL);
  env->SetObjectArrayElement(fakeArray, 0, fakeStr);
  func(env, NULL, fakeArray);
  env->DeleteLocalRef(fakeArray);
  if (env->ExceptionCheck()) {
    LOG_JNI_EXCEPTION(env, true)
    return -2;
   }
  return 0;
}
​

Android 14 & 鸿蒙4 → 反常补丁

一般情况下以上办法均能够到达躲藏接口的拜访免除,但是咱们经过兼容性测验,在鸿蒙和小米的最新版别体系,某些时分依然仍是会出现一下日志:

Accessing hidden method Landroid/app/IUiModeManager$Stub;->asInterface(Landroid/os/IBinder;)Landroid/app/IUiModeManager; (max-target-p, reflection, denied)

而实际上其他的躲藏类是能够正常拜访的,并且在一段时间内该类也是能够拜访的,运转一段时间后就出现此问题。猜想ROM定制了一些缓存机制。所以测验另一种计划:使用VM无法识别调用者的方法损坏调用仓库。这能够经过函数创建的新线程,此刻,咱们处于一个新的VM调用仓库中,没有任何调用历史记录。

#include <future>
​
static jobject reflect_getDeclaredMethod_internal(jobject clazz, jstring method_name, jobjectArray params) {
  bool attach = false;
  JNIEnv *env = jni_get_env(attach);
  if (!env) return;
​
  jclass clazz_class = env->GetObjectClass(clazz);
  jmethodID get_declared_method_id = env->GetMethodID(clazz_class, "getDeclaredMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
  jobject res = env->CallObjectMethod(clazz, get_declared_method_id, method_name, params);
  if (env->ExceptionCheck()) {
    env->ExceptionDescribe();
    env->ExceptionClear();
   }
  jobject global_res = nullptr;
  if (res != nullptr) {
    global_res = env->NewGlobalRef(res);
   }
​
  jni_env_thread_detach();
  return global_res;
}
​
jobject reflect_getDeclaredMethod(JNIEnv *env, jclass interface, jobject clazz, jstring method_name, jobjectArray params) {
  jobject global_clazz = env->NewGlobalRef(clazz);
  jstring global_method_name = (jstring) env->NewGlobalRef(method_name);
  int arg_length = env->GetArrayLength(params);
  jobjectArray global_params = nullptr;
  if (params != nullptr) {
    jobject element;
    for (int i = 0; i < arg_length; i++) {
      element = (jobject) env->GetObjectArrayElement(params, i);
      env->SetObjectArrayElement(params, i, env->NewGlobalRef(element));
     }
    global_params = (jobjectArray) env->NewGlobalRef(params);
   }
​
  auto future = std::async(&reflect_getDeclaredMethod_internal, global_clazz, global_method_name, global_params);
  return future.get();
}

和上面相同,咱们能够扩展出对应其他常用的函数实现(如getMethodgetDeclaredFieldgetField等)。只不过咱们的容器项目需要兼容较久的版别,因此不能使用高版别的std::async特性,为此咱们写了一个pthead的兼容性版别,能够适配低版别的ndk编译。

int ThreadAsyncUtils::threadAsync(BaseThreadAsyncArgument& argument) {
  pthread_t thread;
  int ret = pthread_create(&thread, NULL, threadAsyncInternal, &argument);
  if (0 != ret) {
    LOGE("thread async create error: %d, %s", errno, strerror(errno))
    return ret;
   }
​
  ret = pthread_join(thread, NULL);
  if (0 != ret) {
    LOGE("thread async join error: %d, %s", errno, strerror(errno))
    return ret;
   }
  return 0;
}
​
static void reflect_getDeclaredMethod_internal(BaseThreadAsyncArgument* _args) {
  ReflectThreadAsyncArgument* args = (ReflectThreadAsyncArgument*)_args;
  jobject clazz = args->jcls_clazz;
  jstring method_name = args->jcls_name;
  jobjectArray params = args->jcls_params;
​
  bool attach = false;
  JNIEnv *env = jni_get_env(attach);
  if (!env) return;
​
  jclass clazz_class = env->GetObjectClass(clazz);
  jmethodID get_declared_method_id = env->GetMethodID(clazz_class, "getDeclaredMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
  jobject res = env->CallObjectMethod(clazz, get_declared_method_id, method_name, params);
  if (env->ExceptionCheck()) {
    LOG_JNI_CLEAR_EXCEPTION(env)
   }
  if (res != nullptr) {
    args->jcls_result = env->NewGlobalRef(res);
   }
​
  jni_env_thread_detach();
}
​
jobject ReflectUtils::getDeclaredMethod(JNIEnv *env, jclass interface, jobject clazz, jstring method_name, jobjectArray params) {
  auto global_clazz = env->NewGlobalRef(clazz);
  jstring global_method_name = (jstring) env->NewGlobalRef(method_name);
  int arg_length = env->GetArrayLength(params);
  jobjectArray global_params = nullptr;
  if (params != nullptr) {
    jobject element;
    for (int i = 0; i < arg_length; i++) {
      element = (jobject) env->GetObjectArrayElement(params, i);
      env->SetObjectArrayElement(params, i, env->NewGlobalRef(element));
     }
    global_params = (jobjectArray) env->NewGlobalRef(params);
   }
​
  ReflectThreadAsyncArgument argument(reflect_getDeclaredMethod_internal);
  argument.setMethod(global_clazz, global_method_name, global_params);
  if (0 == ThreadAsyncUtils::threadAsync(argument)) {
    return argument.jcls_result;
   }
  return NULL;
}

此办法作为当获取失利时,再调用此办法补偿,由于计划实现为异步线程转同步,故功率低下,一般只要在咱们确定存在但获取失利的时分才会使用。