Android 11 绕过反射限制

1. 问题出现的背景

腾讯视频在集成我们 replay sdk 的时候发现这么个错误,导致整个 db mock 功能完全失效。

Accessing hidden field Landroid/database/sqlite/SQLiteCursor;
->mDriver:Landroid/database/sqlite/SQLiteCursorDriver; (greylist-max-o, reflection, denied)


java.lang.NoSuchFieldException: No field mDriver in class Landroid/database/sqlite/SQLiteCursor; 
(declaration of 'android.database.sqlite.SQLiteCursor' appears in /system/framework/framework.jar)


我清晰的记得我们引入了一个第三方解决方案,在 9.0 以上已经解决了这个问题,大致的方案是这样的:
if (SDK_INT >= Build.VERSION_CODES.P) {
  try {
    Method forName = Class.class.getDeclaredMethod("forName", String.class);
    Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);


    Class<?> vmRuntimeClass = (Class<?>) forName.invoke(null, "dalvik.system.VMRuntime");
    Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null);
    setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});
    sVmRuntime = getRuntime.invoke(null);
  } catch (Throwable e) {
    Log.e(TAG, "reflect bootstrap failed:", e);
  }
}

吓得我赶紧去看下到底有没有猫腻,发现在 Android 11 上果然有问题:

Accessing hidden method Ldalvik/system/VMRuntime;
->setHiddenApiExemptions([Ljava/lang/String;)V (blacklist,core-platform-api, reflection, denied)


Caused by: java.lang.NoSuchMethodException: dalvik.system.VMRuntime.setHiddenApiExemptions [class [Ljava.lang.String;]
......

2. 分析问题出现的原因

本着时间紧任务重尽量不影响进度的情况下,我还是想去网上搜索看看,但是发现都是一堆旧的方案。迫不得已去看看到底为什么?到底为什么?刚好前几天找同事要了一份 Android 11 的源码。

static jobject Class_getDeclaredMethodInternal(JNIEnv* env, jobject javaThis, jstring name, jobjectArray args) {
  // ……
  Handle<mirror::Method> result = hs.NewHandle(
      mirror::Class::GetDeclaredMethodInternal<kRuntimePointerSize>(
          soa.Self(),
          klass,
          soa.Decode<mirror::String>(name),
          soa.Decode<mirror::ObjectArray<mirror::Class>>(args),
          GetHiddenapiAccessContextFunction(soa.Self())));
  if (result == nullptr || ShouldDenyAccessToMember(result->GetArtMethod(), soa.Self())) {
    return nullptr;
  }
  return soa.AddLocalReference<jobject>(result.Get());
}

如果 ShouldDenyAccessToMember 返回 true,那么就会返回 null,上层就会抛出方法找不到的异常。这里和 Android P 没什么不同,只是把 ShouldBlockAccessToMember 改了个名而已。
ShouldDenyAccessToMember 会调用到 hiddenapi::ShouldDenyAccessToMember,该函数是这样实现的:

template<typename T>
inline bool ShouldDenyAccessToMember(T* member,
                                     const std::function<AccessContext()>& fn_get_access_context,
                                     AccessMethod access_method)
    REQUIRES_SHARED(Locks::mutator_lock_) {


  const uint32_t runtime_flags = GetRuntimeFlags(member);


  // 1:如果该成员是公开API,直接通过
  if ((runtime_flags & kAccPublicApi) != 0) {
    return false;
  }


  // 2:不是公开API(即为隐藏API),获取调用者和被访问成员的 Domain 
  // 主要看这个
  const AccessContext caller_context = fn_get_access_context();
  const AccessContext callee_context(member->GetDeclaringClass());


  // 3:如果调用者是可信的,直接返回
  if (caller_context.CanAlwaysAccess(callee_context)) {
    return false;
  }
  // ......
  }

原来的方案失效了能在 FirstExternalCallerVisitor 的 VisitFrame 方法中找到答案

bool VisitFrame() override REQUIRES_SHARED(Locks::mutator_lock_) {
    ArtMethod *m = GetMethod();
    ......
    ObjPtr<mirror::Class> declaring_class = m->GetDeclaringClass();
    if (declaring_class->IsBootStrapClassLoaded()) {
        ......
        // 如果 PREVENT_META_REFLECTION_BLACKLIST_ACCESS 为 Enabled,跳过来自 java.lang.reflect.* 的访问
        // 系统对“套娃反射”的限制的关键就在此
        ObjPtr<mirror::Class> proxy_class = GetClassRoot<mirror::Proxy>();
        if (declaring_class->IsInSamePackage(proxy_class) && declaring_class != proxy_class) {
            if (Runtime::Current()->isChangeEnabled(kPreventMetaReflectionBlacklistAccess)) {
                return true;
            }
        }
    }


    caller = m;
    return false;
}

3. 解决方案

  • native hook 住 ShouldDenyAccessToMember 方法,直接返回 false

  • 破坏调用堆栈绕过去,使 VM 无法识别调用方

我们采用的是第二种方案,有什么方法可以让 VM 无法识别我的调用栈呢?这可以通过 JniEnv::AttachCurrentThread(…) 函数创建一个新的 Thread 来完成。具体我们可以看下这里 https://developer.android.com/training/articles/perf-jni ,然后配合 std::async(…)  与 std::async::get(..)  就能搞定了,下面是关键代码:

// java 层直接用 jni 调用这个方法
static jobject Java_getDeclaredMethod(
    JNIEnv *env,
    jclass interface,
    jobject clazz,
    jstring method_name,
    jobjectArray params) {
  // ...... 省掉一些转换代码
  //  先用 std::async 调用 getDeclaredMethod_internal 方法
  auto future = std::async(&getDeclaredMethod_internal, global_clazz,
                           global_method_name,
                           global_params);
  auto result = future.get();
  return result;
}


static jobject getDeclaredMethod_internal(
    jobject clazz,
    jstring method_name,
    jobjectArray params) {
  // 这里就是一些普通的 jni 操作了
  JNIEnv *env = attachCurrentThread();
  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);
  detachCurrentThread();
  return env->NewGlobalRef(res);
}


JNIEnv *attachCurrentThread() {
  JNIEnv *env;
  // AttachCurrentThread 核心在这里
  int res = _vm->AttachCurrentThread(&env, nullptr);
  return env;
}


作者:红橙Darren
链接:https://www.jianshu.com/p/6546ce67c8e0

关注我获取更多知识或者投稿

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android 11 中,由于隐私保护机制的加强,对于外部存储的访问权限受到了限制,因此我们不能直接通过反射访问 U 盘路径。不过,我们可以通过 MediaStore API 来获取 U 盘路径,具体实现步骤如下: 1. 首先,我们需要检查 U 盘是否已经挂载。可以通过以下代码实现: ```java File[] externalStorageVolumes = ContextCompat.getExternalFilesDirs(context, null); File primaryStoragePath = externalStorageVolumes[0]; File secondaryStoragePath = externalStorageVolumes.length > 1 ? externalStorageVolumes[1] : null; if (secondaryStoragePath != null && !primaryStoragePath.equals(secondaryStoragePath)) { // U 盘已挂载 String usbPath = secondaryStoragePath.getAbsolutePath(); Log.d(TAG, "USB Path: " + usbPath); } else { // U 盘未挂载 Log.d(TAG, "USB not found!"); } ``` 2. 如果 U 盘已经挂载,我们可以通过 MediaStore API 查询 U 盘路径。可以通过以下代码实现: ```java Cursor cursor = context.getContentResolver().query(MediaStore.Files.getContentUri("external"), new String[]{MediaStore.Files.FileColumns.DATA}, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { String filePath = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)); if (filePath.startsWith(usbPath)) { // 找到了 U 盘路径 Log.d(TAG, "USB Path: " + filePath); break; } } cursor.close(); } ``` 需要注意的是,以上代码仅供参考,具体实现可能会因为不同的 Android 版本、不同的设备等因素而有所差异,具体实现时还需根据实际情况进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值