本文是隐藏API绕过的实践篇,在阅读本文前请先阅读Android Hook - 隐藏API拦截机制,了解Android P隐藏API拦截机制在源码层面的细节。
本文旨在提供介绍绕过隐藏API的各种方式,学习常见Hook技巧和工具的使用。
一、背景
1、隐藏API拦截原理
根据Android Hook - 隐藏API拦截机制的分析,隐藏API拦截机制分别有四个关键的检查位置,即目标方法AccessFlags、系统EnforcementPolicy、调用栈检查、豁免名单检查。
本文将逐一介绍每个关键位置可以使用Hook方式绕过的思路,建议结合源码分析查看。
我们的目标是可以实现调用VMRuntime.setHiddenApiExemptions("L")
,从而等价于把所有方法加入到豁免名单。
2、实践前准备
在动手实践前,需要做一些准备:
-
保证Android studio可以使用LLDB。可以使用lldb来Debug和反编译。
-
MAC安装Hopper Disassembler软件,Windows安装IDA。我们将使用反编译软件,确认动态库中的符号是否存在。这两者都需要付费,但是有试用期。如果不使用软件,可以使用elfreader等脚本代替。
-
了解Inline Hook。部分绕过实现依赖于Inline Hook,如果你从来没有了解过,那么可以把它当做可以Hook动态库指定方法的工具,只需要知道怎么使用而不必去深究其原理。
-
ShadowHook。本文将使用字节跳动Inline Hook开源库android-inline-hook,可以按照官方文档进行依赖配置。通常使用过程如下:
c++代码解读复制代码//1、通过工具从动态库找到要Hook的方法对应的符号 #define SYMBOL "方法符号" //2、进行Inline Hook,分别传入目标动态库名称、方法对应的符号、和代理函数指针 shadowhook_hook_sym_name("libart.so", SYMBOL, proxy, nullptr); //3、代理函数 void proxy(){ //3.1、SHADOWHOOK_CALL_PREV可以调用原函数 SHADOWHOOK_CALL_PREV(...) //3.2、编写hook逻辑代码 Hook逻辑 }
-
dlfcn绕过。
-
dlfcn指
dlopen
、dlsym
、dlclose
系列方法,用于找到动态库中指定方法的指针,从而可以调用这个方法。 -
dlfcn在Android N(7)及以上被禁止用于加载系统私有库,但android-inline-hook提供了
shadowhook_dflcn
系列方法用于绕过这个限制,及使用如shadowhook_dlsym
方法去进行隐藏API绕过的前提是,需要先绕过dlfcn
,这就是另外一个话题了。通常使用过程如下:
c++代码解读复制代码//1、通过工具从动态库找到要Hook的方法对应的符号 #define SYMBOL "方法符号" //2、shadowhook_dlsym传入符号,找到方法指针 void *funcPtr = shadowhook_dlsym(libart, Str_GetArtMethod); //3、使用方法指针来调用方法 funcPtr();
-
-
二、绕过实践
1、修改AccessFlags
第一个例子是相对复杂的例子,需要兼容不同的Android版本,但是在了解这个过程中使用的技巧和工具以后,要理解后面的例子则简单很多。
1.1、Android P实现
根据Android P源码的分析,ArtMethod.access_flags
的高29、30位,记录着该方法属于哪个级别的隐藏API名单,即:
c++ 代码解读复制代码class HiddenApiAccessFlags {
public:
enum ApiList {
kWhitelist = 0, //白名单, 0x00
kLightGreylist, //浅灰名单, 0x01
kDarkGreylist, //深灰名单, 0x10
kBlacklist, //黑名单, 0x11
};
...
}
因此,我们的思路就是在对ArtMethod
进行隐藏API判断前,把它的access_flags
的高29、30位置为0,就等价于把这个方法加入到了白名单内。
1.1.1、Hook的时机
接下来的问题就是找到一个合适的时机(方法),并且这个时机可以获取到ArtMethod
指针,用于修改access_flags
。
c++ 代码解读复制代码static jobject Class_getDeclaredMethodInternal(JNIEnv* env, jobject javaThis,
jstring name, jobjectArray args) {
ScopedFastNativeObjectAccess soa(env);
...
//1、传入方法名和参数类型,获取目标方法对象
Handle result = hs.NewHandle(
mirror::Class::GetDeclaredMethodInternalfalse >(
soa.Self(),
DecodeClass(soa, javaThis),
soa.Decode(name),
soa.Decode>(args)));
//2、进入隐藏拦截进制判断,这里传入的方法对应的ArtMethod指针和当前线程
if (result == nullptr || ShouldBlockAccessToMember(result->GetArtMethod(), soa.Self())) {
return nullptr;
}
//3、将前面得到Method对象返回
return soa.AddLocalReference(result.Get());
}
通过Android P源码我们知道这个时机要在Class_getDeclaredMethodInternal()
和GetHiddenApiAccessFlags()
之间(包括两者),于是可以选择Hook mirror::Class::GetDeclaredMethodInternal()
方法,虽然它返回的是Method对象,但可以看出调用它的GetArtMethod()
方法就可以取得对应的ArtMethod指针。
要实现这个目标,首先需要确认这两个方法的符号在动态库中存在,而不是被内联了。
找到一台Android 9的手机/模拟器,将system/lib64/libart.so
导出,并使用Hopper Disassembler打开,在Navigate->Exported Symbols
中分别找到这两个方法对应的符号:
于是我们得到以下符号:
shell 代码解读复制代码# art::ObjPtr art::mirror::Class::GetDeclaredMethodInternal<(art::PointerSize)8, false>(art::Thread*, art::ObjPtr, art::ObjPtr, art::ObjPtr >)
# 注意,GetDeclaredMethodInternal方法在不同位数的系统上,符号有所不同,这里以64位为例
_ZN3art6mirror5Class25GetDeclaredMethodInternalILNS_11PointerSizeE8ELb0EEENS_6ObjPtrINS0_6MethodEEEPNS_6ThreadENS4_IS1_EENS4_INS0_6StringEEENS4_INS0_11ObjectArrayIS1_EEEE
# art::mirror::Executable::GetArtMethod()
_ZN3art6mirror10Executable12GetArtMethodEv
有了这两个符号,就可以使用inline HOOK拦截所有的方法反射调用:
c++ 代码解读复制代码//方法对应的符号,C++的方法名会被修饰成对应的符号
#define Str_GetDeclaredMethodInternalApi28 "_ZN3art6mirror5Class25GetDeclaredMethodInternalILNS_11PointerSizeE8ELb0EEENS_6ObjPtrINS0_6MethodEEEPNS_6ThreadENS4_IS1_EENS4_INS0_6StringEEENS4_INS0_11ObjectArrayIS1_EEEE"
#define Str_GetArtMethod "_ZN3art6mirror10Executable12GetArtMethodEv"
struct ObjPtr {
uintptr_t reference_;
};
typedef void *(*GetArtMethod)(void *exec);
//1、进行inline hook,proxyGetDeclaredMethodInternalApi28就是代理方法
stub = shadowhook_hook_sym_name("libart.so", Str_GetDeclaredMethodInternalApi28,
(void *) proxyGetDeclaredMethodInternalApi28, nullptr);
//2、hook成功以后,所有反射方法调用,都会进入这里
static ObjPtr proxyGetDeclaredMethodInternalApi28(void *self, ObjPtr klass, ObjPtr name, ObjPtr args) {
SHADOWHOOK_STACK_SCOPE();
//3、调用原GetDeclaredMethodInternal方法,会返回一个ObjPtr
ObjPtr res = SHADOWHOOK_CALL_PREV(proxyGetDeclaredMethodInternalApi28, self, klass, name, args);
//4、res.reference_就是Method对象指针
if (res.reference_ == 0) {
return res;
}
//5、调用GetArtMethod,获得对应的ArtMethod指针
void *artMethod = CallGetArtMethod((void *) res.reference_);
if(artMethod == nullptr){
return res;
}
//6、修改ArtMethod.access_flags
modifyAccessFlags(artMethod);
return res;
}
static void* CallGetArtMethod(void* method){
void *libart = shadowhook_dlopen("libart.so");
if (libart == nullptr) {
return nullptr;
}
//使用shadowhook_dlsym找到GetArtMethod方法指针
void *getArtMethodPtr = shadowhook_dlsym(libart, Str_GetArtMethod);
shadowhook_dlclose(libart);
if(getArtMethodPtr == nullptr){
return nullptr;
}
//调用GetArtMethod
return ((GetArtMethod)getArtMethodPtr)(method);
}
1.1.2、构造ArtMethod结构获取access_flags_
指针
下一个的问题在于,拿到ArtMethod指针后,我们怎么实现modifyAccessFlags()方法,从而修改ArtMethod.access_flags
。
c++ 代码解读复制代码class ArtMethod{
protected:
GcRoot declaring_class_;
std::atomicuint32_t > access_flags_;
...
}
根据art/runtime/gc_root.h、object_reference.h看出GcRoot
的继承关系中实际最终只包含一个uint32_t
大小的成员reference_
,因此实际GcRoot
的大小等于一个uint32_t
的大小。
因此我们按照ArtMethod的内存结构构造一个ArtMethod类,就可以将前面得到ArtMethod指针强转,从而访问其成员变量,即:
c++ 代码解读复制代码//1、通过模拟ArtMethod定义的类,只关心access_flags_前的成员,后面的不需要定义出来
class ArtMethod{
public:
//2、GcRoot等价与uint32_t
uint32_t declaring_class_;
//3、定义access_flags_,从而可以通过指针访问其成员
std::atomic<uint32_t> access_flags_;
//4、其他部分不需要定义,因为我们只用到access_flags_
//...
};
这个技巧在后续的绕过实现里面我们还会用到。
最终,modifyAccessFlags()
方法实现如下:
c++ 代码解读复制代码static void modifyAccessFlags(void* artMethod){
if(artMethodPtr == nullptr){
return;
}
auto artMethod = (ArtMethod*)artMethodPtr;
//将access_flags_高29、30位置位0
//0x9FFFFFFF等于0b10011111111111111111111111111111
artMethod->access_flags_ &= 0x9FFFFFFF;
}
至此,当我们在Java层调用GetMethod()
方法时,都会被proxyGetDeclaredMethodInternalApi28()
方法拦截并修改其access_flags_
,使得隐藏API的拦截不生效。
为了方便起见,可以进一步使用反射调用隐藏APIVMRuntime.setHiddenApiExemptions("L")
从而绕过所有拦截,这里不赘述。
1.2、Android Q兼容
经测试发现,上述的绕过方案,在Android Q的机子上并没有生效,通过查看Android Q源码发现有以下原因:
-
Android Q中
Class::GetDeclaredMethodInternal()
方法的参数对比Android P变化,因此需要使用Hopper Disassembler找到对应的新的符号,这个大家可以自行查找或从后续提供的Github仓库中查找。 -
使用
shadowhook_dlsym()
找不到GetArtMethod方法指针。这导致无法从获取mirror::Method
对应的ArtMethod指针。 -
Android Q不再使用
ArtMethod.access_flags
的高29、30位表示拦截名单登记,而是它们来标记Api是PublicApi的还是PlatformApi。如果是PublicApi,则不需要拦截。art/libdexfile/dex/modifiers.h
c++代码解读复制代码static constexpr uint32_t kAccPublicApi = 0x10000000; // field, method static constexpr uint32_t kAccCorePlatformApi = 0x20000000; // field, method
总的来说,Android Q上系统源码有所变更,导致原有的绕过方式失效了,但是整体拦截机制和修改ArtMethod.access_flags
的思路仍然没有变化。
1.2.1、ArtMethod.access_flags的定义变化
先看看代码变化的主要部分:
c++ 代码解读复制代码//1、Class::GetDeclaredMethodInternal方法和Android P中的入参不一样了
template bool kTransactionActive>
ObjPtr Class::GetDeclaredMethodInternal(
Thread* self,
ObjPtr klass,
ObjPtr name,
ObjPtr> args,
const std::function& fn_get_access_context) {
...
for (auto& m : h_klass->GetDirectMethods(kPointerSize)) {
...
//2、遍历找到目标方法,使用ShouldDenyAccessToMember()判断是否隐藏API
bool m_hidden = hiddenapi::ShouldDenyAccessToMember(&m, fn_get_access_context, access_method);
...
}
...
//3、把ArtMethod转成Method作为结果返回
return result != nullptr
? Method::CreateFromArtMethod(self, result)
: nullptr;
}
c++ 代码解读复制代码//4、ShouldDenyAccessToMember返回true表示是隐藏API
template<typename T>
inline bool ShouldDenyAccessToMember(T* member,
const std::function& fn_get_access_context,
AccessMethod access_method){
...
//5、仍然是获取方法的access_flags
const uint32_t runtime_flags = GetRuntimeFlags(member);
//6、kAccPublicApi为0x10000000,这里是判断access_flags的高29位是否为1,是则仍然是公开方法,不是隐藏API
if ((runtime_flags & kAccPublicApi) != 0) {
return false;
}
...
}
ALWAYS_INLINE inline uint32_t GetRuntimeFlags(ArtMethod* method)
REQUIRES_SHARED(Locks::mutator_lock_) {
...
return method->GetAccessFlags() & kAccHiddenapiBits;
...
}
在Android Q中,隐藏API拦截的核心方法是hiddenapi::ShouldDenyAccessToMember
,其内部仍然首先判断了ArtMethod.access_flags
,只是判断条件不同。
因此,我们只需要将ArtMethod.access_flags
的高29位总是置为1即可,于是modifyAccessFlags()
修改如下:
c++ 代码解读复制代码static constexpr uint32_t kAccPublicApi = 0x10000000;
static void modifyAccessFlags(void* artMethodPtr){
if(artMethodPtr == nullptr){
return;
}
auto artMethod = (ArtMethod*)artMethodPtr;
if(android_get_device_api_level() == __ANDROID_API_P__) {
//Android P
artMethod->access_flags_ &= 0x9FFFFFFF;
}else{
//Android Q及以上
artMethod->access_flags_ |= kAccPublicApi;
}
}
1.2.2、Unsafe获取ArtMethod指针
另一个困难是无法通过GetArtMethod方法()
获得ArtMethod指针了。
这里提供另外一个技巧,由于Java层的Method对象和mirror::Method
实际是共享一份内存的:
c++ 代码解读复制代码class MANAGED Method : public Executable {
...
};
// C++ mirror of java.lang.reflect.Executable.
class MANAGED Executable : public AccessibleObject {
...
private:
...
uint64_t art_method_;
...
}
java 代码解读复制代码public abstract class Executable extends AccessibleObject
implements Member, GenericDeclaration {
...
private long artMethod;
...
}
因此,我们可以通过Unsafe来获取artMethod
在Executable中的偏移,具体实现如下:
kotlin 代码解读复制代码private val artMethodOffset = lazy {
runCatching {
val unsafe = Unsafe::class.java.getDeclaredMethod("getUnsafe").invoke(null) as Unsafe
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
//获取artMethod在Executable中的偏移,等价于art_method_在mirror::Executable中的偏移
unsafe.objectFieldOffset(Executable::class.java.getDeclaredField("artMethod"))
} else {
-1
}
}.getOrDefault(-1)
}
使用这个偏移,我们就可以再次从mirror::Method
对象中取得ArtMethod指针了。
具体实现如下:
c++ 代码解读复制代码static ObjPtr proxyGetDeclaredMethodInternalApi29(void *self, ObjPtr klass, ObjPtr name, ObjPtr args,
std::function const &fn_get_access_context) {
SHADOWHOOK_STACK_SCOPE();
ObjPtr res = SHADOWHOOK_CALL_PREV(proxyGetDeclaredMethodInternalApi29, self, klass, name, args,
fn_get_access_context);
if (res.reference_ == 0) {
return res;
}
//1、gArtMethodOffset是前面获取到的偏移,传到了native层来。加上偏移量,就存储着ArtMethod指针。
auto *artMethodPtr = (uintptr_t *) ((uintptr_t) res.reference_ + gArtMethodOffset);
//2、读取偏移后地址上的值,就是ArtMethod指针
auto artMethod = (void*)*artMethodPtr;
//3、修改AccessFlags
modifyAccessFlags(artMethod);
return res;
}
后续Hook流程和Android P一致,这里不赘述。
1.4、Android S兼容
经测试发现,该方法在Android S(12)上失效了。
原因是Class::GetDeclaredMethodInternal()
又发生了变化:
c++ 代码解读复制代码template
ObjPtr Class::GetDeclaredMethodInternal(
Thread* self,
ObjPtr klass,
ObjPtr name,
ObjPtr> args,
const std::function& fn_get_access_context) {
...
}
对比Android Q则为art::mirror::Class::GetDeclaredMethodInternal<(art::PointerSize)8, false>
,少了一个template参数。
只需要使用Hopper Disassembler查找该方法对应的符号,就可以重新hook成功。
1.3、Android T兼容
继续测试发现,该方法在Android T(13)上又失效了😂。
断点发现是由于Hook Class::GetDeclaredMethodInternal()
方法后,使用反射调用GetMthod()
方法时,代理方法没有被调用,可能这个方法实际被内联了。
art/runtime/native/java_lang_Class.cc
c++ 代码解读复制代码static jobject Class_getDeclaredMethodInternal(JNIEnv* env, jobject javaThis,
jstring name, jobjectArray args) {
...
Handle result = hs.NewHandle(
mirror::Class::GetDeclaredMethodInternal(
soa.Self(),
klass,
soa.Decode(name),
soa.Decode>(args),
GetHiddenapiAccessContextFunction(soa.Self())));
//1、可以尝试Hook ShouldDenyAccessToMember方法,通用可以拿到ArtMethod指针
if (result == nullptr || ShouldDenyAccessToMember(result->GetArtMethod(), soa.Self())) {
return nullptr;
}
return soa.AddLocalReference(result.Get());
}
通过观察源码,可以尝试选择Hook ShouldDenyAccessToMember
方法,同样可以拿到ArtMethod指针。测试下来,顺利Hook成功。
至此Android(P-U) 版本都成功实现了隐藏API绕过,但是从修改AccessFlags的方案来看,实现繁琐且兼容性存在很大的问题,系统代码微小变更就很可能造成方案的失效。
不过在这个过程中,我们积累了一些技巧和熟悉了工具的使用,使得理解后续Hook方案变得更加简单。
2、Hook核心方法
在Android Hook - 隐藏API拦截机制中提过,拦截机制的核心方法是GetMemberAction(),既然有Hook方法的能力,那么Hook这个方法,使得它总是返回kAllow
,就不可以了吗。
2.1、Android P实现
c++ 代码解读复制代码//1、GetMemberAction是一个内联方法,动态库中没有对应的符号
template<typename T>
inline Action GetMemberAction(T* member,
Thread* self,
std::function<bool(Thread*)> fn_caller_is_trusted,
AccessMethod access_method)
...
//2、detail::GetMemberActionImpl不是内联的,可以Hook它
return detail::GetMemberActionImpl(member, api_list, action, access_method);
}
可惜GetMemberAction
是一个内联方法,但是我们马上找到detail::GetMemberActionImpl()
,因此可以Hook它:
c++ 代码解读复制代码//1、GetMemberActionImpl对应的符号,可以使用工具获得
#define Str_GetMemberActionImplApi28 "_ZN3art9hiddenapi6detail19GetMemberActionImplINS_9ArtMethodEEENS0_6ActionEPT_NS_20HiddenApiAccessFlags7ApiListES4_NS0_12AccessMethodE"
//2、Hook GetMemberActionImpl方法
stub = shadowhook_hook_sym_name("libart.so", Str_GetMemberActionImplApi28, (void *) proxyGetMemberActionImpl, nullptr);
static Action proxyGetMemberActionImpl(void *,ApiList ,Action ,AccessMethod ) {
SHADOWHOOK_STACK_SCOPE();
//3、使得GetMemberActionImpl总是返回kAllow
return kAllow;
}
2.2、Android Q兼容
同理,从Android Q开始,核心方法变为ShouldDenyAccessToMember
,因此同理我们可以选择Hook ShouldDenyAccessToMemberImpl()
,使得它总是返回false。
c++ 代码解读复制代码template<typename T>
inline bool ShouldDenyAccessToMember(T* member,
const std::function& fn_get_access_context,
AccessMethod access_method){
...
//Hook这个方法,使得它总是返回flase,表示不拦截。
return detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method);
}
可以看出Hook核心方法比修改AccessFlags简单很多,读者可以自行实现。
3、修改EnforcementPolicy
c++ 代码解读复制代码inline Action GetActionFromAccessFlags(HiddenApiAccessFlags::ApiList api_list) {
...
//获取隐藏名单处理策略,如果是不检查,那么全部返回允许
EnforcementPolicy policy = Runtime::Current()->GetHiddenApiEnforcementPolicy();
if (policy == EnforcementPolicy::kNoChecks) {
// Exit early. Nothing to enforce.
return kAllow;
}
...
}
在分析Android P源码时我们已经知道,当Runtime.hidden_api_policy_
为kNoChecks
时,就不会进行拦截。
因此我们的目标是修改Runtime.hidden_api_policy_
的值,要做到这点需要以下条件:
- 获得
Runtime
对象的指针。 - 获得
Runtime.hidden_api_policy_
的指针或者某个可以修改它的方法。
3.1、获取Runtime*
我们知道在JNI方法调用时,会传入JNIEnv
指针,并通过其GetJavaVM()
方法可以获得一个JavaVM
指针,代表一个虚拟机实例。
c++ 代码解读复制代码extern "C"
JNIEXPORT jboolean JNICALL
Java_com_muye_hook_hiddenapi_BypassByModifyEnforcementPolicy_bypassNative(JNIEnv *env,
jobject thiz) {
JavaVM *vm;
//1、获得JavaVM*
env->GetJavaVM(&vm);
...
}
libnativehelper/include_jni/jni.h
c++ 代码解读复制代码/*
* C++ version.
*/
struct _JavaVM {
const struct JNIInvokeInterface* functions;
}
...
c++ 代码解读复制代码//2、实际是JavaVMExt对象,继承自JavaVM,并且它的第一个成员就是Runtime*
class JavaVMExt : public JavaVM {
...
Runtime* const runtime_;
...
}
从上面的源码关系可以看出,我们获得的虚拟机实例,类型其实是JavaVMExt,并且它的第一个成员就是Runtime*
,因此它实际的内存结构是这样的:
c++ 代码解读复制代码class JavaVMExt {
public:
//从JavaVM中继承的
void *functions;
//Runtime*
void *runtime;
};
和前面的技巧一样,我们模拟真实的JavaVMExt,从而构造出JavaVMExt
内存结构,就可以将指针强转后访问其成员变量JavaVMExt.runtime
,这就是Runtime
对象的指针。
3.2、修改Runtime.hidden_api_policy_
通过搜索源码和反编译发现Runtime.hidden_api_policy_
没有方法可以直接/间接修改(SetHiddenApiEnforcementPolicy()
方法被内联了)。
因此我们故技重施,同样去模拟Runtime
的内存结构,从而可以访问和修改Runtime.hidden_api_policy_
。
但是实践发现,Runtime的类结构很复杂,成员变量相当的多,要准确构造其内存结构并不容易,而且系统版本差异还不小。
c++ 代码解读复制代码class Runtime {
//1、前面有很多成员变量
...
uint64_t callee_save_methods_[kCalleeSaveSize];
...
//2、这个值我们知道,等价于ApplicationInfo.targetSdkVersion
uint32_t target_sdk_version_;
...
//3、目标变量,后面还有很多成员变量
EnforcementPolicy hidden_api_policy_;
...
}
怎么才能比较稳定地模拟Runtime的内存结构呢?
通过观察,我们发现存在成员变量Runtime.target_sdk_version_
,这个变量的值等于JAVA层获取的ApplicationInfo.targetSdkVersion
。
因此,我们可以遍历Runtime*
指向的内存,尝试找到值为ApplicationInfo.targetSdkVersion
的内存地址,就认为这个地址指向Runtime.target_sdk_version_
,进而我们只需要准确定义从Runtime.target_sdk_version_
到Runtime.hidden_api_policy_
之间的成员即可,即:
c++ 代码解读复制代码struct PartialRuntime{
uint32_t target_sdk_version_;
...
EnforcementPolicy hidden_api_policy_;
}
这个技巧可以增加我们对Runtime内存结构模拟的准确性和稳定性,但是仍然存在很高的兼容性风险,因为厂商也可能修改Runtime
的结构。
我们当然可以增加更多的校验和崩溃保护,在没有更好的方案的情况下,有时候是一个不得已的选择,尤其在非常规优的优化场景,可能会使用这种方式来访问Runtime*
。
总的来说,该方案是可行的,并且笔者在模拟器上顺利兼容了Android P-U。
4、绕过调用者检查
隐藏API拦截机制中,最复杂的逻辑就是遍历堆栈找到首个调用反射相关方法的应用方法,我们尝试在这个逻辑中找到突破口。
4.1、修改ClassLoader
如果我们可以把应用方法伪装成系统方法,那么这个方法就可以调用隐藏API了,主要的方式是修改类和Dex对应的ClassLoader。
4.1.1、Android P实现
c++ 代码解读复制代码ALWAYS_INLINE
inline bool IsCallerTrusted(ObjPtr caller,
ObjPtr caller_class_loader,
ObjPtr caller_dex_cache)
REQUIRES_SHARED(Locks::mutator_lock_) {
//1、如果classloader为null,则认为是boot class loader,因此是来自系统的调用
if (caller_class_loader.IsNull()) {
return true;
}
...
}
Android P源码中,如果否某个类的ClassLoader为null(也就是BoostClassLoader),就认为是由boot class loader加载的,可以信任。
因此,我们首先构造一个工具类SetAllHiddenApiExemptions,这个工具类会反射调用VMRuntime.setHiddenApiExemptions("L")
:
java 代码解读复制代码public class SetAllHiddenApiExemptions {
public static boolean invoke(){
try{
Class> runtimeClass = Class.forName("dalvik.system.VMRuntime");
//get VMRuntime instance
Method getRuntimeMethod = runtimeClass.getDeclaredMethod("getRuntime");
getRuntimeMethod.setAccessible(true);
Object runtime = getRuntimeMethod.invoke(null);
//call VMRuntime.setHiddenApiExemptions("L")
Method setHiddenApiExemptionsMethod = runtimeClass.getDeclaredMethod("setHiddenApiExemptions", String[].class);
setHiddenApiExemptionsMethod.setAccessible(true);
setHiddenApiExemptionsMethod.invoke(runtime, new Object[]{new String[]{"L"}});
return true;
}catch (Throwable t){
return false;
}
}
然后手动把SetAllHiddenApiExemptions的Class对象的Class.classLoader
置为null,就可以反射调用它的成员方法,并且成员方法中可以调用隐藏API。
java 代码解读复制代码//1、反射获取我们的Class对象SetAllHiddenApiExemptions
val clazz = Class.forName("com.muye.hook.hiddenapi.SetAllHiddenApiExemptions")
//2、主动把Class.classLoader置为null
Class::class.java.getDeclaredField("classLoader").apply {
isAccessible = true
set(clazz, null)
}
//3、调用SetAllHiddenApiExemptions.invoke()方法,方法内反射调用VMRuntime.setHiddenApiExemptions()
clazz.getDeclaredMethod("invoke").invoke(null)
4.1.2、Android Q兼容
上述方法在Android Q及之后版本失效了,原因是不再简单校验classLoader是否为null。
c++ 代码解读复制代码template <typename T>
bool ShouldDenyAccessToMember(T* member,
const std::function& fn_get_access_context,
AccessMethod access_method) {
...
//1、获取调用者上下文信息
const AccessContext caller_context = fn_get_access_context();
...
//2、上下文中有Domain属性,用于区分调用来源是应用还是系统
switch (caller_context.GetDomain()) {
case Domain::kApplication: {
...
//3、调用者是应用,进行隐藏API拦截检查
return detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method);
}
case Domain::kPlatform: {
//4、调用者是系统,可以调用
...
}
...
}
}
- Android Q获取调用者上下文AccessContext,将其分为是
kApplication
还是kPlatform
,对kApplication
进一步进行检查。
c++ 代码解读复制代码//hidden_api.cc
//1、反射时使用GetReflectionCallerAccessContext获取AccessContext
hiddenapi::AccessContext GetReflectionCallerAccessContext(Thread* self)
REQUIRES_SHARED(Locks::mutator_lock_) {
...
//2、获取调用者对应的Class
ObjPtr caller =
(visitor.caller == nullptr) ? nullptr : visitor.caller->GetDeclaringClass();
//3、根据该Class构造AccessContext
return caller.IsNull() ? AccessContext(/* is_trusted= */ true) : AccessContext(caller);
}
//art/runtime/hidden_api.h
explicit AccessContext(ObjPtr klass)
REQUIRES_SHARED(Locks::mutator_lock_)
: klass_(klass),
dex_file_(GetDexFileFromDexCache(klass->GetDexCache())),
//4、AccessContext构造方法中调用ComputeDomain计算Domain
domain_(ComputeDomain(klass, dex_file_)) {}
static Domain ComputeDomain(ObjPtr class_loader, const DexFile* dex_file) {
...
//5、获取DexFile.hiddenapi_domain_
return dex_file->GetHiddenapiDomain();
}
//art/libdexfile/dex/dex_file.h
hiddenapi::Domain GetHiddenapiDomain() const { return hiddenapi_domain_; }
//hidden_api.cc
void InitializeDexFileDomain(const DexFile& dex_file, ObjPtr class_loader) {
//6、DexFile.hiddenapi_domain_是在加载的时候,根据Dex文件所在的位置确定的
Domain dex_domain = DetermineDomainFromLocation(dex_file.GetLocation(), class_loader);
if (IsDomainMoreTrustedThan(dex_domain, dex_file.GetHiddenapiDomain())) {
dex_file.SetHiddenapiDomain(dex_domain);
}
}
//hidden_api.cc
static Domain DetermineDomainFromLocation(const std::string& dex_location,
ObjPtr class_loader) {
//7、根据Dex文件所在的目录,确定Domain
...
//8、如果加载Dex的class_loader是空,那么Domain也是kPlatform
if (class_loader.IsNull()) {
return Domain::kPlatform;
}
return Domain::kApplication;
}
- 通过上文的源码分析我们知道,前面方法失效的原因是系统判断的是加载DexFile的ClassLoader是否为空,而不是判断某个类的ClassLoader是否为空。
我们能否让BoostClassLoader来加载我们的Dex文件呢?
java 代码解读复制代码@Deprecated
public DexFile(String fileName) throws IOException {
this(fileName, null, null);
}
DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements)
throws IOException {
...
}
DexFile.java
中有这样一个废弃的构造方法,只需要传入Dex文件路径,系统就会使用BoostClassLoader去加载这个Dex文件。
于是,绕过流程如下:
-
按照如图的路径找到
SetAllHiddenApiExemptions.class
文件,主要SetAllHiddenApiExemptions要使用JAVA实现,使用Kotlin不会有这个中间产物。 -
使用Android build-tools中d8工具,把class文件编译成dex文件。
shell代码解读复制代码/Users/XXX/Library/Android/sdk/build-tools/34.0.0/d8 SetAllHiddenApiExemptions.class
-
把生成的classes.dex文件放入工程raw目录,或者直接把文件进行base64转成字符在运行时生成dex文件。
-
最后,加载这个Dex文件并在反射调用
SetAllHiddenApiExemptions.invoke()
。kotlin代码解读复制代码//1、使用废弃的构造方法,使得Dex文件被BoostClassloader家长 val dexFile = DexFile(filePath) //2、加载SetAllHiddenApiExemptions类 val clazz = dexFile.loadClass("com.muye.hook.hiddenapi.SetAllHiddenApiExemptions", null) //3、反射调用invoke()方法,其中可以使用隐藏API clazz?.getDeclaredMethod("invoke")?.invoke(null)
这种绕过方式,目前在AndroidP-U,都是可以成功的。并且是纯JAVA层的修改,兼容性较高,唯一风险是DexFile的废弃构造方法有可能在未来被删除。
4.2、元反射
4.2.1、Android P实现
在Android P源码的分析中,拦截逻辑会反向查找调用栈,从而找到第一个调用Class.class或者java.lang.invoke 包中类的应用方法,进行判断是否应该被拦截。
c++ 代码解读复制代码//1、如果外部第一次调用Class.class或反射方法的地方,是来源于platform DEX file,那么认为是系统调用的
static bool IsCallerTrusted(Thread* self) REQUIRES_SHARED(Locks::mutator_lock_) {
//2、回溯JAVA堆栈,找到第一个不是来自java.lang.Class和java.lang.invoke的栈
struct FirstExternalCallerVisitor : public StackVisitor {
//3、访问当前栈帧,返回true说明要继续向上查找,caller指针用于记录查找结果
bool VisitFrame() REQUIRES_SHARED(Locks::mutator_lock_) {
ArtMethod *m = GetMethod();
if (m == nullptr) {
//4、native线程调用,那么判断为非系统的调用,不继续查找
caller = nullptr;
return false;
} else if (m->IsRuntimeMethod()) {
// 5、判断是否虚拟机内部方法,是则继续查找
return true;
}
ObjPtr declaring_class = m->GetDeclaringClass();
//6、如果是classloader是BootStrapClassLoad,也就是classLoader为空
if (declaring_class->IsBootStrapClassLoaded()) {
//6.1、如果是Class类,那么向上再找
if (declaring_class->IsClassClass()) {
return true;
}
//6.2、检查 java.lang.invoke 包中的类。在撰写本文时,感兴趣的类是 MethodHandles 和 MethodHandles.Lookup,但这有可能发生变化,因此保守地覆盖整个包。注意 java.lang.invoke 中的静态初始化器是允许的,不需要进一步的堆栈检查。
//也就是说,如果包名为java.lang.invoke,那么继续向上查找
ObjPtr lookup_class = mirror::MethodHandlesLookup::StaticClass();
if ((declaring_class == lookup_class || declaring_class->IsInSamePackage(lookup_class))
//并且不是构造方法或静态方法
&& !m->IsClassInitializer()) {
return true;
}
}
//7、如果classloader不是BootStrapClassLoad,那么此时caller就为第一个调用反射的类
//如果classloader是BootStrapClassLoad,但又不是Class或者在java.lang.invoke包内,那么也找到了
caller = m;
return false;
}
ArtMethod* caller;
};
FirstExternalCallerVisitor visitor(self);
//根据调用栈向上查找反射入口
visitor.WalkStack();
//8、如果找到调用反射的方法,那么进一步检查它是否可信,找不到说明来自native,直接不可信
return visitor.caller != nullptr &&
hiddenapi::IsCallerTrusted(visitor.caller->GetDeclaringClass());
}
因此,如果调用者如果是系统类就不会被拦截,而反射相关的类是在包名java.lang.reflect
下的,也就是说使用反射去调用反射,那么按照这个逻辑,就会找到首个调用反射的类是java.lang.reflect
下的相关类,从而被认为是系统api在调用。
这种使用反射来调用反射的方法被称为元反射。
根据这个逻辑,构造一个元反射方法如下:
java 代码解读复制代码private val getDeclaredMethodMethod = lazy {
//1、反射获取Class.getDeclaredMethod()
runCatching {
Class::class.java.getDeclaredMethod(
"getDeclaredMethod",
String::class.java,
arrayOf>()::class.java
)
}.getOrNull()
}
private fun getMethod(
targetClass: Class<*>,
methodName: String,
parameterTypes: Array>? = null,
): Method? = kotlin.runCatching {
//2、反射调用Class.getDeclaredMethod()
getDeclaredMethodMethod.value?.invoke(
targetClass,
methodName,
*parameterTypes
) as Method?
}.getOrNull()
使用构造出来的元反射getMethod()
,就可以访问隐藏API。
这个实现比较巧妙,但是只在Android P到Android Q生效,在Android R中Google对这个问题进行了修复。
4.2.2、Android R兼容
首先来看,为什么在Android R上原来的方法不可行:
c++ 代码解读复制代码 struct FirstExternalCallerVisitor : public StackVisitor {
...
bool VisitFrame() override REQUIRES_SHARED(Locks::mutator_lock_) {
ArtMethod *m = GetMethod();
if (m == nullptr) {
caller = nullptr;
return false;
}
...
ObjPtr declaring_class = m->GetDeclaringClass();
if (declaring_class->IsBootStrapClassLoaded()) {
...
//1、增加了逻辑。对于java.lang.reflect包名,继续向上查找
ObjPtr proxy_class = GetClassRoot();
if (declaring_class->IsInSamePackage(proxy_class) && declaring_class != proxy_class) {
if (Runtime::Current()->isChangeEnabled(kPreventMetaReflectionBlacklistAccess)) {
return true;
}
}
}
caller = m;
return false;
}
ArtMethod* caller;
};
...
//2、这里的逻辑也修改了,如果caller是null,那么会构造一个AccessContext(true)表明调用者可以信任
ObjPtr caller = (visitor.caller == nullptr)
? nullptr : visitor.caller->GetDeclaringClass();
return caller.IsNull() ? hiddenapi::AccessContext(/* is_trusted= */ true)
: hiddenapi::AccessContext(caller);
原来是新增了对java.lang.reflect
包名的向上查找逻辑,可以说是比较针对性的修改。
因此只能寻找其他突破口。
注意到:Android R中,如果caller为空,那么会构建hiddenapi::AccessContext(/* is_trusted= */ true)
,从而获得kCorePlatform权限。
因此,我们可以新启动一个Native线程,把Native AttachCurrentThread()
后,才可以调用JNI方法,并且此时根据堆栈查找的逻辑,找到的caller就会为空。从而绕过拦截。
实现逻辑如下:
c++ 代码解读复制代码extern "C"
JNIEXPORT jboolean JNICALL
Java_com_muye_hook_hiddenapi_MetaReflectApi30_bypassNative(JNIEnv *env, jclass clazz) {
JavaVM *vm;
env->GetJavaVM(&vm);
//1、启动native线程,传入vm作为参数
auto f = std::async(std::launch::async, [&]() ->bool {
JNIEnv *env = nullptr;
//2、attach,从而可以调用jni,此时调用起始caller就是null
vm->AttachCurrentThread(&env, nullptr);
//3、通过jni,反射调用VMRuntime的setHiddenApiExemptions方法,将所有API都加入到黑名单中
bool flag = setApiBlacklistExemptions(env);
vm->DetachCurrentThread();
return flag;
});
return f.get();
}
然而需要注意,这个逻辑在Android P、Q是不生效的,因为Android P、Q中当caller为null时,反而会被拦截。
该方案的缺点为pthread创建线程使caller为null的方案将受限。
4.3、JNI_OnLoad
这个方法思路来自安卓hiddenapi访问绝技,在我们加载动态库时,会调用其定义的JNI_OnLoad()
方法用于进行一些初始化,如果在JNI_OnLoad()方法中调用隐藏API,那么就可以绕过检测。
为了验证这个思路,我们尝试断点在JNI_OnLoad()
看看:
可以看出,遍历堆栈找到的第一个JAVA方法是Runtime.nativeLoad()
,这是一个系统方法,因此是被系统信任的,从而不会进行拦截。
我们可以将这个方法单独打入一个动态库,那么在加载这个动态库就等于开启绕过隐藏API检测机制。
5、修改豁免名单
在Android Hook - 隐藏API拦截机制提到,想要在JAVA层反射调用VMRuntime.setHiddenApiExemptions()
方法修改豁免名单,是一个鸡生蛋蛋生鸡的问题。
但是这个方法有对应的JNI方法:
c++ 代码解读复制代码static void VMRuntime_setHiddenApiExemptions(JNIEnv* env,
jclass,
jobjectArray exemptions) {
....
Runtime::Current()->SetHiddenApiExemptions(exemptions_vec);
}
在动态库中我们也顺利找到这个方法对应的符号:
因此我们直接使用shadowhook_dlsym()
找到这个方法指针并且调用即可。
c++ 代码解读复制代码void *libart = shadowhook_dlopen("libart.so");
//1、通过shadowhook_dlsym找到方法指针
void *vmRuntimeSetHiddenApiExemptionsPtr = shadowhook_dlsym(libart,Str_VMRuntime_setHiddenApiExemptions);
//3、构造参数,直接调用vmRuntimeSetHiddenApiExemptions()
//...省略调用代码
目前这种方式非常稳定,因为VMRuntime_setHiddenApiExemptions()
从Andorid P~U都没有修改过。
6、Unsafe反射
Unsafe反射是一种使用Unsafe来实现反射调用效果的技巧,具体参考笔者文章Android Hook - Unsafe反射,因此详细的实现方式这里不做介绍。
需要说明的是其核心思路。
Unsafe反射实现隐藏API绕过的核心思路是,通过替换某个我们有权限访问Method对象对应的底层指针为目标隐藏API的底层指针,从而得到隐藏API对应的Method对象。
这种方式获取Method对象的过程,不需要调用getMethod()/getDeclaredMethod()
,因此更不会进入到隐藏API的检测流程了,从而完全避免系统对隐藏API的检查。
并且是纯JAVA实现,不需要依赖额外的Hook能力,依赖的是Class等JAVA层API的稳定性,因此稳定性也是比较高的,官方很难封禁。
三、总结
1、方案对比
本文详细列举绕过隐藏API的各种方案,并且所有方案经过自测都兼容Android P-U的所有版本。
正如Android Hook - 隐藏API拦截机制提到的,绕过的方式有很多,通过阅读源码、熟练使用工具和掌握文中提到的技巧,相信大家也能找到更多的绕过方式,而这比了解方案本身更加重要。
接下来对本文列举的所有方案进行比较,分别从实现复杂度、稳定性和对外部依赖(尤其是Inline Hook的依赖)方面进行评价。
绕过方案 | 稳定性 | 外部依赖 | 复杂度 | 参考文章 |
---|---|---|---|---|
修改AccessFlags | 差。系统版本差异大,兼容性差。 | Inline Hook | 高 | 突破Android P(Preview 1)对调用隐藏API限制的方法 |
Hook核心方法 | 高。核心方法符号变化不大。 | Inline Hook | 低 | |
修改EnforcementPolicy | 差。系统版本差异大,兼容性差。需要构造Runtime类内存结构。 | 无 | 高 | 一种绕过Android P对非SDK接口限制的简单方法 |
修改ClassLoader | 中。Dex构造函数可能被彻底废弃。 | 无 | 中 | Android 11 绕过反射限制 |
元反射 | 中。依赖于系统代码的逻辑漏洞。 | 无 | 中 | 另一种绕过 Android P以上非公开API限制的办法、Android API restriction bypass for all Android Versions |
JNI_OnLoad | 高。JNI_OnLoad一直由系统调用。 | 无 | 低 | 安卓hiddenapi访问绝技 |
修改豁免名单 | 高。VMRuntime_setHiddenApiExemptions长期没有变更。 | dlsym绕过 | 低 | |
Unsafe反射 | 高。纯JAVA实现。 | 无 | 高 | 一个通用的纯 Java 安卓隐藏 API 限制绕过方案 |
2、技巧总结
简单总结一下我们使用过的技巧:
- Inline Hook的使用。
- dlfcn的使用。
- 模拟系统类的内存结构。根据源码模拟,如果比较复杂例如Runtime,那么可以只模拟一部分。
- Unsafe可以利用JAVA层Method对象和Native层mirror:Method对象的内存关系,从而读写Method对象的成员。
四、写在最后
1、源码下载
2、免责声明
本文涉及的代码,旨在展示和描述方案的可行性,可能存bug或者性能问题。
不建议未经修改验证,直接使用于生产环境。
3、转载声明
本文欢迎转载,转载请注明出处。
4、留言讨论
你是否也在现实开发中遇到类似的场景或者应用,是否有更多的意见和想法,欢迎留言一起学习讨论。
5、欢迎关注
如果你对更多的Android Hook开发技巧、思路感兴趣的,欢迎关注我的栏目。
后续将提供更多优质内容,硬核干货。
评论记录:
回复评论: