美文网首页
Android jni 方法 hook 的实现方案

Android jni 方法 hook 的实现方案

作者: 头秃到底 | 来源:发表于2024-03-25 22:13 被阅读0次

    简介

    本文主要是简述一下 jni 方法的调用流程,然后讨论下对jni 方法hook的实现方案。

    JNI 即 Java Native Interface,作为Java代码和native代码之间的中间层,方便Java代码调用native api以及native 代码调用Java api。

    以 Android 上Java 代码启动线程为例,调用 Thread.start 方法时,会调到 nativeCreate 进而调用到他的 native peer Thread_nativeCreate,最后创建相应的 pthread。

    那么我们说的jni hook主要做的就是可以修改 Java native method 的 native peer,以上面创建线程为例,hook前,nativeCreate 的 native peer 是 Thread_nativeCreate,通过jni hook,我们可以将native peer改为我们指定的 Thread_nativeCreate_proxy,这样后面调用 nativeCreate 就会执行到 Thread_nativeCreate_proxy

    要实现 jni hook,主要需要做2点:

    1. 修改 native peer 为我们指定的 proxy 方法
    2. 获取原来的方法地址,因为很多时候在proxy方法中都需要调用原方法

    在实现hook之前,我们先来看看jni方法的链接和调用过程。

    jni 方法的链接

    jni 方法链接有两种方式:

    1. 通过 RegisterNatives主动注册
    2. 按照 jni 的规范命名,由虚拟机在运行时自动查找和绑定,Java native method 和 jni native method的命名映射规范可以参考:Resolving Native Method Names

    主动注册的流程

    以Android 12 的代码为例:RegisterNatives 的实现是在 ClassLinker 中,会通过ArtMethod::SetEntryPointFromJni将我们的jni方法地址存储到 ArtMethod 的 data_ 字段中。

    struct PtrSizedFields {
        // Depending on the method type, the data is
        //   - native method: pointer to the JNI function registered to this method
        //                    or a function to resolve the JNI function,
        //   - resolution method: pointer to a function to resolve the method and
        //                        the JNI function for @CriticalNative.
        //   - conflict method: ImtConflictTable,
        //   - abstract/interface method: the single-implementation if any,
        //   - proxy method: the original interface method or constructor,
        //   - other methods: during AOT the code item offset, at runtime a pointer
        //                    to the code item.
        void* data_;
    
        // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
        // the interpreter.
        void* entry_point_from_quick_compiled_code_;
    } ptr_sized_fields_;
    
    

    从注释中也可以看到对于Java native 方法,data_里面存储的是jni方法地址(或者是查找目标jni方法的stub方法地址,对应于上面提到的第二种方式)

    隐式注册的流程

    “隐式注册”是指不用我们主动调用RegisterNatives,而是由虚拟机自己去查找jni方法的符号地址。而这个查找jni方法符号的辅助方法的地址也是存储在 ArtMethod 的 data_ 中的。这个赋值逻辑是在方法链接的过程中进行的。

    当加载一个类的时候,通常会走以下几个步骤:

    1. loading:寻找指定类的字节码,并按照 class file format 进行解析
    2. linking:将从字节码中加载的数据处理成虚拟机运行时需要的数据结构,主要有以下几步:
      1. verification:验证字节码的正确性,发现问题的话会抛出 VerifyError
      2. preparation:为类(或接口)创建静态字段并初始化为默认值(或者 ConstantValue Attribute指定的值,如果有这个属性的话)
      3. resolution:将符号引用转换为内存中对应数据结构的引用(在字节码中,比如对某个类的引用,实际是常量池中 CONSTANT_Class 对应的索引)
    3. initialization:执行 <clinit>

    在linking阶段,也会对类中方法体(code attribute)进行链接,具体代码是在 class_linker.cc 的 LinkCode中,下面摘一下为data_赋值查找jni方法符号的辅助方法的逻辑(Android 12代码为例):

    static void LinkCode(ClassLinker* class_linker,
        ArtMethod* method,
        const OatFile::OatClass* oat_class,
        uint32_t class_def_method_index) REQUIRES_SHARED(Locks::mutator_lock_) {
        // ...
        if (method->IsNative()) {
            method->SetEntryPointFromJni(
                        method->IsCriticalNative() ? GetJniDlsymLookupCriticalStub() : GetJniDlsymLookupStub());
                        // ...
        }
    }
    
    

    可以看到对于Java native method,会把 data_字段赋值为 GetJniDlsymLookupStub返回的查找jni方法符号地址的stub地址。(备注:本文不考虑 FastNative & CriticalNative,这类方法实现要求快速、不能阻塞,CriticalNative限制会更多,所以实现一般都比较简单,还未遇到hook他们的需求)

    jni方法符号查找的主要流程(Android 12为例):

    art_jni_dlsym_lookup_stub
        -> artFindNativeMethod
            -> artFindNativeMethodRunnable
                -> 1. 通过 class_linker.GetRegisteredNative 查看是否有其他线程已经完成了注册,如果有,直接返回(Android 12 之前的版本没有这个逻辑)
                   2. 调用 JavaVMExt::FindCodeForNativeMethod
                       -> FindNativeMethod
                           1. 根据 JNI 规范中定义的 Java native method 和 jni method 名称映射规范生成 jni_short_name & jni_long_name
                           2. 调用 FindNativeMethodInternal,通过 dlsym 查找符号
                   3. 将返回的符号地址通过 class_linker->RegisterNative 进行注册,下次就不必查找了
    
    

    jni 方法的调用过程

    1. ArtMethod::invoke 方法可以看到,对于 Java native method,调用会通过 art_quick_invoke_stub 或者 art_quick_invoke_static_stub 来进行,我们下面以static方法的流程来看
    2. arm64 架构上 art_quick_invoke_static_stub 是以汇编代码实现的,主要的工作:
      1. 部分寄存器的暂存(比如 lr、fp等)
      2. 参数的预处理:对于 AACPS64 calling convention 参数是存放到 x0 ~ x7 中的,另外(hardfp)浮点参数 float、double 是存放到 s/d 寄存器中的,所以会根据参数类型进行分组
      3. 另外 jni 方法(非 CriticalNative)会增加 JNIEnv、jobject/jclass 参数,此处也会在栈上预留空间等
      4. 通过 blr 跳转到 ART_METHOD_QUICK_CODE_OFFSET_64 处执行,对应的地址是:art::ArtMethod::EntryPointFromQuickCompiledCodeOffset(art::PointerSize::k64).Int32Value() ,也就是 ArtMethod 的 entry_point_from_quick_compiled_code_
      5. jni 方法返回后,根据返回值的类型从x0、d0、s0从取出返回值

    上面提到 Java native method 调用会跳转到 ArtMethod entry_point_from_quick_compiled_code_ 所指的内存处执行,那么 entry_point_from_quick_compiled_code_ 对应的代码是什么呢?

    上面的 entry_point_from_quick_compiled_code_ 就是在 linking 过程中赋值的,具体逻辑在 class_linker.cc LinkCode 中:

    // ...
    if (quick_code == nullptr) {
        method->SetEntryPointFromQuickCompiledCode(
    method->IsNative() ? GetQuickGenericJniStub() : GetQuickToInterpreterBridge());
    else if (/*xxx*/) {
    //...
    } else {
        method->SetEntryPointFromQuickCompiledCode(quick_code);
    }
    
    

    从上面的代码可以看到:对于 native method entry_point_from_quick_compiled_code_ 赋的是:art_quick_generic_jni_trampoline(quick_code 是 jit compiler生成的,对于native方法,他生成的跟 art_quick_generic_jni_trampoline 的功能应该是一致的)

    现在我们继续看调用流程:

    1. art_quick_generic_jni_trampoline:这里主要做了以下几点:
      1. 调用 artQuickGenericJniTrampoline ,这里会切换线程状态到 kNative,这个状态是gc安全的,也就是如果要触发gc的话,不需要suspend kNative 的Java 线程。另外会通过 GetEntryPointFromJni获取 jni 方法的地址(准确的说,这个地址可能是jni 方法的地址,也可能是负责查找目标jni方法的stub方法地址)
      2. 通过 blr 到上面 GetEntryPointFromJni 的地址实现目标jni方法调用
      3. 调用 artQuickGenericJniEndTrampoline来处理frame,以及将线程状态切换到kRunnable

    上面提到从 GetEntryPointFromJni 获取的jni 方法地址,也就是 ArtMethod 中的 data_字段。

    jni 方法链接&调用小结

    这里简单总结一下上面jni方法链接和调用的过程(Android 12为例):

    1. class_linker.ccLinkCode中将 jni entry point: ArtMethod::data_ 设置为 查找目标jni方法符号的stub地址:art_jni_dlsym_lookup_stub
    2. 在调用java native method时,会跳到ArtMethod::data_地址处执行
      • 如果在调用之前有通过 RegisterNatives 主动注册jni方法地址的话,那么执行的就是jni方法
      • 如果调用之前没有主动注册的话,那么此次data_处对应的就是查找jni方法的stub地址,该方法会按照Java native method 和 jni native method的命名映射规范:Resolving Native Method Names来查找目标jni方法符号的地址
        • 如果没有找到,则抛出UnsatisfiedLinkError
        • 如果找到了,则将其地址存入ArtMethod::data_中(后续调用就不必再查找了,流程跟plt延迟绑定很像),然后再跳转到该目标地址执行

    无需调用原方法的jni hook实现

    如果不需要调用原方法,那么jni hook的实现非常简单:直接通过RegisterNatives重新注册一下就好了。

    这个方案有一个小点需要注意一下:对于fast native,重新注册之前要先去掉access_flags_中的fast native标志位,否则可能会crash。

    以Android 8.0 为例,可以看到首次注册时会向access_flags_中添加fast标志,如果再调一次,在CHECK(!IsFastNative()) << PrettyMethod();处就会出错,所以要先清除对应的标志位。

    const void* ArtMethod::RegisterNative(const void* native_method, bool is_fast) {
        CHECK(IsNative()) << PrettyMethod();
        CHECK(!IsFastNative()) << PrettyMethod();
        CHECK(native_method != nullptr) << PrettyMethod();
        if (is_fast) {
          AddAccessFlags(kAccFastNative);
        }
        //...
    }
    
    

    需要调用原方法的jni hook实现

    跟上面的实现主要的不同就是要获取原方法的地址。这就要分2中情况:

    1. 原方法已经注册,也即ArtMethod::data_中的值就是原方法地址,读出来即可
    2. 原方法未注册,也即ArtMethod::data_中存储的是查找jni方法的stub地址,我们需要自己去查找原方法的地址

    如何获取原方法地址

    那对于一个指定的ArtMethod,我们怎么判断data_中存储的是原方法地址还是查找jni方法的stub地址呢?之前看过有2个“简化方案”:

    1. 不关心data_中存的什么,直接按照java native method 和 jni 方法命名映射规范去查找符号地址。

    这个方案是有问题的,对于设计上主动通过RegisterNatives注册的case,通常我们不会按照默认的映射规范去命名jni方法(方法名太长了),所以查不到。而且即使能查到,如果之前通过RegisterNatives注册过,那么查到的也不是这个“原”方法。

    1. hook前先触发一下目标方法的执行,然后读取data_字段的值。

    这个方法其实更不好,因为hook一个方法通常是不应该触发其执行的,这个不符合使用者的预期,而且比如我们是想通过hook来规避一个可能的crash,结果hook的时候先触发了他的执行,那不就。。。

    那如何判断呢?

    一个直观的想法:上面分析的时候提到:查找jni方法的stub符号是:art_jni_dlsym_lookup_stub & art_jni_dlsym_lookup_critical_stub

    那我们先获取这2个符号的地址,然后看data_的值是否是其中之一就行了:

    • 如果是,那就可以自己查找目标jni方法符号地址来获取原方法地址
    • 如果不是,那data_的值就是原方法地址

    然而有点麻烦的是art_jni_dlsym_lookup_stub & art_jni_dlsym_lookup_critical_stub这2个符号都没有导出:因此我们需要 section header table,symbol table,string table。他们不是运行时需要的,有可能被strip掉,即使没有也很可能没有map进内存。

    在我自己的设备上测了一下,libart.so没有strip掉上面的信息,并且该文件app可读,所以能查到到上面2个符号的地址(当前so的 load bias + symbol.st_value 即是目标符号在当前进程的虚拟地址),所以这个方法可行,但并不可靠,因为可能某些设备上的libart.so是strip过的。

    其实有更简单的方案:上面jni方法链接过程中提到:在LinkCode的时候会统一将data_字段赋值为查找jni方法的stub地址。因此我们可以在hook库中添加一个 java native method,并且不为其注册jni方法,那么它对应的ArtMethod中的data_字段的值就是stub方法的地址。

    如何查找jni方法地址

    如果data_中存储的值是查找jni方法的stub地址,那么原方法地址就需要我们自己查找:

    1. jni方法的名称是什么
    2. 如何根据方法名找到方法地址

    获取jni方法名称可以有2种方案:

    1. 根据Resolving Native Method Names命名映射规范自己生成 jni short name & jni long name,这个方法简单可靠
    2. libart.so中导出了相关的符号,我们可以通过dlsym获取其地址,然后调用即可。(只是Android 7.0开始引入了linker namespace,某些so 比如 libart.so 我们可能无法dlopen,这个时候就需要我们自己解析elf,然后根据 dynamic segment: PT_DYNAMIC, 动态符号表: DT_SYMTAB, 动态字符串表: DT_STRTAB, sysv hash 表: DT_HASH, gnu hash 表: DT_GNU_HASH 来查找符号地址。
    • jni short name的符号:8.0以上:_ZN3art9ArtMethod12JniShortNameEv,以下:_ZN3art12JniShortNameEPNS_9ArtMethodE
    • jni long name的符号:8.0以上:_ZN3art9ArtMethod11JniLongNameEv,以下:_ZN3art11JniLongNameEPNS_9ArtMethodE

    在拿到jni方法名后,可以借助dlsym来查找符号地址,如果是特殊的so无权限dlopen的话,可以像上面提到的自己解析elf获取地址。

    怎么获取java native method对应的ArtMethod地址

    1. 对于Android 11及以上,art method的地址可以从Executable.artMethod获取:
    public abstract class Executable extends AccessibleObject
        implements Member, GenericDeclaration {
            // ...
        /**
         * The ArtMethod associated with this Executable, required for dispatching due to entrypoints
         * Classloader is held live by the declaring class.
         */
        @SuppressWarnings("unused") // set by runtime
        private long artMethod;
        // ...
    }
    
    
    1. 对于 Android 11以下的,art method 的地址就是 jmethodID对应的值:env->FromReflectedMethod(javaMethod)

    怎么获取 data_ 字段在 ArtMethod 中的偏移

    不同版本 data_ 字段在 ArtMethod 中的偏移可能不同,而且其他rom也可能有改动,那怎么获取其偏移呢?

    我们可以在hook库中增加一个java native method:A,为其主动注册一个jni方法 B,那么可知 A 对应的 ArtMethod A'中的 data_ 的值为 B 的地址。然后我们可以从 A' 开始搜索,看偏移多少的值与B的地址相同,那么该偏移就是 data_ 在 ArtMethod 中的偏移。(有没有可能恰好ArtMethod开头的某个数据跟B的地址相同导致偏移计算错了呢?有可能,但这个可能性极低)

    hook流程的整体概述

    hook library初始化流程:

    1. 计算 ArtMethod 中 data_ 字段的偏移量(在低版本的Android中这个字段名不是data_,但不影响,我们是动态搜索目标字段的偏移,后面读写都是用的偏移量)
    2. 计算查找jni方法的stub方法地址:stubAddr

    方法hook的流程:

    1. 从目标方法 ArtMethod 的data_字段中读出value:oldAddr
    2. 如果 oldAddr != stubAddr,那么原方法地址就是 oldAddr
    3. 如果 oldAddr == stubAddr,那么根据命名映射规范生成 jni short name & jni long name,然后通过dlsym(或者自己解析elf)查找符号地址,该值便是原方法地址
    4. 将新方法地址写入 data_ 中
    5. 通过 __builtin___clear_cache 刷新指令缓存

    相关文章

      网友评论

          本文标题:Android jni 方法 hook 的实现方案

          本文链接:https://www.haomeiwen.com/subject/piyjtjtx.html