Bootstrap

Android黑科技——破解系统隐藏API

1 背景

以下内容摘录自官方文档

针对非 SDK 接口的限制

从 Android 9(API 级别 28)开始,Android 平台对应用能使用的 SDK 接口实施了限制。只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用。这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用发生崩溃的风险,同时为开发者降低紧急发布的风险。如需详细了解有关此限制的决定

方法反射(Method Reflection)

Java 类java.lang.reflect.Method实例是对类方法(Method)的反射。Method类继承自通用抽象父类Executable,其自身是不可变(Immutable)类。

方法描述
Method[] getMethods()返回目标类中所有可访问的公开方法,包括从父类继承的公开方法。
Method[] getDeclaredMethods()返回目标类中所有方法,不包括从父类继承的方法。
Method getMethod(String name, Class... parameterTypes)根据方法名与参数类型取得目标方法对象。
Method getDeclaredMethod(String name, Class... parameterTypes)根据方法名与参数类型取得目标方法对象,不包括从父类继承的方法。

大白话解读:

  • getDeclaredMethod:获取当前类的所有声明的方法,包括public、protected和private修饰的方法。需要注意的是,这些方法一定是在当前类中声明的,从父类中继承的不算,实现接口的方法由于有声明所以包括在内。

  • getMethod:获取当前类和父类的所有public的方法。这里的父类,指的是继承层次中的所有父类。比如说,A继承B,B继承C,那么B和C都属于A的父类。

案例:

整理了一份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记的【点击此处即可】即可免费获取

typescript

代码解读

复制代码

public class Fruit { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }

 

scala

代码解读

复制代码

public class Apple extends Fruit { private int price; public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } }

 

java

代码解读

复制代码

import java.lang.reflect.Constructor; import java.lang.reflect.Method; public class ReflectTest { public static void main(String[] args) throws Exception { //正常调用 Apple apple = new Apple(); apple.setPrice(5); System.out.println("Apple Price:" + apple.getPrice()); //使用反射调用-getMethod Class clz = Class.forName("Apple"); Method setPriceMethod = clz.getMethod("setPrice", int.class); Constructor appleConstructor = clz.getConstructor(); Object appleObj = appleConstructor.newInstance(); setPriceMethod.invoke(appleObj, 14); Method getPriceMethod = clz.getMethod("getPrice"); System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj)); System.out.println("================================"); //静态方法反射调用 Method showMethod = clz.getMethod("show",int.class); showMethod.invoke(null,9); System.out.println("================================"); //获取本类+父类所有方法 Method[] methods = clz.getMethods(); for (Method method : methods) { System.out.println("method:" + method); } System.out.println("================================"); //获取本类所有方法 Method[] declaredMethods = clz.getDeclaredMethods(); for (Method method : declaredMethods) { System.out.println("declaredMethod:" + method); } System.out.println("================================"); //使用反射调用-getDeclaredMethod setPriceMethod = clz.getDeclaredMethod("setPrice", int.class); appleConstructor = clz.getConstructor(); appleObj = appleConstructor.newInstance(); setPriceMethod.invoke(appleObj, 22); getPriceMethod = clz.getDeclaredMethod("getPrice"); System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj)); } }

运行结果:

 

vbnet

代码解读

复制代码

Apple Price:5 Apple Price:14 ================================ show static method called,price:9 ================================ method:public static void Apple.show(int) method:public void Apple.setPrice(int) method:public int Apple.getPrice() method:public java.lang.String Fruit.getName() method:public void Fruit.setName(java.lang.String) method:public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException method:public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException method:public final void java.lang.Object.wait() throws java.lang.InterruptedException method:public boolean java.lang.Object.equals(java.lang.Object) method:public java.lang.String java.lang.Object.toString() method:public native int java.lang.Object.hashCode() method:public final native java.lang.Class java.lang.Object.getClass() method:public final native void java.lang.Object.notify() method:public final native void java.lang.Object.notifyAll() ================================ declaredMethod:public static void Apple.show(int) declaredMethod:public void Apple.setPrice(int) declaredMethod:public int Apple.getPrice() ================================ Apple Price:22

4 源码分析

以访问Ldalvik/system/VMRuntime;->setTargetSdkVersion(I)V为例

 

kotlin

代码解读

复制代码

try { val runtimeClass = Class.forName("dalvik.system.VMRuntime") val nativeLoadMethod = runtimeClass.getDeclaredMethod( "setTargetSdkVersionNative", *arrayOf<Class<*>?>(Int::class.javaPrimitiveType) ) Log.i(TAG, "setTargetSdkVersionNative success,nativeLoadMethod:$nativeLoadMethod") } catch (e: Throwable) { e.printStackTrace() }

然后运行时抛出了异常NoSuchMethodException

 

php

代码解读

复制代码

2024-06-20 07:57:04.716 29837-29837 .ndk.hidden.api com.ygq.ndk.hidden.api W Accessing hidden method Ldalvik/system/VMRuntime;->setTargetSdkVersionNative(I)V (blocked, reflection, denied) 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W java.lang.NoSuchMethodException: dalvik.system.VMRuntime.setTargetSdkVersionNative [int] 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W at java.lang.Class.getMethod(Class.java:2937) 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W at java.lang.Class.getDeclaredMethod(Class.java:2914) 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W at com.ygq.ndk.hiddenapi.MainActivity.onCreate$lambda$0(MainActivity.kt:32) 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W at com.ygq.ndk.hiddenapi.MainActivity.$r8$lambda$bSb4PNoYIVmHUKhD73qlXfirQvk(Unknown Source:0) 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W at com.ygq.ndk.hiddenapi.MainActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:0) 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W at android.view.View.performClick(View.java:7799) 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1218) 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W at android.view.View.performClickInternal(View.java:7776) 2024-06-20 07:57:04.717 29837-29837 System.err com.ygq.ndk.hidden.api W at android.view.View.-$$Nest$mperformClickInternal(Unknown Source:0) 2024-06-20 07:57:04.718 29837-29837 System.err com.ygq.ndk.hidden.api W at android.view.View$PerformClick.run(View.java:31213) 2024-06-20 07:57:04.718 29837-29837 System.err com.ygq.ndk.hidden.api W at android.os.Handler.handleCallback(Handler.java:958) 2024-06-20 07:57:04.718 29837-29837 System.err com.ygq.ndk.hidden.api W at android.os.Handler.dispatchMessage(Handler.java:99) 2024-06-20 07:57:04.718 29837-29837 System.err com.ygq.ndk.hidden.api W at android.os.Looper.loopOnce(Looper.java:224) 2024-06-20 07:57:04.718 29837-29837 System.err com.ygq.ndk.hidden.api W at android.os.Looper.loop(Looper.java:318) 2024-06-20 07:57:04.718 29837-29837 System.err com.ygq.ndk.hidden.api W at android.app.ActivityThread.main(ActivityThread.java:8754) 2024-06-20 07:57:04.718 29837-29837 System.err com.ygq.ndk.hidden.api W at java.lang.reflect.Method.invoke(Native Method) 2024-06-20 07:57:04.718 29837-29837 System.err com.ygq.ndk.hidden.api W at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:561) 2024-06-20 07:57:04.718 29837-29837 System.err com.ygq.ndk.hidden.api W at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)

