Dalvik/ART下JNI方法的动态注册原理与追踪

xiaoeryu Lv5

在之前的JNI代码中我们都是使用的静态注册,使用extern "C"来编译,这样的代码在编译之后的so文件中仍然会保留原函数名,对APK的安全性有一定影响。

接下来我们尝试一下将静态注册的JNI函数改为动态注册,比较一下和之前有何不同。

以及对于改为动态注册的函数我们应该如何去追踪。

静态注册与动态注册

JNI函数被调用前,必须要完成Java与so的绑定:

  • 被动(静态):由Dalvik/ART虚拟机在调用前查找并完成地址的绑定
    • 静态函数名规则:Java_包名_类名_方法名
    • 优点:函数名简单明了
    • 缺点:名字过长、查抓效率不高、安全性降低
  • 主动(动态):由APP自己完成地址的绑定

静态注册

反编译静态注册的so文件

  • 例如这个静态注册JNI代码编译后的so文件,用IDA反编译之后就能直接通过搜索函数名定义到函数位置。

动态注册

可以将原来的extern "C"给去掉,函数名无所谓可以随便写就是一个标识

在JNI_OnLoad中手动绑定

 JNINativeMethod jniNativeMethod[] = {
         {"onCreate", "(Landroid/os/Bundle;)V", (void*)onCreate},
         {"newObject", "()V", (void*)newObject}
 };
 jclass MainActivityjclass = env->FindClass("com/xiaoeryu/reflectiontest/MainActivity");
 env->RegisterNatives(MainActivityjclass, jniNativeMethod, sizeof(jniNativeMethod)/sizeof(JNINativeMethod));
 jclass tmpjclass = env->FindClass("com/xiaoeryu/reflectiontest/Test");
 jclass testjclass = static_cast<jclass>(env->NewWeakGlobalRef(tmpjclass));

通过RegisterNatives()方法完成手动绑定

在JNI函数前加上__attribute__ ((visibility ("hidden"))) 来隐藏符号信息

隐藏了之后就无法在IDA里面直接搜索到这个函数了

Dalvik下动态注册的原理

查看源码分析一下RegisterNatives的实现

  • 查看其在Android4.4中的实现

    2453/*
    2454 * Register one or more native functions in one class.
    2455 *
    2456 * This can be called multiple times on the same method, allowing the
    2457 * caller to redefine the method implementation at will.
    2458 */
    2459static jint RegisterNatives(JNIEnv* env, jclass jclazz,
    2460    const JNINativeMethod* methods, jint nMethods)
    2461{
    2462    ScopedJniThreadState ts(env);
    2463
    2464    ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(ts.self(), jclazz);
    2465
    2466    if (gDvm.verboseJni) {
    2467        ALOGI("[Registering JNI native methods for class %s]",
    2468            clazz->descriptor);
    2469    }
    2470
    2471    for (int i = 0; i < nMethods; i++) {
    2472        if (!dvmRegisterJNIMethod(clazz, methods[i].name,
    2473                methods[i].signature, methods[i].fnPtr))
    2474        {
    2475            return JNI_ERR;
    2476        }
    2477    }
    2478    return JNI_OK;
    2479}
    
  • 函数参数:

    • jclass jclazz:表示要注册本地方法的Java类
    • const JNINativeMethod* methods:是一个指向JNINativeMethod结构体数组的指针,该结构体包含了本地方法的名称、签名和函数指针
    • jint nMethods:表示要注册的本地方法的数量
  • 这个函数的目的是将本地方法与Java类相关联,以便在Java代码中调用这些本地方法。

  • 往下分析可以看到具体的实现在dvmRegisterJNIMethod函数中,跟进去看一下

    static bool dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName,
                                     const char* signature, void* fnPtr)
    {
        
        ...
    
    
        Method* method = dvmFindDirectMethodByDescriptor(clazz, methodName, signature);	// 这个method表示Java函数在Dalvik当中对应的结构体
            
        ...
            
        if (method->nativeFunc != dvmResolveNativeMethod) {
            /* this is allowed, but unusual */
            /* 这里表示允许这个JNI函数注册两次,不过不常见*/
            ALOGV("Note: %s.%s:%s was already registered", clazz->descriptor, methodName, signature);
        }
        method->fastJni = fastJni;
        dvmUseJNIBridge(method, fnPtr);
    }
    

    比如说在这里注册的时候可以让它注册两次,可以绑定不同的本地方法。(如果在动态调试APP的时候碰到有JNI函数在不同的时刻注册在了不同的地址,可能就是这种情况)暂时没发现有啥用

  • 继续往下分析,还可以看到有很多地方都使用到了method结构体,那分析这个有什么用呢?

    • 我们可以修改源码在调用method的地方添加log,重新编译,然后再运行APP的时候就可以将动态注册的结构体的注册信息打印出来。
    • 暂时没有运行4.4的设备,而且Dalvik比较少用了就不去修改源码重新编译了,等下分析ART源码跟踪一下在ART中的动态注册的结构体。

