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;
}