结论:非 sdk 接口,greylist以及whitelist不受限制,但是blacklist以及greylist-max-x会进行限制

4.1 查找漏洞

从android framework的角度分析非sdk接口限制的原理,找到系统漏洞

java.lang.Class;->getDeclaredMethod函数,源码如下:

 

java

代码解读

复制代码

@CallerSensitive public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { // Android-changed: ART has a different JNI layer. return getMethod(name, parameterTypes, false); }

其内部调用了getMethod函数

 

java

代码解读

复制代码

// BEGIN Android-added: Internal methods to implement getMethod(...). private Method getMethod(String name, Class<?>[] parameterTypes, boolean recursivePublicMethods) throws NoSuchMethodException { if (name == null) { throw new NullPointerException("name == null"); } if (parameterTypes == null) { parameterTypes = EmptyArray.CLASS; } for (Class<?> c : parameterTypes) { if (c == null) { throw new NoSuchMethodException("parameter type is null"); } } Method result = recursivePublicMethods ? getPublicMethodRecursive(name, parameterTypes) : getDeclaredMethodInternal(name, parameterTypes); // Fail if we didn't find the method or it was expected to be public. if (result == null || (recursivePublicMethods && !Modifier.isPublic(result.getAccessFlags()))) { throw new NoSuchMethodException(getName() + "." + name + " " + Arrays.toString(parameterTypes)); } return result; }

第三个参数recursivePublicMethods为false,所以内部实际调用的是getDeclaredMethodInternal

 

java

代码解读

复制代码

@FastNative private native Method getDeclaredMethodInternal(String name, Class<?>[] args);

getDeclaredMethodInternal为native函数,会调用到c++

4.2 源码分析

查看java_lang_Class.cc中getDeclaredMethodInternal源码

 

scss

代码解读

复制代码

//art/runtime/native/java_lang_Class.cc static jobject Class_getDeclaredMethodInternal(JNIEnv* env, jobject javaThis, jstring name, jobjectArray args) { ScopedFastNativeObjectAccess soa(env); StackHandleScope<1> hs(soa.Self()); DCHECK_EQ(Runtime::Current()->GetClassLinker()->GetImagePointerSize(), kRuntimePointerSize); DCHECK(!Runtime::Current()->IsActiveTransaction()); ObjPtr<mirror::Class> klass = DecodeClass(soa, javaThis); if (UNLIKELY(klass->IsObsoleteObject())) { ThrowRuntimeException("Obsolete Object!"); return nullptr; } 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()))); /* 3.hiddenapi访问上下文 */ if (result == nullptr || ShouldDenyAccessToMember(result->GetArtMethod(), soa.Self())) { return nullptr; } return soa.AddLocalReference<jobject>(result.Get()); }

其内部调用了mirror::Class::GetDeclaredMethodInternal

 

C

代码解读

复制代码

//art/runtime/mirror/class.cc template <PointerSize kPointerSize> ObjPtr<Method> Class::GetDeclaredMethodInternal( Thread* self, ObjPtr<Class> klass, ObjPtr<String> name, ObjPtr<ObjectArray<Class>> args, const std::function<hiddenapi::AccessContext()>& fn_get_access_context) { // Covariant return types (or smali) permit the class to define // multiple methods with the same name and parameter types. // Prefer (in decreasing order of importance): // 1) non-hidden method over hidden // 2) virtual methods over direct // 3) non-synthetic methods over synthetic // We never return miranda methods that were synthesized by the runtime. StackHandleScope<3> hs(self); auto h_method_name = hs.NewHandle(name); if (UNLIKELY(h_method_name == nullptr)) { ThrowNullPointerException("name == null"); return nullptr; } auto h_args = hs.NewHandle(args); Handle<Class> h_klass = hs.NewHandle(klass); constexpr hiddenapi::AccessMethod access_method = hiddenapi::AccessMethod::kNone; ArtMethod* result = nullptr; bool result_hidden = false; for (auto& m : h_klass->GetDeclaredVirtualMethods(kPointerSize)) { /* 4.遍历virtual method */ if (m.IsMiranda()) { continue; } ArtMethod* np_method = m.GetInterfaceMethodIfProxy(kPointerSize); if (!np_method->NameEquals(h_method_name.Get())) { /* 5.判断方法名与参数类型 */ continue; } // `ArtMethod::EqualParameters()` may throw when resolving types. if (!np_method->EqualParameters(h_args)) { if (UNLIKELY(self->IsExceptionPending())) { return nullptr; } continue; } bool m_hidden = hiddenapi::ShouldDenyAccessToMember(&m, fn_get_access_context, access_method); /* 6.调用ShouldDenyAccessToMember */ if (!m_hidden && !m.IsSynthetic()) { // Non-hidden, virtual, non-synthetic. Best possible result, exit early. return Method::CreateFromArtMethod<kPointerSize>(self, &m); } else if (IsMethodPreferredOver(result, result_hidden, &m, m_hidden)) { // Remember as potential result. result = &m; result_hidden = m_hidden; } } if ((result != nullptr) && !result_hidden) { // We have not found a non-hidden, virtual, non-synthetic method, but // if we have found a non-hidden, virtual, synthetic method, we cannot // do better than that later. DCHECK(!result->IsDirect()); DCHECK(result->IsSynthetic()); } else { for (auto& m : h_klass->GetDirectMethods(kPointerSize)) { /* 7.遍历direct method */ auto modifiers = m.GetAccessFlags(); if ((modifiers & kAccConstructor) != 0) { continue; } ArtMethod* np_method = m.GetInterfaceMethodIfProxy(kPointerSize); if (!np_method->NameEquals(h_method_name.Get())) { /* 8.判断方法名与参数类型 */ continue; } // `ArtMethod::EqualParameters()` may throw when resolving types. if (!np_method->EqualParameters(h_args)) { if (UNLIKELY(self->IsExceptionPending())) { return nullptr; } continue; } DCHECK(!m.IsMiranda()); // Direct methods cannot be miranda methods. bool m_hidden = hiddenapi::ShouldDenyAccessToMember(&m, fn_get_access_context, access_method); /* 9.调用ShouldDenyAccessToMember */ if (!m_hidden && !m.IsSynthetic()) { // Non-hidden, direct, non-synthetic. Any virtual result could only have been // hidden, therefore this is the best possible match. Exit now. DCHECK((result == nullptr) || result_hidden); return Method::CreateFromArtMethod<kPointerSize>(self, &m); } else if (IsMethodPreferredOver(result, result_hidden, &m, m_hidden)) { // Remember as potential result. result = &m; result_hidden = m_hidden; } } } return result != nullptr ? Method::CreateFromArtMethod<kPointerSize>(self, result) : nullptr; }

4.3 权限判断

接下来看看ShouldDenyAccessToMember函数源码

 

c

代码解读

复制代码

//art/runtime/hidden_api.cc template <typename T> bool ShouldDenyAccessToMember(T* member, const std::function<AccessContext()>& fn_get_access_context, AccessMethod access_method) { DCHECK(member != nullptr); // First check if we have an explicit sdk checker installed that should be used to // verify access. If so, make the decision based on it. // // This is used during off-device AOT compilation which may want to generate verification // metadata only for a specific list of public SDKs. Note that the check here is made // based on descriptor equality and it's aim to further restrict a symbol that would // otherwise be resolved. // // The check only applies to boot classpaths dex files. Runtime* runtime = Runtime::Current(); if (UNLIKELY(runtime->IsAotCompiler())) { if (member->GetDeclaringClass()->IsBootStrapClassLoaded() && runtime->GetClassLinker()->DenyAccessBasedOnPublicSdk(member)) { return true; } } // Get the runtime flags encoded in member's access flags. // Note: this works for proxy methods because they inherit access flags from their // respective interface methods. const uint32_t runtime_flags = GetRuntimeFlags(member); // Exit early if member is public API. This flag is also set for non-boot class // path fields/methods. if ((runtime_flags & kAccPublicApi) != 0) { /* 1.如果方法是public api,则允许访问 */ return false; } // Determine which domain the caller and callee belong to. // This can be *very* expensive. This is why ShouldDenyAccessToMember // should not be called on every individual access. const AccessContext caller_context = fn_get_access_context(); /* 2.获取caller的上下文 */ const AccessContext callee_context(member->GetDeclaringClass()); /* 3.获取所调用方法的上下文 */ // Non-boot classpath callers should have exited early. DCHECK(!callee_context.IsApplicationDomain()); // Check if the caller is always allowed to access members in the callee context. if (caller_context.CanAlwaysAccess(callee_context)) { /* 4.caller是否可以不受约束访问callee */ return false; } // Check if this is platform accessing core platform. We may warn if `member` is // not part of core platform API. switch (caller_context.GetDomain()) { /* 5.根据domain级别区分对待hiddenapi策略 */ case Domain::kApplication: { DCHECK(!callee_context.IsApplicationDomain()); // Exit early if access checks are completely disabled. EnforcementPolicy policy = runtime->GetHiddenApiEnforcementPolicy(); if (policy == EnforcementPolicy::kDisabled) { /* 5.1.如果policy disable,则返回false,即允许访问 */ return false; } // If this is a proxy method, look at the interface method instead. member = detail::GetInterfaceMemberIfProxy(member); // Decode hidden API access flags from the dex file. // This is an O(N) operation scaling with the number of fields/methods // in the class. Only do this on slow path and only do it once. ApiList api_list(detail::GetDexFlags(member)); DCHECK(api_list.IsValid()); // Member is hidden and caller is not exempted. Enter slow path. return detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method); } case Domain::kPlatform: { DCHECK(callee_context.GetDomain() == Domain::kCorePlatform); // Member is part of core platform API. Accessing it is allowed. if ((runtime_flags & kAccCorePlatformApi) != 0) { return false; } // Allow access if access checks are disabled. EnforcementPolicy policy = Runtime::Current()->GetCorePlatformApiEnforcementPolicy(); if (policy == EnforcementPolicy::kDisabled) { return false; } // If this is a proxy method, look at the interface method instead. member = detail::GetInterfaceMemberIfProxy(member); // Access checks are not disabled, report the violation. // This may also add kAccCorePlatformApi to the access flags of `member` // so as to not warn again on next access. return detail::HandleCorePlatformApiViolation(member, caller_context, access_method, policy); } case Domain::kCorePlatform: { LOG(FATAL) << "CorePlatform domain should be allowed to access all domains"; UNREACHABLE(); } } }

fn_get_access_context的源头是在java_lang_Class->getMethodIdInternal传过来的函数指针,即GetHiddenapiAccessContextFunction

 

c

代码解读

复制代码

//art/runtime/native/java_lang_Class.cc static std::function<hiddenapi::AccessContext()> GetHiddenapiAccessContextFunction(Thread* self) { return [=]() REQUIRES_SHARED(Locks::mutator_lock_) { return hiddenapi::GetReflectionCallerAccessContext(self); }; }

 

c

代码解读

复制代码

//art/runtime/hidden_api.h class AccessContext { ... // Returns true if this domain is always allowed to access the domain of `callee`. bool CanAlwaysAccess(const AccessContext& callee) const { return IsDomainMoreTrustedThan(domain_, callee.domain_); } ... }

 

c

代码解读

复制代码

//art/libartbase/base/hiddenapi_domain.h enum class Domain : char { kCorePlatform = 0, kPlatform, kApplication, }; inline bool IsDomainMoreTrustedThan(Domain domainA, Domain domainB) { return static_cast<char>(domainA) <= static_cast<char>(domainB); }

也就是说,如果caller的domain值越小,能访问的hiddenapi范围越广。比如corePlatform能访问所有级别的api,但是application级别不能访问corePlatform以及platform级别的api。

4.4 Android 系统API的隐藏策略

  1. 第三方app肯定会走到这里,根据domain级别区分对待hiddenapi策略
  2. 如果policy disable,则返回false,即允许访问(重点)
  3. 最终会调用detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method)
 