ART下动态注册的原理

接下来分析Android8.0的ART源码,并修改源码在关键点加LOG获取动态注册的JNI函数地址。

ART下动态注册分析

跟进来之后可以看到函数的实现

2148    static jint RegisterNatives(JNIEnv* env, jclass java_class, const JNINativeMethod* methods,
2149                                jint method_count) {
2150      return RegisterNativeMethods(env, java_class, methods, method_count, true);
2151    }
  • 跟进RegisterNativeMethods
3054  void RegisterNativeMethods(JNIEnv* env, const char* jni_class_name, const JNINativeMethod* methods,
3055                             jint method_count) {
3056    ScopedLocalRef<jclass> c(env, env->FindClass(jni_class_name));
3057    if (c.get() == nullptr) {
3058      LOG(FATAL) << "Couldn't find class: " << jni_class_name;
3059    }
3060    JNI::RegisterNativeMethods(env, c.get(), methods, method_count, false);
3061  }
3062  
3063  }  // namespace art
  • 跟进JNI::RegisterNativeMethods
  • 对于任意一个在Java类中定义的函数而言,它在调用前都会有一个准备的过程。这个过程中会调用LoadClassMembers完成被加载的过程:遍历当前类中的field和函数,将其准备好。
  • 在这个遍历过程中会准备好每一个函数的ArtMethod对象
  • 只有在准备好了之后才能被调用,不论这个函数是Java还是JNI实现

接下来看一下ArtMethod的定义:

继续之前的JNI::RegisterNativeMethods

  • 跟进RegisterNative()
  • 继续跟进SetEntryPointFromJni(),参数是函数地址

  • 看这个名字我们就能发现他是对于JNI入口地址的绑定

    art_method.h里面也有关于EntryPointFromJni的定义

  • 到这里还可以继续往下分析,SetEntryPointFromJniPtrSize第一个参数是函数地址,第二个参数是运行系统位数

以上就是ART下一个函数的动态注册流程

修改源码HOOK

接下来我们在这些流程中添加一些log信息,追踪动态注册的函数

  1. RegisterNatives
  2. RegisterNativeMethods
  3. jint::RegisterNativeMethods
  4. RegisterNative
  5. SetEntryPointFromJni
  6. 。。。

通过在这些函数加LOG日志(LOG(WARNING) << xxx;)的方法将函数的信息都打印出来

修改完源码之后重新编译

source build/envsetup.sh

lunch执行后选择要编译的系统版本

time make -x多给几个线程编译会快点

编译完成刷入手机

在编译的过程中,再来看一下art_method.h中JNI的注册

  • 通过注释我们就可以知道JNI函数最终是注册到了这里
  • 不过这个地方在不同系统版本的实现会有所不同,这个我们在HOOK的时候要注意

例如:

