炒冷飯,再聊聊大家都知曉的隱藏介面的限制解除。
說明
由於我們容器產品的特性,需要將應用完整的執行起來,所以必須涉及一些隱藏介面的反射呼叫,而突破反射限制則成為我們實現的基礎。現將我們的解決方案分享給大家,一起學習。
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.h
class 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
,此時caller
為java.lang.System
其domain
級別為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();
}
和上面一樣,我們可以擴充套件出對應其他常用的函式實現(如getMethod
,getDeclaredField
,getField
等)。只不過我們的容器專案需要相容較久的版本,因此不能使用高版本的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;
}
此方法作為當獲取失敗時,再呼叫此方法補償,由於方案實現為非同步執行緒轉同步,故效率低下,通常只有在我們確定存在但獲取失敗的時候才會使用。