c

代码解读

复制代码

//art/runtime/hidden_api.cc template <typename T> bool ShouldDenyAccessToMemberImpl(T* member, ApiList api_list, AccessMethod access_method) { DCHECK(member != nullptr); Runtime* runtime = Runtime::Current(); CompatFramework& compatFramework = runtime->GetCompatFramework(); EnforcementPolicy hiddenApiPolicy = runtime->GetHiddenApiEnforcementPolicy(); DCHECK(hiddenApiPolicy != EnforcementPolicy::kDisabled) << "Should never enter this function when access checks are completely disabled"; MemberSignature member_signature(member); // 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; } ... }

这里返回了false(重点), 后边的代码省略,因为当前已经找到了两处返回false的地方(返回false表示可以访问)。

4.4.1 第一处返回false关键点

 

c

代码解读

复制代码

//art/runtime/hidden_api.cc // Exit early if access checks are completely disabled. EnforcementPolicy policy = runtime->GetHiddenApiEnforcementPolicy(); if (policy == EnforcementPolicy::kDisabled) { /* 5.1.如果policydisable,则返回false,即允许访问 */ return false; }

也就是说policy策略关闭时,可以自由访问hiddenapi,该方案可使用FreeReflection

但是需要适配不同的系统版本。

开源的方案是通过修改runtime内存实现的, 内存hidden_api_policy_的偏移值可因为系统版本,也可因为厂家定制导致不统一,所以该方案兼容性问题较大。

 