android-6.0.1
android-14.0.0_r2
运行我们的demo
  • 可以看到我们标记的第一部分所有绑定的函数都是同一个函数地址:所以这很可能是一个函数跳转表之类的东西

  • 懒绑定:对于静态函数来说,第一次编译的时候都绑定在了跳转表的地址,第二次调用的时候才从跳转表查询要调用的函数并绑定,等再次调用的时候就不需要重复查询了。

  • 这里面除了标红的onCreate、newObject之外都是静态函数

  • 第二部分是我们动态绑定的函数,地址在我们插入LOG的几个位置都打印出来了,从地址长度可以看出来是运行在64位的设备上

再拿一个加壳的APP跑一下试试

执行完过滤一下结果,可以看到加载的函数地址也都打印出来了

  • 说明我们HOOK是成功的
  • 但是设备会特别卡,应该在哪里过滤一下会好点

接下来了解一些so加载过程中,函数执行时机的问题

在Android里面有两个函数可以加载so文件:

  • System.loadLibrary,参数只需要传入文件名即可,它内部会自动拼接文件名
  • System.load,参数需要传入绝对路径

在项目中JNI_OnLoad函数是执行比较早的,要早于大部分JNI函数的执行时机

但是在它之前还有两类函数(INIT和INIT_ARRAY)要比它更早执行

INIT_ARRAY默认是按定义的顺序执行,也可以通过给constructor传入参数,手动设置它的执行顺序

分析编译后生成的apk文件

解压apk分析里面的so文件

用Ubuntu自带的readelf工具,可以解析so文件

tom@ubuntu:~/Downloads$ readelf -d libreflectiontest.so 

Dynamic section at offset 0x4bb30 contains 31 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libandroid.so]
 0x0000000000000001 (NEEDED)             Shared library: [liblog.so]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
 0x000000000000000e (SONAME)             Library soname: [libreflectiontest.so]
 0x000000000000001e (FLAGS)              BIND_NOW
 0x000000006ffffffb (FLAGS_1)            Flags: NOW
 0x0000000000000007 (RELA)               0xb880
 0x0000000000000008 (RELASZ)             34992 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffff9 (RELACOUNT)          992
 0x0000000000000017 (JMPREL)             0x14130
 0x0000000000000002 (PLTRELSZ)           3912 (bytes)
 0x0000000000000003 (PLTGOT)             0x4ce38
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000006 (SYMTAB)             0x2f8
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000005 (STRTAB)             0x6720
 0x000000000000000a (STRSZ)              20829 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x4408
 0x0000000000000004 (HASH)               0x5328
 0x0000000000000019 (INIT_ARRAY)         0x4cb10	// 这个是我们定义的INIT_ARRAY函数
 0x000000000000001b (INIT_ARRAYSZ)       32 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x4cb00
 0x000000000000001c (FINI_ARRAYSZ)       16 (bytes)
 0x000000000000000c (INIT)               0x2146c	// 在IDA里面直接可以搜索INIT函数的位置
 0x000000006ffffff0 (VERSYM)             0x3ec8
 0x000000006ffffffe (VERNEED)            0x43c4
 0x000000006fffffff (VERNEEDNUM)         2
 0x0000000000000000 (NULL)               0x0

在IDA里面按G搜索0x2146c这个地址就可以跳转到INIT函数处

再来搜索一下INIT_ARRAY函数

  • 这边的几个函数就是我们之前按顺序定义的initarray_3、initarray_2、initarray_1函数

以后我们如果要对一些加壳的SO文件进行分析的时候需要断在_INIT之前就可以用上面这种方法下断点,断下来之后开始动态调试

代码地址

  • 标题: Dalvik/ART下JNI方法的动态注册原理与追踪
  • 作者: xiaoeryu
  • 创建于 : 2023-10-21 22:04:43
  • 更新于 : 2023-11-04 11:58:51
  • 链接: https://github.com/xiaoeryu/2023/10/21/Dalvik-ART下动态注册原理追踪/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论