Android Hook - 隐藏API绕过实践

1,178 阅读28分钟

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

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


一、背景

1、隐藏API拦截原理

Android隐藏API绕过.drawio.svg

根据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,可以按照官方文档进行依赖配置。通常使用过程如下:

      //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,这就是另外一个话题了。

        通常使用过程如下:

        //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名单,即:

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

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中分别找到这两个方法对应的符号

Android Hook - 隐藏API绕过实践.png

Android Hook - 隐藏API绕过实践2.png

于是我们得到以下符号:

# 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++的方法名会被修饰成对应的符号
#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:

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指针强转,从而访问其成员变量,即:

//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()方法实现如下:

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

    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

//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

//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()修改如下:

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

class MANAGED Method : public Executable {
 ...
};

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

Executable.java

public abstract class Executable extends AccessibleObject
    implements Member, GenericDeclaration {
  ...
  private long artMethod;
  ...
}

因此,我们可以通过Unsafe来获取artMethod在Executable中的偏移,具体实现如下:

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指针了。

具体实现如下:

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

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

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

//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它:

//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

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

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指针,代表一个虚拟机实例。

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++ version.
 */
struct _JavaVM {
    const struct JNIInvokeInterface* functions;
}
...

art/runtime/jni/java_vm_ext.h

//2、实际是JavaVMExt对象,继承自JavaVM,并且它的第一个成员就是Runtime*
class JavaVMExt : public JavaVM {
 ...
 Runtime* const runtime_;
 ...
}

从上面的源码关系可以看出,我们获得的虚拟机实例,类型其实是JavaVMExt,并且它的第一个成员就是Runtime*,因此它实际的内存结构是这样的:

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

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_之间的成员即可,即:

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

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")

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。

//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

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进一步进行检查。
//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文件呢?

@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不会有这个中间产物。 Android Hook - 隐藏API绕过实践4.png

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

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

  4. 最后,加载这个Dex文件并在反射调用SetAllHiddenApiExemptions.invoke()

    //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

//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在调用。

这种使用反射来调用反射的方法被称为元反射

根据这个逻辑,构造一个元反射方法如下:

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上原来的方法不可行:

  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就会为空。从而绕过拦截。

实现逻辑如下:

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()看看:

Android Hook - 隐藏API绕过实践5.png

可以看出,遍历堆栈找到的第一个JAVA方法是Runtime.nativeLoad(),这是一个系统方法,因此是被系统信任的,从而不会进行拦截。

我们可以将这个方法单独打入一个动态库,那么在加载这个动态库就等于开启绕过隐藏API检测机制。


5、修改豁免名单

Android Hook - 隐藏API拦截机制提到,想要在JAVA层反射调用VMRuntime.setHiddenApiExemptions()方法修改豁免名单,是一个鸡生蛋蛋生鸡的问题。

但是这个方法有对应的JNI方法:

dalvik_system_VMRuntime.cc

static void VMRuntime_setHiddenApiExemptions(JNIEnv* env,
                                            jclass,
                                            jobjectArray exemptions) {
  ....
  
  Runtime::Current()->SetHiddenApiExemptions(exemptions_vec);
}

在动态库中我们也顺利找到这个方法对应的符号:

Android Hook - 隐藏API绕过实践3.png

因此我们直接使用shadowhook_dlsym()找到这个方法指针并且调用即可。

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开发技巧、思路感兴趣的,欢迎关注我的栏目。

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