c

代码解读

复制代码

//art/runtime/runtime.h hiddenapi::EnforcementPolicy GetHiddenApiEnforcementPolicy() const { return hidden_api_policy_; } void SetHiddenApiEnforcementPolicy(hiddenapi::EnforcementPolicy policy) { hidden_api_policy_ = policy; }

4.4.2 第二处返回false关键点

runtime->GetHiddenApiExemptions, 顾名思义:获取豁免的hiddenapi签名

 

c

代码解读

复制代码

//art/runtime/hidden_api.cc // 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; }

MemberSignature::DoesPrefixMatchAny函数

 

c

代码解读

复制代码

//art/runtime/hidden_api.cc bool MemberSignature::DoesPrefixMatchAny(const std::vector<std::string>& exemptions) { for (const std::string& exemption : exemptions) { if (DoesPrefixMatch(exemption)) { return true; } } return false; }

该函数会遍历exemptions,只要有一个exemption,匹配当前访问的method->signature前缀,就可返回false。

 

c

代码解读

复制代码

//art/runtime/hidden_api.cc bool MemberSignature::DoesPrefixMatch(const std::string& prefix) const { size_t pos = 0; for (const char* part : GetSignatureParts()) { size_t count = std::min(prefix.length() - pos, strlen(part)); if (prefix.compare(pos, count, part, 0, count) == 0) { pos += count; } else { return false; } } // We have a complete match if all parts match (we exit the loop without // returning) AND we've matched the whole prefix. return pos == prefix.length(); }

