2018年发布的Android 9中引入了对隐藏API的限制,这对整个Android生态来说当然是一件好事,但也严重限制了以往我们通过反射等手段实现的“黑科技”(如插件化等),所以开发者们纷纷寻找手段绕过这个限制,比如我曾经提出了两个绕过方法,其中一个便是几乎完美的双重反射(即“元反射”,现在来看叫“套娃反射”比较好);而在即将发布的Android R中把这个方法封杀了(谷歌:禁止套娃!),因此我重新研究了Android R中的限制策略。

上有政策

常言道,知己知彼,百战百胜。要想破解这个限制,就必须去搞懂系统是怎么施加的限制;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);

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

// 4:非可信调用者尝试访问隐藏API,根据调用者的Domain决定行为
switch (caller_context.GetDomain()) {
case Domain::kApplication: {
DCHECK(!callee_context.IsApplicationDomain());

// 如果访问检查被完全禁用,那么直接返回
EnforcementPolicy policy = Runtime::Current()->GetHiddenApiEnforcementPolicy();
if (policy == EnforcementPolicy::kDisabled) {
return false;
}

// 5:调用detail::ShouldDenyAccessToMemberImpl,决定是否需要拒绝访问
return detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method);
}
// 省略
}

这个函数还是比较长的,我们一步步分析。

  1. 判断目标成员是否是公开API,如果是那么直接通过,具体可以看见是通过GetRuntimeFlags获取,这个flags其实是储存在该成员的access_flags_中(其实这里不太完全,暂且这么认为吧)
  2. 获取调用者的Domain,判断是否可信,如果可信直接通过
  3. 以上条件都不满足,根据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) {
// Attached native thread. Assume this is *not* boot class path.
caller = nullptr;
return false;
} else if (m->IsRuntimeMethod()) {
// 跳过 ART Runtime 内部方法
return true;
}

ObjPtr<mirror::Class> declaring_class = m->GetDeclaringClass();
if (declaring_class->IsBootStrapClassLoaded()) {
// 跳过 java.lang.Class 中的内部方法
if (declaring_class->IsClassClass()) {
return true;
}

// 跳过 java.lang.invoke.*
ObjPtr<mirror::Class> lookup_class = GetClassRoot<mirror::MethodHandlesLookup>();
if ((declaring_class == lookup_class || declaring_class->IsInSamePackage(lookup_class))
&& !m->IsClassInitializer()) {
return true;
}

// 如果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;
}

根据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) {
// dex_file == nullptr && class_loader == nullptr(即被BootClassLoader加载的类),那么可信
return ComputeDomain(/* is_trusted= */ class_loader.IsNull());
}
// 获取dex_file的Domain
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()) {
// debug mode下开发者可以主动指定某类可信,用于调试
domain = ComputeDomain(/* is_trusted= */ 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()) {
// 在几个核心apex module内
if (LocationIsOnArtModule(dex_location.c_str())
|| LocationIsOnConscryptModule(dex_location.c_str())
|| LocationIsOnI18nModule(dex_location.c_str())) {
return Domain::kCorePlatform;
}

// 在apex下但是不是核心module
if (LocationIsOnApex(dex_location.c_str())) {
return Domain::kPlatform;
}
}

// 在/system/framework/下
if (LocationIsOnSystemFramework(dex_location.c_str())) {
return Domain::kPlatform;
}

// 在/system/ext/framework/下
if (LocationIsOnSystemExtFramework(dex_location.c_str())) {
return Domain::kPlatform;
}

// ClassLoader是null(即BootClassLoader)
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);

// 如果是需要暴露出来的Core Platform API,通过
if ((runtime_flags & kAccCorePlatformApi) != 0) {
return false;
}

// 完全关闭访问限制,通过
// Android Q中默认是关闭,R中不知道
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);

// ART内部存在一个豁免名单,如果匹配,那么通过
if (member_signature.DoesPrefixMatchAny(runtime->GetHiddenApiExemptions())) {
MaybeUpdateAccessFlags(runtime, member, kAccPublicApi);
return false;
}

bool deny_access = false;

EnforcementPolicy testApiPolicy = runtime->GetTestApiEnforcementPolicy();
// 如果hiddenApiPolicy == EnforcementPolicy::kJustWarn,那么不作处理(EnforcementPolicy只有三种状态)
if (hiddenApiPolicy == EnforcementPolicy::kEnabled) {
if (testApiPolicy == EnforcementPolicy::kDisabled && api_list.IsTestApi()) {
// ART内部对测试API(即有@TestApi注解的API)的特别处理
deny_access = false;
} else {
// 比较SdkVersion,决定是否需要拒绝访问
// 所谓的 灰名单/黑名单 便在此实现
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) {
// 省略代码:发出关于访问隐藏API的警告

// If this access was not denied, move the member into whitelist and skip
// the warning the next time the member is accessed.
if (!deny_access) {
MaybeUpdateAccessFlags(runtime, member, kAccPublicApi);
}
}

return deny_access;
}

OK,大致流程都已经清晰,梳理一下:

  1. 如果目标成员是公开API,直接通过
  2. 获取调用者的AccessContext,如果可信那么通过
  3. 如果访问检查被完全关闭,那么通过
  4. 判断目标成员是否在豁免名单里,如果在那么通过
  5. hiddenApiPolicy == EnforcementPolicy::kJustWarn,不会对deny_access赋值,警告后通过
  6. 以上条件都不满足,根据targetSdkVersion决定是否需要拒绝访问

下有对策

把系统的策略搞清楚了,接下来绕过就容易了。
我们一步一步来:
首先如果这个member的access flags里有kAccPublicApi,那么系统就认为这是一个公开API,就不会进行任何限制了,然而我们如果要对access_flags动手脚,必须先拿到这个member,然而系统就是限制了我们去拿这个member的过程,死循环了,放弃;

然后,如果调用者是可信的,那么也会通过,有这些情况:

  1. 调用者所在的类在系统路径下
  2. 调用者所在的类对应的类加载器(ClassLoader)为null(即被BootClassLoader加载的类)
  3. 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~
感谢你能看到最后~