2018年发布的Android 9中引入了对隐藏API的限制 ,这对整个Android生态来说当然是一件好事,但也严重限制了以往我们通过反射等手段实现的“黑科技”(如插件化等),所以开发者们纷纷寻找手段绕过这个限制,比如我曾经提出了两个绕过方法,其中一个便是几乎完美的双重反射(即“元反射”,现在来看叫“套娃反射”比较好) ;而在即将发布的Android R中把这个方法封杀了(谷歌:禁止套娃!),因此我重新研究了Android R中的限制策略。
2021/4/10 更新: 本文存在纰漏,请看一个通用的纯 Java 安卓隐藏 API 限制绕过方案
上有政策 常言道,知己知彼,百战百胜。要想破解这个限制,就必须去搞懂系统是怎么施加的限制;ok,废话不多说,let’s go! 以我们在Java层通过反射获取一个Method为例,Class.getMethod/getDeclaredMethod
最终都会进入一个native方法getDeclaredMethodInternal
,这个方法的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
,该函数是这样实现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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); if ((runtime_flags & kAccPublicApi) != 0 ) { return false ; } const AccessContext caller_context = fn_get_access_context(); const AccessContext callee_context (member->GetDeclaringClass()) ; if (caller_context.CanAlwaysAccess(callee_context)) { return false ; } switch (caller_context.GetDomain()) { case Domain::kApplication: { DCHECK(!callee_context.IsApplicationDomain()); EnforcementPolicy policy = Runtime::Current()->GetHiddenApiEnforcementPolicy(); if (policy == EnforcementPolicy::kDisabled) { return false ; } return detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method); } }
这个函数还是比较长的,我们一步步分析。
判断目标成员是否是公开API,如果是那么直接通过,具体可以看见是通过GetRuntimeFlags
获取,这个flags其实是储存在该成员的access_flags_
中(其实这里不太完全,暂且这么认为吧) 获取调用者的Domain,判断是否可信,如果可信直接通过 以上条件都不满足,根据Domain走不同的实现,我们应用的代码对应的Domain是kApplication,主要看第一个case就行。 第二步中获取调用者的函数中核心部分如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 bool VisitFrame () override REQUIRES_SHARED (Locks::mutator_lock_) { ArtMethod *m = GetMethod(); if (m == nullptr ) { caller = nullptr ; return false ; } else if (m->IsRuntimeMethod()) { return true ; } ObjPtr<mirror::Class> declaring_class = m->GetDeclaringClass(); if (declaring_class->IsBootStrapClassLoaded()) { if (declaring_class->IsClassClass()) { return true ; } ObjPtr<mirror::Class> lookup_class = GetClassRoot<mirror::MethodHandlesLookup>(); if ((declaring_class == lookup_class || declaring_class->IsInSamePackage(lookup_class)) && !m->IsClassInitializer()) { return true ; } 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 ; }
根据caller选择Domain:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static Domain ComputeDomain (ObjPtr<mirror::ClassLoader> class_loader, const DexFile* dex_file) { if (dex_file == nullptr ) { return ComputeDomain( class_loader.IsNull()); } return dex_file->GetHiddenapiDomain(); } static Domain ComputeDomain (ObjPtr<mirror::Class> klass, const DexFile* dex_file) REQUIRES_SHARED (Locks::mutator_lock_) { Domain domain = ComputeDomain(klass->GetClassLoader(), dex_file); if (domain == Domain::kApplication && klass->ShouldSkipHiddenApiChecks() && Runtime::Current()->IsJavaDebuggable()) { domain = ComputeDomain( true ); } return domain; }
dex_file的Domain在第一次加载Class时被初始化(注:Android 8时就已经不允许一个DexFile同时加载多个ClassLoader了):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 static Domain DetermineDomainFromLocation (const std ::string & dex_location, ObjPtr<mirror::ClassLoader> class_loader) { if (ArtModuleRootDistinctFromAndroidRoot()) { if (LocationIsOnArtModule(dex_location.c_str()) || LocationIsOnConscryptModule(dex_location.c_str()) || LocationIsOnI18nModule(dex_location.c_str())) { return Domain::kCorePlatform; } if (LocationIsOnApex(dex_location.c_str())) { return Domain::kPlatform; } } if (LocationIsOnSystemFramework(dex_location.c_str())) { return Domain::kPlatform; } if (LocationIsOnSystemExtFramework(dex_location.c_str())) { return Domain::kPlatform; } if (class_loader.IsNull()) { LOG(WARNING) << "DexFile " << dex_location << " is in boot class path but is not in a known location" ; return Domain::kPlatform; } return Domain::kApplication; }
值得注意的是Android Q中细分出了三个Domain:kCorePlatform、kPlatform、kApplication,同时对kPlatform访问kCorePlatform的情况也做出了一定限制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 case Domain::kPlatform: { DCHECK(callee_context.GetDomain() == Domain::kCorePlatform); if ((runtime_flags & kAccCorePlatformApi) != 0 ) { return false ; } EnforcementPolicy policy = Runtime::Current()->GetCorePlatformApiEnforcementPolicy(); if (policy == EnforcementPolicy::kDisabled) { return false ; } return detail::HandleCorePlatformApiViolation(member, caller_context, access_method, policy); }
Q中默认是关闭这个功能,R中不知道;这部分简单了解一下就好,主要还是关注kApplication即应用代码访问系统API的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 template <typename T>bool ShouldDenyAccessToMemberImpl (T* member, ApiList api_list, AccessMethod access_method) { Runtime* runtime = Runtime::Current(); EnforcementPolicy hiddenApiPolicy = runtime->GetHiddenApiEnforcementPolicy(); MemberSignature member_signature (member) ; if (member_signature.DoesPrefixMatchAny(runtime->GetHiddenApiExemptions())) { MaybeUpdateAccessFlags(runtime, member, kAccPublicApi); return false ; } bool deny_access = false ; EnforcementPolicy testApiPolicy = runtime->GetTestApiEnforcementPolicy(); if (hiddenApiPolicy == EnforcementPolicy::kEnabled) { if (testApiPolicy == EnforcementPolicy::kDisabled && api_list.IsTestApi()) { deny_access = false ; } else { switch (api_list.GetMaxAllowedSdkVersion()) { case SdkVersion::kP: deny_access = runtime->isChangeEnabled(kHideMaxtargetsdkPHiddenApis); break ; case SdkVersion::kQ: deny_access = runtime->isChangeEnabled(kHideMaxtargetsdkQHiddenApis); break ; default : deny_access = IsSdkVersionSetAndMoreThan(runtime->GetTargetSdkVersion(), api_list.GetMaxAllowedSdkVersion()); } } } if (access_method != AccessMethod::kNone) { if (!deny_access) { MaybeUpdateAccessFlags(runtime, member, kAccPublicApi); } } return deny_access; }
OK,大致流程都已经清晰,梳理一下:
如果目标成员是公开API,直接通过 获取调用者的AccessContext,如果可信那么通过 如果访问检查被完全关闭,那么通过 判断目标成员是否在豁免名单里,如果在那么通过 hiddenApiPolicy == EnforcementPolicy::kJustWarn
,不会对deny_access
赋值,警告后通过以上条件都不满足,根据targetSdkVersion决定是否需要拒绝访问 下有对策 把系统的策略搞清楚了,接下来绕过就容易了。 我们一步一步来: 首先如果这个member的access flags里有kAccPublicApi
,那么系统就认为这是一个公开API,就不会进行任何限制了,然而我们如果要对access_flags动手脚,必须先拿到这个member,然而系统就是限制了我们去拿这个member的过程,死循环了,放弃;
然后,如果调用者是可信的,那么也会通过,有这些情况:
调用者所在的类在系统路径下 调用者所在的类对应的类加载器(ClassLoader)为null(即被BootClassLoader加载的类) debug版并且主动设置了跳过限制,对应接口为VMDebug.allowHiddenApiReflectionFrom(Class<?> klass) 我们有两种方法,一种是直接把自己的类变成系统类,另一种是通过系统API发起调用;第二种对应的实现方案就是套娃反射,然而现在已经被谷歌封掉了,我也找不到其他API,就只剩下把自己的类变成系统类了。 首先排除只能在debug mode工作的3;而1也没法满足,主动修改dex_file->hiddenapi_domain_
需要先拿到这个dex_file指针,而不使用ART内部接口的情况下是不方便拿到的,而且修改hiddenapi_domain_
需要提前知道这个成员变量对应的偏移,先放弃;2我觉得是这三种方法里最好的,Class对象直接在java层就能拿到,改也可以直接在java层改,类似这样:
1 2 3 Field classLoaderField = Class.class.getDeclaredField("classLoader" ); classLoaderField.setAccessible(true ); classLoaderField.set(MyClass.class, null );
然后就可以用这个MyClass进行反射。 问题在于这个classLoader变量也是隐藏API,当然你也可以用Unsafe等方案,但终究不保险;我们最好使用公开API,那有这样的公开API吗? 有! dalvik.system.DexFile中有这样一个方法 :
1 public Class loadClass (String name, ClassLoader loader)
第二个参数就是指定该Class的ClassLoader,设置成null即可。 但使用这个方法,你需要自己额外准备一个dex文件,加载获得DexFile对象后再调用loadClass,略显繁琐,实际使用的话可以弄个gradle脚本,拦截mergeDexDebug/Release拿到dex;另一个问题是DexFile是Deprecated,虽然现在用没问题,但保不准哪天就被谷歌给删了。 提到这个方法,有的小伙伴可能会想起来,我们的目标是获得一个行为受我们控制且class_loader==null的类,java.lang.reflect.Proxy 中也有类似的接口,那么我们可以用动态代理吗? 事实证明是不行的,你确实可以获得一个class_loader==null的代理类,然而动态代理只是一个桥梁,最后执行动作是用的InvocationHandler
对象,最终栈回溯的结果取决于这个InvocationHandler对应的Class。 (注:这就是我当时提出的另一个绕过方法,当时在我自己的贴吧里发了个贴,之后被百度删了,申诉四次没过,呵呵)
似乎从这里入手不太好的样子,我们继续。 第三步和第五步中,都需要通过runtime->GetHiddenApiEnforcementPolicy()
的返回值做判断,查看实现可以看见其实就是返回了一个成员变量hidden_api_policy_
,那我们改这个成员变量不就行了?然而打开runtime.h你就会发现这个对象太大了,什么东西都往里面扔,改里面的值存在一定风险;搜索一下你会发现art通过RAII机制封装了一个ScopedHiddenApiEnforcementPolicySetting
出来,然而这个类的构造函数并没有导出,我们无法直接调用,先放着。
第四步中提到art内部有一个豁免名单,而这个名单同样保存在runtime中,和上面的情况一样;不过这个API暴露到了java层,java层中有对应的接口(VMRuntime.setHiddenApiExemptions(String[] exemptions)
),不过这个接口在黑名单内,也无法直接调用。
OK,研究完了,得出结论:没有较为方便通用稳定的方法…… 才怪。我们的最终目标是成功拿到member,反映到代码里就是ShouldDenyAccessToMember返回false,为此我们可以通过各种方式干扰这个函数的执行,但最终都还是为了让它返回false。既然我们只是为了这个,那么其实可以用更直观的方式:native hook。 查看art代码可以发现无论是P上的ShouldBlockAccessToMember
还是Q上的ShouldDenyAccessToMember
,执行流程中都会调用某个关键的且符号已被导出的函数,P上是GetMemberActionImpl
,Q上是ShouldDenyAccessToMemberImpl
,直接hook住,修改它的返回值就ok了,目前Pine采用了这种方式,具体实现可见这个commit 。
总结 嗯,大概就是这样啦~ 又放一下咱的QQ群:949888394 ~ 感谢你能看到最后~