Android Hook - 隐藏API绕过实践

本文是隐藏API绕过的实践篇,在阅读本文前请先阅读Android Hook - 隐藏API拦截机制,了解Android P隐藏API拦截机制在源码层面的细节。

本文旨在提供介绍绕过隐藏API的各种方式,学习常见Hook技巧和工具的使用。

一、背景

1、隐藏API拦截原理

根据Android Hook - 隐藏API拦截机制的分析,隐藏API拦截机制分别有四个关键的检查位置,即目标方法AccessFlags、系统EnforcementPolicy、调用栈检查、豁免名单检查。

本文将逐一介绍每个关键位置可以使用Hook方式绕过的思路,建议结合源码分析查看。

我们的目标是可以实现调用VMRuntime.setHiddenApiExemptions("L"),从而等价于把所有方法加入到豁免名单。

2、实践前准备

在动手实践前,需要做一些准备:

  1. 保证Android studio可以使用LLDB。可以使用lldb来Debug和反编译。

  2. MAC安装Hopper Disassembler软件,Windows安装IDA 。我们将使用反编译软件,确认动态库中的符号是否存在。这两者都需要付费,但是有试用期。如果不使用软件,可以使用elfreader等脚本代替。

  3. 了解Inline Hook 。部分绕过实现依赖于Inline Hook,如果你从来没有了解过,那么可以把它当做可以Hook动态库指定方法的工具,只需要知道怎么使用而不必去深究其原理。

    1. 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逻辑
      }
    2. dlfcn绕过

      1. dlfcn指dlopendlsymdlclose系列方法,用于找到动态库中指定方法的指针,从而可以调用这个方法。

      2. 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<mirror::Method> result = hs.NewHandle(
      mirror::Class::GetDeclaredMethodInternal<kRuntimePointerSize, false>(
          soa.Self(),
          DecodeClass(soa, javaThis),
          soa.Decode<mirror::String>(name),
          soa.Decode<mirror::ObjectArray<mirror::Class>>(args)));
  //2、进入隐藏拦截进制判断,这里传入的方法对应的ArtMethod指针和当前线程
  if (result == nullptr || ShouldBlockAccessToMember(result->GetArtMethod(), soa.Self())) {
    return nullptr;
  }
  //3、将前面得到Method对象返回
  return soa.AddLocalReference<jobject>(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::Method> art::mirror::Class::GetDeclaredMethodInternal<(art::PointerSize)8, false>(art::Thread*, art::ObjPtr<art::mirror::Class>, art::ObjPtr<art::mirror::String>, art::ObjPtr<art::mirror::ObjectArray<art::mirror::Class> >)
# 注意,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

观察源码art/runtime/art_method.h:

c++ 复制代码
class ArtMethod{
protected:
   GcRoot<mirror::Class> declaring_class_;
   std::atomic<std::uint32_t> access_flags_;
  ...
}

根据art/runtime/gc_root.hobject_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源码发现有以下原因:

  1. Android Q中Class::GetDeclaredMethodInternal()方法的参数对比Android P变化,因此需要使用Hopper Disassembler找到对应的新的符号,这个大家可以自行查找或从后续提供的Github仓库中查找。

  2. 使用shadowhook_dlsym()找不到GetArtMethod方法指针。这导致无法从获取mirror::Method对应的ArtMethod指针。

  3. 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的定义变化

先看看代码变化的主要部分:

art/runtime/mirror/class.cc

c++ 复制代码
//1、Class::GetDeclaredMethodInternal方法和Android P中的入参不一样了
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) {
  ...
  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<kPointerSize, kTransactionActive>(self, result)
      : nullptr;
}

art/runtime/hidden_api.h

c++ 复制代码
//4、ShouldDenyAccessToMember返回true表示是隐藏API
template<typename T>
inline bool ShouldDenyAccessToMember(T* member,
                                     const std::function<AccessContext()>& 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实际是共享一份内存的:

art/runtime/mirror/method.h

c++ 复制代码
class MANAGED Method : public Executable {
 ...
};

// C++ mirror of java.lang.reflect.Executable.
class MANAGED Executable : public AccessibleObject {
  ...
 private:
  ...
  uint64_t art_method_;
  ...
}

Executable.java

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<AccessContext> 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()又发生了变化:

art/runtime/mirror/class.cc

c++ 复制代码
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) {
  ...
}

对比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<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())));
  //1、可以尝试Hook ShouldDenyAccessToMember方法,通用可以拿到ArtMethod指针
  if (result == nullptr || ShouldDenyAccessToMember(result->GetArtMethod(), soa.Self())) {
    return nullptr;
  }
  return soa.AddLocalReference<jobject>(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。

art/runtime/hidden_api.cc

c++ 复制代码
template<typename T>
inline bool ShouldDenyAccessToMember(T* member,
                                     const std::function<AccessContext()>& fn_get_access_context,
                                     AccessMethod access_method){
  ...
  //Hook这个方法,使得它总是返回flase,表示不拦截。
  return detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method);
}