4.4.3 接下来看看MemberSignature->GetSignatureParts

 

c

代码解读

复制代码

//art/runtime/hidden_api.cc inline std::vector<const char*> MemberSignature::GetSignatureParts() const { if (type_ == kField) { return {class_name_.c_str(), "->", member_name_.c_str(), ":", type_signature_.c_str()}; } else { DCHECK_EQ(type_, kMethod); return {class_name_.c_str(), "->", member_name_.c_str(), type_signature_.c_str()}; } }

无论MemberSignature的type_为field还是method,其返回值都是以class_name.c_str为前缀

MemberSignature的构造函数:

 

c

代码解读

复制代码

//art/runtime/hidden_api.cc MemberSignature::MemberSignature(const ClassAccessor::Field& field) { const DexFile& dex_file = field.GetDexFile(); const dex::FieldId& field_id = dex_file.GetFieldId(field.GetIndex()); class_name_ = dex_file.GetFieldDeclaringClassDescriptor(field_id); member_name_ = dex_file.GetFieldName(field_id); type_signature_ = dex_file.GetFieldTypeDescriptor(field_id); type_ = kField; } MemberSignature::MemberSignature(const ClassAccessor::Method& method) { const DexFile& dex_file = method.GetDexFile(); const dex::MethodId& method_id = dex_file.GetMethodId(method.GetIndex()); class_name_ = dex_file.GetMethodDeclaringClassDescriptor(method_id); member_name_ = dex_file.GetMethodName(method_id); type_signature_ = dex_file.GetMethodSignature(method_id).ToString(); type_ = kMethod; }

另外,我们知道一个class的签名,形式都是如Ljava/lang/String;这种,肯定是以L开头的。

接下来分析HiddenApiExemptions,从runtime->GetHiddenApiExemptions着手

综上:所以exemption只要是L,就可以达到返回false的目的。

 

c

代码解读

复制代码

//art/runtime/runtime.h void SetHiddenApiExemptions(const std::vector<std::string>& exemptions) { hidden_api_exemptions_ = exemptions; } const std::vector<std::string>& GetHiddenApiExemptions() { return hidden_api_exemptions_; }

搜一下SetHiddenApiExemptions调用的位置

 

c

代码解读

复制代码

//art/runtime/native/dalvik_system_VMRuntime.cc static void VMRuntime_setHiddenApiExemptions(JNIEnv* env, jclass, jobjectArray exemptions) { std::vector<std::string> exemptions_vec; int exemptions_length = env->GetArrayLength(exemptions); for (int i = 0; i < exemptions_length; i++) { jstring exemption = reinterpret_cast<jstring>(env->GetObjectArrayElement(exemptions, i)); const char* raw_exemption = env->GetStringUTFChars(exemption, nullptr); exemptions_vec.push_back(raw_exemption); env->ReleaseStringUTFChars(exemption, raw_exemption); } Runtime::Current()->SetHiddenApiExemptions(exemptions_vec); }

 

c

代码解读

复制代码

static JNINativeMethod gMethods[] = { ... NATIVE_METHOD(VMRuntime, setHiddenApiExemptions, "([Ljava/lang/String;)V"), ... }

VMRuntime_setHiddenApiExemptions是jni方法,其native函数的声明在VMRuntime.java中。

 

c

代码解读

复制代码

//libcore/libart/src/main/java/dalvik/system/VMRuntime.java /** * 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) public native void setHiddenApiExemptions(String[] signaturePrefixes);

也就是说,想要调用setHiddenApiExemptions, 必须fake掉hiddenapi的限制。

之前的关键点中,caller是否可以不受约束访问callee,给了我们一些启示

caller的上下文是这样的获得的,如果该上下文domain的值越小,拥有的hiddenapi访问权限越大。

 

c

代码解读

复制代码

const AccessContext caller_context = fn_get_access_context(); /* 2.获取caller的上下文 */

 

c

代码解读

复制代码

