通过替换类加载器来绕过访问权限的一些问题

0x00 前言

组里有java热修复项目,其中有一部分功能是根据diff自动生成补丁,在最近维护的时候,遇到了一个问题,在补丁运行时会出现IllegalAccessException,说到这里可能很多人就已经知道答案了,由于IsInSamePackage导致,因此反射替换类加载器为原先的类即可。 这样确实没什么问题,但是我们的场景在系统中,于是这里遇上了一些新的问题。情况是,在替换类加载器后,仍然在IsInSamePackage中没有通过类加载器是否相同的校验,导致返回false,抛出非法访问的异常。

0x01 分析

这里还是需要先简单介绍一下,IllegalAccessException这个异常出现的原因,首先可以在虚拟机里搜索这个字段,我们这里只探讨,补丁类对象在访问被修补的类的同包类是,导致的非法访问异常。可以搜索到涉及的报错函数如下

1
2
3
4
5
6
void ThrowIllegalAccessErrorClass(ObjPtr<mirror::Class> referrer, ObjPtr<mirror::Class> accessed) {
std::ostringstream msg;
msg << "Illegal class access: '" << mirror::Class::PrettyDescriptor(referrer)
<< "' attempting to access '" << mirror::Class::PrettyDescriptor(accessed) << "'";
ThrowException("Ljava/lang/IllegalAccessError;", referrer, msg.str().c_str());
}

通过在源码中检索这个函数,发现它都是在一个叫CanAccess的函数判断结束之后调用的

1
2
3
inline bool Class::CanAccess(ObjPtr<Class> that) {
return that->IsPublic() || this->IsInSamePackage(that);
}

这里的实现也非常简单,可以看到做了两个判断,目标类是否是public的,或是否是同包下的。而接下来,就是很多人都分析过的,IsInSamePackage这个函数。

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
bool Class::IsInSamePackage(std::string_view descriptor1, std::string_view descriptor2) {
size_t i = 0;
size_t min_length = std::min(descriptor1.size(), descriptor2.size());
while (i < min_length && descriptor1[i] == descriptor2[i]) {
++i;
}
if (descriptor1.find('/', i) != std::string_view::npos ||
descriptor2.find('/', i) != std::string_view::npos) {
return false;
} else {
return true;
}
}

bool Class::IsInSamePackage(ObjPtr<Class> that) {
ObjPtr<Class> klass1 = this;
ObjPtr<Class> klass2 = that;
if (klass1 == klass2) {
return true;
}
// Class loaders must match.
if (klass1->GetClassLoader() != klass2->GetClassLoader()) {
return false;
}
// Arrays are in the same package when their element classes are.
while (klass1->IsArrayClass()) {
klass1 = klass1->GetComponentType();
}
while (klass2->IsArrayClass()) {
klass2 = klass2->GetComponentType();
}
// trivial check again for array types
if (klass1 == klass2) {
return true;
}
// Compare the package part of the descriptor string.
std::string temp1, temp2;
return IsInSamePackage(klass1->GetDescriptor(&temp1), klass2->GetDescriptor(&temp2));
}

实现也不复杂,首先就是对类的类加载器是否相同进行检查,而后调用两个参数的重载函数对类的报名进行对比。这里为了让他们通过类加载器对比的校验,常用的解决方案都是直接将目标类加载器补丁的类加载器,来达到通过检查的目的。而在对系统进行java热修复时,这里会有例外的情况,在android中,有很多类是直接使用bootclassloader进行创建的,而在修补这些类的时候,是不能通过替换类加载器来达到目的。我之前也纳闷很急这里为什么不行。后来看了下GetClassLoader,发现它的实现并不是直接返回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
static ObjPtr<mirror::ClassLoader> GetClassLoader(const ScopedObjectAccess& soa)
REQUIRES_SHARED(Locks::mutator_lock_) {
ArtMethod* method = soa.Self()->GetCurrentMethod(nullptr);
// If we are running Runtime.nativeLoad, use the overriding ClassLoader it set.
if (method ==
jni::DecodeArtMethod<kEnableIndexIds>(WellKnownClasses::java_lang_Runtime_nativeLoad)) {
return soa.Decode<mirror::ClassLoader>(soa.Self()->GetClassLoaderOverride());
}
// If we have a method, use its ClassLoader for context.
if (method != nullptr) {
return method->GetDeclaringClass()->GetClassLoader();
}
// We don't have a method, so try to use the system ClassLoader.
ObjPtr<mirror::ClassLoader> class_loader =
soa.Decode<mirror::ClassLoader>(Runtime::Current()->GetSystemClassLoader());
if (class_loader != nullptr) {
return class_loader;
}
// See if the override ClassLoader is set for gtests.
class_loader = soa.Decode<mirror::ClassLoader>(soa.Self()->GetClassLoaderOverride());
if (class_loader != nullptr) {
// If so, CommonCompilerTest should have marked the runtime as a compiler not compiling an
// image.
CHECK(Runtime::Current()->IsAotCompiler());
CHECK(!Runtime::Current()->IsCompilingBootImage());
return class_loader;
}
// Use the BOOTCLASSPATH.
return nullptr;
}

可以看到,如果需要BOOTCLASS的时候,它是直接返回null的,所以如果使用原类的类加载器替换补丁的加载器,可能会出现,使用bootclassloader来替换补丁类加载器的情况,而使用bootclassloader加载的类,在GetClassLoader的时候,返回的其实是空,这样就导致IsInSamePackage中校验类加载器是否不同的地方走入if流程,返回false,导致非法访问异常。

0x02 解决方案

既然这里需要让它在对比类加载器的时候成功,那么bootclassloader在GetClassLoader的地方返回空,那我们也让补丁类在调用这个方法返回空即可,也就是说这里使用反射,将补丁类的classloader置为空即可。目前尝试了下,这么做的话,确实是可以避免这个IllegalAccessExecption的,但是目前来说,对补丁类的类加载器进行置空的操作,并不确定是否存在其他问题,具体得看后续的测试结果了。

0x03 总结

总结就是,我好菜啊,没有考虑到常见热修方案都是app的,可能没有考虑过系统上的一些问题。。。还得多学点儿东西的样子,溜了溜了。