preface
Starting with Android9, Google has restricted calls to non-SDK interfaces on the Android platform. Any time an application uses a non-SDK interface, or attempts to invoke a non-SDK interface using reflection or JNI, it will receive certain restrictions. And as Android versions get more and more restrictive, more and more interfaces are restricted. These limitations are deadly to the Android platform’s dark technologies (plugins, hotfixes, dual-open apps, performance monitoring, Art hooks, etc.). As a result, various gods are looking for ways around this limitation.
While looking through the Android ART VIRTUAL machine source code recently, I found another easy way around the restrictions. After testing, it can run stably on Android 9-12.
The following introduces the principle of the mainstream bypass restriction, and then introduces this new bypass strategy in detail, and finally gives a complete source code implementation.
How are non-SDK interfaces restricted
Usually a non-SDK interface is called, and in most cases the Method corresponding to the Class is retrieved by reflection at the Java layer, and the Method invocation is implemented via Method.invoke. Class. GetDeclaredMethod getDeclaredMethodInternal will eventually enter a native method, the realization of this method are as follows: (the following source all from Android 11)
// 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()); . Handle<mirror::Method> result = hs.NewHandle( mirror::Class::GetDeclaredMethodInternal<kRuntimePointerSize, false>( 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()); }Copy the code
Given the name of the function, it’s obvious that the call restriction is ShouldDenyAccessToMember(). If this function returns false, an empty Jobject object is returned, and the upper layer cannot get the corresponding Method.
Then, in the mirror: : Class: : GetDeclaredMethodInternal function, also there are several calls ShouldDenyAccessToMember this function, the returned results are restrictions:
template <PointerSize kPointerSize, bool kTransactionActive> 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) { ...... bool m_hidden = hiddenapi::ShouldDenyAccessToMember(&m, fn_get_access_context, access_method); if (! m_hidden && ! m.IsSynthetic()) { // Non-hidden, virtual, non-synthetic. Best possible result, exit early. return Method::CreateFromArtMethod<kPointerSize, kTransactionActive>(self, &m); } else if (IsMethodPreferredOver(result, result_hidden, &m, m_hidden)) { // Remember as potential result. result = &m; result_hidden = m_hidden; }}... DCHECK(! m.IsMiranda()); // Direct methods cannot be miranda methods. bool m_hidden = hiddenapi::ShouldDenyAccessToMember(&m, fn_get_access_context, access_method); 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, kTransactionActive>(self, &m); } else if (IsMethodPreferredOver(result, result_hidden, &m, m_hidden)) { } } } return result ! = nullptr ? Method::CreateFromArtMethod<kPointerSize,kTransactionActive>(self, result) : nullptr; }Copy the code
From the perspective of the logic of all those places, in the virtual machine is through hiddenapi: : ShouldDenyAccessToMember access restrictions for this function. The only way around this seems to be to tamper with the return value of this function. Some of the current mainstream bypass methods do exactly that.
The mainstream way around
There are several ways around the non-SDK call restriction in the open source community. The main idea is to interfere with ShouldDenyAccessToMember and change its return value to false.
Since ShouldDenyAccessToMember is an exported function, open libart.so and you can see that it looks up the corresponding function symbol. One of the simplest ways to do this is to hook the ShouldDenyAccessToMember function so that it returns false without calling the original one, using the native inline hook technique. This simply bypasses the call restriction.
Alternatively, you can modify the return value of this function if the hook technique is not applicable. Only need to modify this function calls the Runtime: : Current () – > GetHiddenApiEnforcementPolicy interface () returns a value, the return value is EnforcementPolicy. KDisabled, So ShouldDenyAccessToMember must return false. Because the ShouldDenyAccessToMember function has a line like this:
// art/runtime/hidden_api.h #ShouldDenyAccessToMember()
EnforcementPolicy policy = Runtime::Current()->GetHiddenApiEnforcementPolicy();
if (policy == EnforcementPolicy::kDisabled) {
return false;
}
Copy the code
The offset of the HIDden_API_policy_ member in the Runtime class and the address of the current_Runtime is used to calculate the address of the hidden_API_policy_ member. To modify the address of the memory value of EnforcementPolicy. KDisabled.
// art/runtime/runtime.h // Therefore can only access to the member variables by memory search. Hiddenapi: : EnforcementPolicy GetHiddenApiEnforcementPolicy const () {return hidden_api_policy_; } // Whether access checks on hidden API should be performed. hiddenapi::EnforcementPolicy hidden_api_policy_;Copy the code
In Android9 you can also use double reflection (metareflection) to get around this. But in android11 the meta reflection method has been blocked by Google. There is another way to circumvent Google’s blocking. Compile the code associated with the double reflection call into a dex file separately, then construct a DexFile object to execute the double reflection method and, at load time, set the classloader loaded to null to bypass the restriction. The specific code is as follows:
DexFile dexFile = new DexFile(dexFile); Class<? > bootstrapClass = dexFile.loadClass(BootstrapClass.class.getCanonicalName(), null); Method exemptAll = bootstrapClass.getDeclaredMethod("exemptAll"); return (boolean) exemptAll.invoke(null);Copy the code
Why does this work? Tracking source can be known, dexFile. LoadClass (), will eventually perform to ClassLinker: : RegisterDexFileLocked, this function call InitializeDexFileDomain:
// art/runtime/class_linker.cc void ClassLinker::RegisterDexFileLocked(const DexFile& dex_file, ObjPtr<mirror::DexCache> dex_cache, ObjPtr<mirror::ClassLoader> class_loader) { ... // Let hiddenapi assign a domain to the newly registered dex file. hiddenapi::InitializeDexFileDomain(dex_file, class_loader); . }Copy the code
InitializeDexFileDomain this function incoming this is empty, the following DetermineDomainFromLocation must return a Domain: : kPlatform, The value of the dex_file corresponding hiddenAPI_domain_ member object is Domain::kPlatform.
// // 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);
if (IsDomainMoreTrustedThan(dex_domain, dex_file.GetHiddenapiDomain())) {
dex_file.SetHiddenapiDomain(dex_domain);
}
}
static Domain DetermineDomainFromLocation(const std::string& dex_location,
ObjPtr<mirror::ClassLoader> class_loader) {
...
if (LocationIsOnSystemFramework(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;
}
Copy the code
How does this affect the return of the ShouldDenyAccessToMember() function? Back to the ShouldDenyAccessToMember() function, it has this logic:
// art/runtime/hidden_api.h template<typename T> inline bool ShouldDenyAccessToMember(T* member, const std::function<AccessContext()>& fn_get_access_context, AccessMethod access_method) { ... // Determine which domain the caller and callee belong to. const AccessContext caller_context = fn_get_access_context(); const AccessContext callee_context(member->GetDeclaringClass()); // 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)) { return false; }... } // 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_); }Copy the code
Where caller_context corresponds to the caller’s context, and the corresponding domain is dex_file’s domain, which returns kPlatform, and callee_context is calculated based on class and dex_file. This value is also kPlatform. So caller_Context.canalwaysAccess (callee_context) in the above code returns true, because the IsDomainMoreTrustedThan function simply compares the size of two domains:
// 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);
}
Copy the code
So ShouldDenyAccessToMember() also returns false, bypassing the access restriction. The essence of this scheme is that the domain corresponding to the DexFile of App should be kApplication. As an empty classloader is passed in the class loading process, the domain value becomes kPlatform, thus bypassing the access restriction.
In addition, Google engineers have figured out a way to block this scenario, and the fix has been submitted, but perhaps because the test cases didn’t all pass, the code is not currently incorporated into the main branch. The Patch submission is: android-review.googlesource.com/c/platform/…
This Patch is actually very simple, other is DetermineDomainFromLocation () function, when this is empty, return Domain: : kApplication, rather than the Domain: : kPlatform.
In addition, an elegant way to bypass the method is the Unsafe class for Java operation memory. Implementation source code: github.com/LSPosed/And…
The implementation steps of this scheme are as follows:
- According to the difference between two adjacent methods in the class, the size of the native layer ArtMethod data structure is calculated.
- Construct a Class that is exactly the same as the java.lang.Class member and calculate the offset of the methods member in the native layer.
- Unsafe determines the number of methods in a class, and traverses all methods in a class based on the offset, number, and size of each method.
- Use MethodHandleImpl. Java to convert the ArtMethod(jmethodId) pointer to mirror::Method(corresponding to Java layer java.lang.method).
The following is a mirror Class of java.lang.Class, which is used to prevent reflection from retrieving private member objects in java.lang.
static final public class Class { private transient ClassLoader classLoader; private transient java.lang.Class<? > componentType; private transient Object dexCache; private transient Object extData; private transient Object[] ifTable; private transient String name; private transient java.lang.Class<? > superClass; private transient Object vtable; private transient long iFields; private transient long methods; private transient long sFields; private transient int accessFlags; private transient int classFlags; private transient int classSize; private transient int clinitThreadId; . . }Copy the code
Mirror ::Method: Java.lang.Method: Mirror ::Method: Java.lang.Method
// art/runtime/native/java_lang_invoke_MethodHandleImpl.cc
static jobject MethodHandleImpl_getMemberInternal(JNIEnv* env, jobject thiz) {
ScopedObjectAccess soa(env);
StackHandleScope<2> hs(soa.Self());
Handle<mirror::MethodHandleImpl> handle = hs.NewHandle(
soa.Decode<mirror::MethodHandleImpl>(thiz));
const mirror::MethodHandle::Kind handle_kind = handle->GetHandleKind();
MutableHandle<mirror::Object> h_object(hs.NewHandle<mirror::Object>(nullptr));
if (handle_kind >= mirror::MethodHandle::kFirstAccessorKind) {
ArtField* const field = handle->GetTargetField();
h_object.Assign(mirror::Field::CreateFromArtField<kRuntimePointerSize, false>(
soa.Self(), field, /* force_resolve= */ false));
} else {
ArtMethod* const method = handle->GetTargetMethod();
if (method->IsConstructor()) {
h_object.Assign(mirror::Constructor::CreateFromArtMethod<kRuntimePointerSize, false>(
soa.Self(), method));
} else {
h_object.Assign(mirror::Method::CreateFromArtMethod<kRuntimePointerSize, false>(
soa.Self(), method));
}
}
return soa.AddLocalReference<jobject>(h_object.Get());
}
Copy the code
This function does not call ShouldDenyAccessToMember, so this method works.
New way around
JNIEnv->GetMethodId(); JNIEnv->GetMethodId(); Then call JNIEnv->CallObjectMethod() to execute the corresponding method:
jclass context_class = env->FindClass("android/content/Context"); jmethodID get_content_resolver_mid = env->GetMethodID(context_class, "getContentResolver", "()Landroid/content/ContentResolver;" ); jobject content_resolver_obj = env->CallObjectMethod(context, get_content_resolver_mid);Copy the code
Perhaps we can find some breakthrough from the native layer. JNIEnv->GetMethodId()
// art/runtime/jni/jni_internal.cc static jmethodID GetMethodID(JNIEnv* env, jclass java_class, const char* name, const char* sig) { ScopedObjectAccess soa(env); return FindMethodID<kEnableIndexIds>(soa, java_class, name, sig, false); } template<bool kEnableIndexIds> static jmethodID FindMethodID(ScopedObjectAccess& soa, jclass jni_class, const char* name, const char* sig, bool is_static) REQUIRES_SHARED(Locks::mutator_lock_) { return jni::EncodeArtMethod<kEnableIndexIds>(FindMethodJNI(soa, jni_class, name, sig, is_static)); } ArtMethod* FindMethodJNI(const ScopedObjectAccess& soa, jclass jni_class, const char* name, const char* sig, bool is_static) { ObjPtr<mirror::Class> c = EnsureInitialized(soa.Self(), soa.Decode<mirror::Class>(jni_class)); . ArtMethod* method = nullptr; auto pointer_size = Runtime::Current()->GetClassLinker()->GetImagePointerSize(); if (c->IsInterface()) { method = c->FindInterfaceMethod(name, sig, pointer_size); } else { method = c->FindClassMethod(name, sig, pointer_size); } if (method ! = nullptr && ShouldDenyAccessToMember(method, soa.Self())) { method = nullptr; }... return method; }Copy the code
In FindMethodJNI function, see the familiar again: ShouldDenyAccessToMember function. In other words, in this process, ShouldDenyAccessToMember is also used to restrict App access to ArtMethod objects corresponding to non-SDK methods. FindClassMethod(name, sig, pointer_size); Does this call flow have similar restrictions:
// art/runtime/mirror/class.cc ArtMethod* FindClassMethod(std::string_view name, std::string_view signature, PointerSize pointer_size) { return FindClassMethodWithSignature(this, name, signature, pointer_size); template <typename SignatureType> static inline ArtMethod* FindClassMethodWithSignature(ObjPtr<Class> this_klass, std::string_view name, const SignatureType& signature, PointerSize pointer_size) REQUIRES_SHARED(Locks::mutator_lock_) { for (ArtMethod& method : this_klass->GetDeclaredMethodsSlice(pointer_size)) { ArtMethod* np_method = method.GetInterfaceMethodIfProxy(pointer_size); if (np_method->GetName() == name && np_method->GetSignature() == signature) { return &method; } } ObjPtr<Class> klass = this_klass->GetSuperClass(); ArtMethod* uninherited_method = nullptr; for (; klass ! = nullptr; klass = klass->GetSuperClass()) { DCHECK(! klass->IsProxyClass()); for (ArtMethod& method : klass->GetDeclaredMethodsSlice(pointer_size)) { if (method.GetName() == name && method.GetSignature() == signature) { if (IsInheritedMethod(this_klass, klass, method)) { return &method; } uninherited_method = &method; break; } } if (uninherited_method ! = nullptr) { break; } } ObjPtr<Class> end_klass = klass; DCHECK_EQ(uninherited_method ! = nullptr, end_klass ! = nullptr); klass = this_klass; . for (; klass ! = end_klass; klass = klass->GetSuperClass()) { DCHECK(! klass->IsProxyClass()); for (ArtMethod& method : klass->GetCopiedMethodsSlice(pointer_size)) { if (method.GetName() == name && method.GetSignature() == signature) { return &method; // No further check needed, copied methods are inherited by definition. } } } return uninherited_method; // Return the `uninherited_method` if any. } }Copy the code
Obviously, ShouldDenyAccessToMember is not called in the FindClassMethod process, so there are no access restrictions. The main flow of this function is to iterate over all ArtMethod Pointers from mirror::Class and its parent, looking for ArtMethod Pointers that match the target name and signture.
One good thing came out of
Since mirror: : Class: : FindClassMethod this function not the limit on the SDK interface logic, so why don’t we call this function directly? To call a non-public native function, the following two conditions must be met:
- You can get the address of this function;
- The ability to construct the parameters that need to be passed to call a function;
FindClassMethod is a non-inline function and can be found in the libart.so file symbol table.
_ZN3art6mirror5Class15FindClassMethodENSt3__117basic_string_viewIcNS2_11char_traitsIcEEEES6_NS_11PointerSizeE
Therefore, the address of this function can be easily obtained using the Linux dynamic library DL interface as follows:
const char *func_name = "_ZN3art6mirror5Class15FindClassMethodENSt3__117basic_string_viewIcNS2_11char_traitsIcEEEES6_NS_11PointerSizeE";
void *art_so_handle = dlopen("libart.so", RTLD_NOW);
void* address = dlsym(art_so_handle, func_name);
auto findClassMethod = reinterpret_cast<void *(*)(void *, std::string_view, std::string_view, size_t)>(address);
Copy the code
However, it is important to note that as of Android7.0, Android restricts apps from opening the system dynamic library using dlopen. However, I have developed a library that can easily circumvent this limitation. Bypass_dlfunctions implementation principle: another way to bypass the Android system library access restrictions
The function passes a pointer to the mirror::Class Class, the name of the method, the signature of the method, and the size of the pointer. Obviously, all but the first object are known. The difficulty is the first argument. Go back up and see how the FindMethodJNI function gets the mirror::Class pointer:
ObjPtr<mirror::Class> c = soa.Decode<mirror::Class>(jni_class);
Copy the code
The Decode function is as follows:
// art/runtime/scoped_thread_state_change-inl.h
template<typename T>
inline ObjPtr<T> ScopedObjectAccessAlreadyRunnable::Decode(jobject obj) const {
Locks::mutator_lock_->AssertSharedHeld(Self());
DCHECK(IsRunnable()); // Don't work with raw objects in non-runnable states.
return ObjPtr<T>::DownCast(Self()->DecodeJObject(obj));
}
Copy the code
Thread::DecodeJObject Thread::DecodeJObject Thread::DecodeJObject Thread::DecodeJObject
// art/runtime/thread.cc ObjPtr<mirror::Object> Thread::DecodeJObject(jobject obj) const { ... IndirectRef ref = reinterpret_cast<IndirectRef>(obj); IndirectRefKind kind = IndirectReferenceTable::GetIndirectRefKind(ref); ObjPtr<mirror::Object> result; bool expect_null = false; if (kind == kLocal) { IndirectReferenceTable& locals = tlsPtr_.jni_env->locals_; result = locals.Get<kWithoutReadBarrier>(ref); } else if (kind == kJniTransitionOrInvalid) { result = reinterpret_cast<mirror::CompressedReference<mirror::Object>*>(obj)->AsMirrorPtr(); VerifyObject(result); } else if (kind == kGlobal) { result = tlsPtr_.jni_env->vm_->DecodeGlobal(ref); } else { result = tlsPtr_.jni_env->vm_->DecodeWeakGlobal(const_cast<Thread*>(this), ref); if (Runtime::Current()->IsClearedJniWeakGlobal(result)) { expect_null = true; result = nullptr; }}... return result; }Copy the code
The DecodeJObject function is also an export function. The corresponding symbol is libart.so
_ZNK3art6Thread13DecodeJObjectEP8_jobject
The first argument to this function is a pointer to the current Thread’s native Thread. The pointer can be obtained in two ways. The first way is to obtain the member variable of The nativePeer of Thread. Java by reflection. The pointer of the native Thread is stored in this value.
jclass thread_class = env->FindClass("java/lang/Thread"); jmethodID currentThread_id = env->GetStaticMethodID(thread_class, "currentThread", "()Ljava/lang/Thread;" ); jobject current_thread = env->CallStaticObjectMethod(thread_class, currentThread_id); jfieldID nativePeer_id = env->GetFieldID(thread_class, "nativePeer", "J"); jlong native_thread = env->GetLongField(current_thread, nativePeer_id);Copy the code
Using this approach requires reflection to retrieve the nativePeer, a private member variable in Thread.java, which is currently listed in GreyList. The GreyList API can be called, but it may be blacklisted in future TargetSDK versions.
Accessing hidden field Ljava/lang/Thread; ->nativePeer:J (greylist, JNI, allowed)Copy the code
To do this, we can use another method to get the native Thread pointer.
In the JNIEnv structure, the first position is the virtual function table, and the second position is the native Thread pointer. The JNIEnv is known in each thread. Therefore, the Thread pointer can be easily obtained by using the following method, which results in exactly the same result as the above method.
auto* fakeEnv = reinterpret_cast<FakeJNIEnv*>(jni_env);
void* native_thread = fakeEnv->self_;
struct FakeJNIEnv {
void* vtb_;
void *const self_; // Link to Thread::Current().
void *const vm_; // The invocation interface JavaVM.
};
Copy the code
Call Thread::DecodeJObject to retrieve the mirror:: class pointer to the current class, the name of the method, and the signature. Calls the mirror: : Class: : method to get the corresponding ArtMethod FindClassMethod function pointer, the pointer is jmethodId this method. Call CallObjectMethod and pass in the jmethodId to implement calls to non-SDK interfaces. At this point, the ART virtual machine invocation restriction on non-SDK interfaces has been successfully bypassed.
Complete the process
The complete code flow is as follows:
struct FakeJNIEnv { void* vtb_; void *const self_; // Link to Thread::Current(). void *const vm_; // The invocation interface JavaVM. }; void bypassHiddenApi(JNIEnv *env) { auto* fakeEnv = reinterpret_cast<MirrorJNIEnv*>(env); void* current_thread = fakeEnv->self_; const char *findClassMethod_func_name = "_ZN3art6mirror5Class15FindClassMethodENSt3__117basic_string_viewIcNS2_11char_traitsIcEEEES6_NS_11PointerSizeE"; void *art_so_handle = bp_dlopen("libart.so", RTLD_NOW); void* address = bp_dlsym(art_so_handle, findClassMethod_func_name); auto findClassMethod = reinterpret_cast<void *(*)(void *, std::string_view, std::string_view, size_t)>(address); const char *decodeJObject_sig = "_ZNK3art6Thread13DecodeJObjectEP8_jobject"; void *art_so_address = bp_dlopen("libart.so", RTLD_NOW); auto decodeJObject_func = reinterpret_cast<void *(*)(void *, void *)>(bp_dlsym(art_so_address, decodeJObject_sig)); const char *VMRuntime_class_name = "dalvik/system/VMRuntime"; jclass vmRumtime_class = env->FindClass(VMRuntime_class_name); void *VMRuntime_mirror_class_ObjPtr = decodeJObject_func(current_thread, vmRumtime_class); size_t pointer_size = sizeof(void*); void *getRuntime_art_method = findClassMethod(VMRuntime_mirror_class_ObjPtr, "getRuntime", "()Ldalvik/system/VMRuntime;" , pointer_size); jobject vmRuntime_instance = env->CallStaticObjectMethod(vmRumtime_class, (jmethodID)getRuntime_art_method); const char *target_char = "L"; jstring mystring = env->NewStringUTF(target_char); jclass cls = env->FindClass("java/lang/String"); jobjectArray jarray = env->NewObjectArray(1, cls, nullptr); env->SetObjectArrayElement(jarray, 0, mystring); void *setHiddenApiExemptions_art_method = findClassMethod(VMRuntime_mirror_class_ObjPtr, "setHiddenApiExemptions", "([Ljava/lang/String;)V", pointer_size); env->CallVoidMethod(vmRuntime_instance, (jmethodID)setHiddenApiExemptions_art_method, jarray); }Copy the code
In the above implementation, reflection ends up calling two hidden interfaces:
VMRuntime runtime = VMRuntime.*getRuntime*();
runtime.setHiddenApiExemptions(new String[]{"L"});
Copy the code
The code implementation of ShouldDenyAccessToMember checks whether the class name prefix for member is included in the vector returned by GetHiddenApiExemptions. All class names start with ‘L’. Put ‘L’ in HiddenApiExemptions and ShouldDenyAccessToMember always returns false.
Source code and usage
Complete source code release to making: bypassHiddenApiRestriction
Add the following dependencies to build.gradle:
allprojects {
repositories {
mavenCentral()
}
}
Copy the code
Dependencies {implementation 'IO. Making. Windysha: bypassHiddenApiRestriction: 1.0.2'}Copy the code
Add the following code to the App entry code:
import com.wind.hiddenapi.bypass.HiddenApiBypass
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
HiddenApiBypass.startBypass();
}
Copy the code