//art/runtime/native/java_lang_Class.cc static std::function<hiddenapi::AccessContext()> GetHiddenapiAccessContextFunction(Thread* self) { return [=]() REQUIRES_SHARED(Locks::mutator_lock_) { return hiddenapi::GetReflectionCallerAccessContext(self); }; }

 

c

代码解读

复制代码

//art/runtime/hidden_api.cc hiddenapi::AccessContext GetReflectionCallerAccessContext(Thread* self) REQUIRES_SHARED(Locks::mutator_lock_) { // Walk the stack and find the first frame not from java.lang.Class, // java.lang.invoke or java.lang.reflect. This is very expensive. // Save this till the last. struct FirstExternalCallerVisitor : public StackVisitor { explicit FirstExternalCallerVisitor(Thread* thread) : StackVisitor(thread, nullptr, StackVisitor::StackWalkKind::kIncludeInlinedFrames), caller(nullptr) {} bool VisitFrame() override REQUIRES_SHARED(Locks::mutator_lock_) { ArtMethod* m = GetMethod(); if (m == nullptr) { // Attached native thread. Assume this is *not* boot class path. caller = nullptr; return false; } else if (m->IsRuntimeMethod()) { // Internal runtime method, continue walking the stack. return true; } ObjPtr<mirror::Class> declaring_class = m->GetDeclaringClass(); if (declaring_class->IsBootStrapClassLoaded()) { if (declaring_class->IsClassClass()) { return true; } // MethodHandles.makeIdentity is doing findStatic to find hidden methods, // where reflection is used. if (m == WellKnownClasses::java_lang_invoke_MethodHandles_makeIdentity) { return false; } // Check classes in the java.lang.invoke package. At the time of writing, the // classes of interest are MethodHandles and MethodHandles.Lookup, but this // is subject to change so conservatively cover the entire package. // NB Static initializers within java.lang.invoke are permitted and do not // need further stack inspection. ObjPtr<mirror::Class> lookup_class = GetClassRoot<mirror::MethodHandlesLookup>(); if ((declaring_class == lookup_class || declaring_class->IsInSamePackage(lookup_class)) && !m->IsClassInitializer()) { return true; } // Check for classes in the java.lang.reflect package, except for java.lang.reflect.Proxy. // java.lang.reflect.Proxy does its own hidden api checks (https://r.android.com/915496), // and walking over this frame would cause a null pointer dereference // (e.g. in 691-hiddenapi-proxy). ObjPtr<mirror::Class> proxy_class = GetClassRoot<mirror::Proxy>(); CompatFramework& compat_framework = Runtime::Current()->GetCompatFramework(); if (declaring_class->IsInSamePackage(proxy_class) && declaring_class != proxy_class) { if (compat_framework.IsChangeEnabled(kPreventMetaReflectionBlocklistAccess)) { return true; } } } caller = m; return false; } ArtMethod* caller; }; FirstExternalCallerVisitor visitor(self); visitor.WalkStack(); // Construct AccessContext from the calling class found on the stack. // If the calling class cannot be determined, e.g. unattached threads, // we conservatively assume the caller is trusted. ObjPtr<mirror::Class> caller = (visitor.caller == nullptr) ? nullptr : visitor.caller->GetDeclaringClass(); return caller.IsNull() ? AccessContext(/* is_trusted= */ true) : AccessContext(caller); }

最后一行caller.IsNull()的时候上下文传入了true

 

c

代码解读

复制代码

// Represents the API domain of a caller/callee. class AccessContext { public: // Initialize to either the fully-trusted or fully-untrusted domain. explicit AccessContext(bool is_trusted) : klass_(nullptr), dex_file_(nullptr), domain_(ComputeDomain(is_trusted)) {} private: static Domain ComputeDomain(bool is_trusted) { return is_trusted ? Domain::kCorePlatform : Domain::kApplication; } }

在此,domain_通过ComputeDomain初始化,当is_trusted为true的时候,确实会获取级别最高的hiddenapi访问权限

再来看看caller不为null时

 

c

代码解读

复制代码

// Represents the API domain of a caller/callee. class AccessContext { public: // Initialize from Class. explicit AccessContext(ObjPtr<mirror::Class> klass) REQUIRES_SHARED(Locks::mutator_lock_) : klass_(klass), dex_file_(GetDexFileFromDexCache(klass->GetDexCache())), domain_(ComputeDomain(klass, dex_file_)) {} private: static Domain ComputeDomain(ObjPtr<mirror::ClassLoader> class_loader, const DexFile* dex_file) { if (dex_file == nullptr) { return ComputeDomain(/* is_trusted= */ class_loader.IsNull()); } return dex_file->GetHiddenapiDomain(); } static Domain ComputeDomain(ObjPtr<mirror::Class> klass, const DexFile* dex_file) REQUIRES_SHARED(Locks::mutator_lock_) { // Check other aspects of the context. Domain domain = ComputeDomain(klass->GetClassLoader(), dex_file); if (domain == Domain::kApplication && klass->ShouldSkipHiddenApiChecks() && Runtime::Current()->IsJavaDebuggableAtInit()) { /* 只有debugable的包才会走if中的逻辑 */ // Class is known, it is marked trusted and we are in debuggable mode. domain = ComputeDomain(/* is_trusted= */ true); } return domain; } }

