1 背景
以下内容摘录自官方文档
针对非 SDK 接口的限制
从 Android 9(API 级别 28)开始,Android 平台对应用能使用的非 SDK 接口 实施了限制。只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用。这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用发生崩溃的风险,同时为开发者降低紧急发布的风险。如需详细了解有关此限制的决定,请参阅通过减少非 SDK 接口的使用来提高稳定性。
官方文档
developer.android.com/guide/app-c...
2 资源准备
git clone android.googlesource.com/platform/ar... --depth 1
3 方法反射(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的父类。
案例:
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的隐藏策略
- 第三方app肯定会走到这里,根据domain级别区分对待hiddenapi策略
- 如果policy disable,则返回false,即允许访问(重点)
- 最终会调用
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 总结
- 系统framework代码中可以通过设置setHiddenApiExemptions,达到随意访问hiddenapi的目的
- 由于class VMRuntime被hide,可以在JNI_OnLoad中操作VMRuntime,达到调用setHiddenApiExemptions的目的
初步形成解决方案
系统类伪装
如果调用者是系统类,那么就允许被调用。即如果我们能以系统类的身份去反射,那么就能畅通无阻:
- 首先通过反射 API 拿到 getDeclaredMethod 方法 。getDeclaredMethod 是 public 的,不存在问题;这个通过反射拿到的方法网上称之为元反射方法。
- 然后通过刚刚的元反射方法去反射调用 getDeclardMethod。这里我们就实现了以系统身份去反射的目的------反射相关的 API 都是系统类,因此我们的元反射方法也是被系统类加载的方法;所以我们的元反射方法调用的 getDeclardMethod 会被认为是系统调用的,可以反射任意的方法。
- 另外系统在检查豁免时是通过方法签名前缀进行匹配的,而 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
,此时caller
为java.lang.System
其domain
级别为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;
}
Demo源码
暂时无法在路特斯桌面文档外展示此内容