可以看出Hook核心方法比修改AccessFlags简单很多,读者可以自行实现。

3、修改EnforcementPolicy

hidden_api.h

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_的值,要做到这点需要以下条件:

  1. 获得Runtime对象的指针。
  2. 获得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;
}
...

art/runtime/jni/java_vm_ext.h

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 的类结构很复杂,成员变量相当的多,要准确构造其内存结构并不容易,而且系统版本差异还不小。

art/runtime/runtime.h

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实现

hidden_api.h

c++ 复制代码
ALWAYS_INLINE
inline bool IsCallerTrusted(ObjPtr<mirror::Class> caller,
                            ObjPtr<mirror::ClassLoader> caller_class_loader,
                            ObjPtr<mirror::DexCache> 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。

art/runtime/hidden_api.cc

c++ 复制代码
template <typename T>
bool ShouldDenyAccessToMember(T* member,
                              const std::function<AccessContext()>& 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<mirror::Class> 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<mirror::Class> 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<mirror::ClassLoader> 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<mirror::ClassLoader> 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<mirror::ClassLoader> 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文件

于是,绕过流程如下:

  1. 按照如图的路径找到SetAllHiddenApiExemptions.class文件,主要SetAllHiddenApiExemptions要使用JAVA 实现,使用Kotlin不会有这个中间产物。

  2. 使用Android build-toolsd8工具,把class文件编译成dex文件。

    shell 复制代码
    /Users/XXX/Library/Android/sdk/build-tools/34.0.0/d8 SetAllHiddenApiExemptions.class
  3. 把生成的classes.dex文件放入工程raw目录,或者直接把文件进行base64转成字符在运行时生成dex文件。

  4. 最后,加载这个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 包中类的应用方法,进行判断是否应该被拦截。

java_lang_Class.cc

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<mirror::Class> 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<mirror::Class> 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<*>>()::class.java
        )
    }.getOrNull()
}

private fun getMethod(
    targetClass: Class<*>,
    methodName: String,
    parameterTypes: Array<Class<*>>? = 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<mirror::Class> declaring_class = m->GetDeclaringClass();
      if (declaring_class->IsBootStrapClassLoaded()) {
        ...
        //1、增加了逻辑。对于java.lang.reflect包名,继续向上查找
        ObjPtr<mirror::Class> proxy_class = GetClassRoot<mirror::Proxy>();
        if (declaring_class->IsInSamePackage(proxy_class) && declaring_class != proxy_class) {
          if (Runtime::Current()->isChangeEnabled(kPreventMetaReflectionBlacklistAccess)) {
            return true;
          }
        }
      }

      caller = m;
      return false;
    }

    ArtMethod* caller;
  };
...
//2、这里的逻辑也修改了,如果caller是null,那么会构造一个AccessContext(true)表明调用者可以信任
ObjPtr<mirror::Class> 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方法:

dalvik_system_VMRuntime.cc

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、源码下载

BypassHiddenApi

2、免责声明

本文涉及的代码,旨在展示和描述方案的可行性,可能存bug或者性能问题。

不建议未经修改验证,直接使用于生产环境。

3、转载声明

本文欢迎转载,转载请注明出处

4、留言讨论

你是否也在现实开发中遇到类似的场景或者应用,是否有更多的意见和想法,欢迎留言一起学习讨论。

5、欢迎关注

如果你对更多的Android Hook开发技巧、思路感兴趣的,欢迎关注我的栏目。

后续将提供更多优质内容,硬核干货。

相关推荐
测试小工匠24 分钟前
移动端自动化环境搭建_Android
android·运维·自动化
二流小码农1 小时前
鸿蒙开发:自定义一个任意位置弹出的Dialog
android·ios·harmonyos
ziix3 小时前
android-sdk 安装脚本
android·android-sdk安装脚本
柯南二号3 小时前
Android 实现悬浮球的功能
android·gitee·悬浮球
我想睡到自然醒₍˄·͈༝·͈˄*₎◞ ̑3 小时前
【Android】View的解析—滑动篇
android·java
chadm3 小时前
android bindService打开失败
android
诸神黄昏EX3 小时前
Android 常用命令和工具解析之内存相关
android·java·开发语言
zhangphil3 小时前
Android Glide load origin Bitmap, Kotlin
android·kotlin·glide
dreamsever3 小时前
Glide源码学习
android·java·学习·glide