通过kclass拿到dex_file,然后调用computeDomain计算该dex_file的domain,最终dex_file的domain值是通过GetHiddenapiDomain()获取的

 

c

代码解读

复制代码

//art/libdexfile/dex/dex_file.h hiddenapi::Domain GetHiddenapiDomain() const { return hiddenapi_domain_; } void SetHiddenapiDomain(hiddenapi::Domain value) const { hiddenapi_domain_ = value; }

查看调用位置

 

c

代码解读

复制代码

//art/runtime/hidden_api.cc void InitializeDexFileDomain(const DexFile& dex_file, ObjPtr<mirror::ClassLoader> class_loader) { Domain dex_domain = DetermineDomainFromLocation(dex_file.GetLocation(), class_loader); // Assign the domain unless a more permissive domain has already been assigned. // This may happen when DexFile is initialized as trusted. if (IsDomainMoreTrustedThan(dex_domain, dex_file.GetHiddenapiDomain())) { dex_file.SetHiddenapiDomain(dex_domain); } }

DetermineDomainFromLocation顾名思义:根据dex_file的文件位置,计算出其domain值

 

c

代码解读

复制代码

static Domain DetermineDomainFromLocation(const std::string& dex_location, ObjPtr<mirror::ClassLoader> class_loader) { // If running with APEX, check `path` against known APEX locations. // These checks will be skipped on target buildbots where ANDROID_ART_ROOT // is set to "/system". if (ArtModuleRootDistinctFromAndroidRoot()) { /* 1.只是为了判断相关的dir路径是否存在 */ if (LocationIsOnArtModule(dex_location) /* 2.dex的路径是否是在artModule */ || LocationIsOnConscryptModule(dex_location) /* 3.dex的路径是否是在ConscryptModule */ ||LocationIsOnI18nModule(dex_location)) { /*4.dex的路径是否是在i18nModule */ return Domain::kCorePlatform; } if (LocationIsOnApex(dex_location)) { /*5.dex的路径是否是在apex目录 */ return Domain::kPlatform; } } if (LocationIsOnSystemFramework(dex_location)) { / *6.dex的路径是否是在system / framework目录 */ return Domain::kPlatform; } if (LocationIsOnSystemExtFramework(dex_location)) { / *7.dex的路径是否是在system_ext/framework目录 */ return Domain::kPlatform; } if (class_loader.IsNull()) { if (kIsTargetBuild && !kIsTargetLinux) { // This is unexpected only when running on Android. LOG(WARNING) << "DexFile " << dex_location << " is in boot class path but is not in a known location"; } return Domain::kPlatform; } return Domain::kApplication; }

代码中一共判断了7个文件位置,分别对应不同的domain

这些位置可以在init.environ.rc中查看到:

 

c

代码解读

复制代码

//core/rootdir/init.environ.rc.in # set up the global environment on early-init export ANDROID_BOOTLOGO 1 export ANDROID_ROOT /system export ANDROID_ASSETS /system/app export ANDROID_DATA /data export ANDROID_STORAGE /storage export ANDROID_ART_ROOT /apex/com.android.art export ANDROID_I18N_ROOT /apex/com.android.i18n export ANDROID_TZDATA_ROOT /apex/com.android.tzdata export EXTERNAL_STORAGE /sdcard export ASEC_MOUNTPOINT /mnt/asec %EXPORT_GLOBAL_ASAN_OPTIONS% %EXPORT_GLOBAL_GCOV_OPTIONS% %EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS% %EXPORT_GLOBAL_HWASAN_OPTIONS% %EXPORT_GLOBAL_SCUDO_ALLOCATION_RING_BUFFER_SIZE%

1.只是为了判断相关module的dir是否存在,一般都是存在的

2.artModule路径为/apex/com.android.art(android 14)

3.conscryptModule路径为/apex/com.android.conscrypt

4.apex的路径为/apex/

5.SystemFramework的路径为/system/framework

如果caller的路径为artModule或者conscryptModule即可将domain值置为kCorePlatform,达到目的。

先随便找个debug的app看一下运行时apex路径都有哪些

cat /proc/7156/maps |grep "/apex/.*.jar"

core-oj.jar正是可以利用的点:

这里有一个java.lang.System类,该类我们经常使用其加载so库,比如System.loadLibrary。

所以,我们可以通过System.loadLibrary,然后在native层的JNI_OnLoad中通过反射调用setHiddenApiExemptions(此时caller为java.lang.System.其domain级别为corePlatform),然后就可以随意访问hiddenapi了

5 总结

  1. 系统framework代码中可以通过设置setHiddenApiExemptions,达到随意访问hiddenapi的目的
  2. 由于class VMRuntime被hide,可以在JNI_OnLoad中操作VMRuntime,达到调用setHiddenApiExemptions的目的

初步形成解决方案

系统类伪装

如果调用者是系统类,那么就允许被调用。即如果我们能以系统类的身份去反射,那么就能畅通无阻:

  1. 首先通过反射 API 拿到 getDeclaredMethod 方法。getDeclaredMethod 是 public 的,不存在问题;这个通过反射拿到的方法网上称之为元反射方法
  2. 然后通过刚刚的元反射方法去反射调用 getDeclardMethod。这里我们就实现了以系统身份去反射的目的——反射相关的 API 都是系统类,因此我们的元反射方法也是被系统类加载的方法;所以我们的元反射方法调用的 getDeclardMethod 会被认为是系统调用的,可以反射任意的方法。
  3. 另外系统在检查豁免时是通过方法签名前缀进行匹配的,而 Java 方法签名都是 L 开头的,因此我们可以把直接传个 L 进去,那么所有的隐藏API全部被赦免了!
 

arduino

代码解读

复制代码

//frameworks/base/core/java/com/android/internal/os/ZygoteInit.java /** * Sets the list of classes/methods for the hidden API */ public static void setApiDenylistExemptions(String[] exemptions) { VMRuntime.getRuntime().setHiddenApiExemptions(exemptions); }

 

ini

代码解读

复制代码

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}); //Java class的签名都是以L开头的,所以这里全部进行豁免 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了。

最终解决方案

 

scss

代码解读

复制代码

#include <jni.h> #include <string.h> #include <android/log.h> #define LOG_TAG "ygq_hidden_api" #define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE,LOG_TAG,__VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) /** * frameworks/base/core/java/com/android/internal/os/ZygoteInit.java * * Android 12+ & static method * Lcom/android/internal/os/ZygoteInit;->setApiDenylistExemptions([Ljava/lang/String;)V * <p> * setApiDenylistExemptions(new String[]{"L"}) * * Android 9+ & static method * Lcom/android/internal/os/ZygoteInit;->setApiBlacklistExemptions([Ljava/lang/String;)V * <p> * setApiBlacklistExemptions(new String[]{"L"} * * @param env JNIEnv */ bool setApiDenylistExemptions(JNIEnv *env) { // Android 9.0 + int sdkInt = android_get_device_api_level(); if (sdkInt < __ANDROID_API_P__) { LOGV("setApiDenylistExemptions below Android 9.0, just ignored"); return true; } const char* zygoteInitClass = "com/android/internal/os/ZygoteInit"; jclass clazz = env->FindClass(zygoteInitClass); if (clazz == nullptr) { env->ExceptionClear(); LOGI("setApiDenylistExemptions can't find %s class", *zygoteInitClass); return false; } jmethodID setApiDenylistExemptions; if (sdkInt >= __ANDROID_API_S__) { setApiDenylistExemptions = env->GetStaticMethodID(clazz, "setApiDenylistExemptions", "([Ljava/lang/String;)V"); } else { setApiDenylistExemptions = env->GetStaticMethodID(clazz, "setApiBlacklistExemptions", "([Ljava/lang/String;)V"); } if (setApiDenylistExemptions == nullptr) { env->ExceptionClear(); LOGI("setApiDenylistExemptions can't find %s method", "setApiDenylistExemptions"); return false; } 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(clazz, setApiDenylistExemptions, fakeArray); env->DeleteLocalRef(fakeStr); env->DeleteLocalRef(fakeArray); LOGD("setApiDenylistExemptions success"); return true; } /** * libcore/libart/src/main/java/dalvik/system/VMRuntime.java * * Android 9+ & object method * Ldalvik/system/VMRuntime;->setHiddenApiExemptions([Ljava/lang/String;)V * <p> * setHiddenApiExemptions(new String[]{"L"}) * * @param env JNIEnv */ bool setHiddenApiExemptions(JNIEnv *env) { // Android 9.0 + int sdkInt = android_get_device_api_level(); if (sdkInt < __ANDROID_API_P__) { LOGV("setHiddenApiExemptions below Android 9.0, just ignored"); return true; } const char* vmRuntimeClass = "dalvik/system/VMRuntime"; jclass clazz = env->FindClass(vmRuntimeClass); if (clazz == nullptr) { env->ExceptionClear(); LOGI("setHiddenApiExemptions can't find %s class", *vmRuntimeClass); return false; } jmethodID getRuntime = env->GetStaticMethodID(clazz, "getRuntime", "()Ldalvik/system/VMRuntime;"); if (getRuntime == nullptr) { env->ExceptionClear(); LOGI("setHiddenApiExemptions can't find %s method", "getRuntime"); return false; } jobject vmRuntime = env->CallStaticObjectMethod(clazz, getRuntime); if (vmRuntime == nullptr) { env->ExceptionClear(); LOGI("setHiddenApiExemptions can't get vmRuntime instance"); return false; } jmethodID setHiddenApiExemptions = env->GetMethodID(clazz, "setHiddenApiExemptions", "([Ljava/lang/String;)V"); if (setHiddenApiExemptions == nullptr) { env->ExceptionClear(); LOGI("setHiddenApiExemptions can't find %s method", "setHiddenApiExemptions"); return false; } 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->CallVoidMethod(vmRuntime, setHiddenApiExemptions, fakeArray); env->DeleteLocalRef(fakeStr); env->DeleteLocalRef(fakeArray); LOGD("setHiddenApiExemptions success"); return true; } bool checkHiddenApiExemptions(JNIEnv *env) { if (!setHiddenApiExemptions(env)){ return setApiDenylistExemptions(env); } return true; } extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = nullptr; if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // 设置 hidden-api 访问豁免 if(!checkHiddenApiExemptions(env)){ LOGE("checkHiddenApiExemptions error"); return JNI_ERR; } return JNI_VERSION_1_6; }

;