Jni 函数 hook

本篇所讲到的一切内容均已开源,JniHook

在开发中,咱们应该才智到了很多种黑科技hook,比方got/plt hook,inline hook等等,得益于这些技能的开展,咱们在性能优化范畴中也向着更深的范畴出发。回忆字节最近几期的Android基础技能揭秘,其实都离不开hook。咱们今天要介绍的是,jni函数 hook。

咱们日常中,接触到的jni函数可不少,比方体系大部分的java与native的功能转化,或者是常见的jni开发,其实都离不开jni函数。jni函数是java 与 native 衔接的大门,jni函数通常用关键字标识,比方java native关键字润饰,kotlin external 关键字。 当然,native层注册对应的jni函数完成,有动态注册与静态注册之分,咱们以一个静态注册的函数为比方

external fun testJniHook()

testJniHook对应的native完成是

JNIEXPORT void JNICALL
Java_com_example_jnihook_MainActivity_testJniHook(JNIEnv *env, jobject thiz) {
    __android_log_print(ANDROID_LOG_ERROR, "hello", "%s", "test_jni_hook");
}

在之前开发中,我就遇到了需求对jni函数进行hook的需求,比方咱们想要记录jni函数的调用信息或者是改动jni函数调用参数。在这之前,我通常是在jni的本地办法中(c办法)去找函数有没有hook点。这儿值得注意的是,假如咱们想要直接hook Java_com_example_jnihook_MainActivity_testJniHook函数,咱们常用的got/plt hook其实是不可的,由于大部分没有经过got表查找这一进程,一起大部分情况下,咱们很有可能拿不到适宜的symbol(符号),所以这儿要么采用inline hook去写死偏移找到函数修正,要么看后续的函数调用有没有适宜的hook点。这两种方式中,无论是哪种,其实对咱们来说都不太方便。

走运的是,最近看了btrace的源码,发现了一个风趣的计划,这便是咱们本期的jni hook完成的前身。

Jni hook计划

原理篇

上文咱们讲到,已然咱们无法随意拿到jni 函数自身的symbol,那么咱们有没有办法对办法自身进行修正呢?这儿就要完毕咱们的老朋友了,ArtMethod。

  • java 层中有类 ArtMethod,Method 与之一对一, Method 中含有 ArtMethod 的引用,而 mirror::ArtMethod 便是 java 层 ArtMethod 的映射。
  • 6.0 之后,java ArtMethod 不复存在,被完全躲藏,准确来说,其实都放在了native层

ArtMethod的界说在这儿,它基本上保留着最重要的几个参数,比方办法的界说类,当时的拜访权限以及办法在dex中的索引等等


  // 这点需求特别重视,影响完成
  GcRoot<mirror::Class> declaring_class_;
  // java 层的 Modifier 只有其高 16 位
  // 低 16 位用作 ART 的内部运转,在 java 层被躲藏了
  std::atomic<std::uint32_t> access_flags_;
  // 办法的 CodeItem 在 Dex 中的偏移
  uint32_t dex_code_item_offset_;
  // 办法在 Dex 中的 index
  uint32_t dex_method_index_;
  // 虚办法则为完成办法在 VTable 中的 index
  // 非虚办法则是办法在 DexCodeCache 中的 index
  uint16_t method_index_;
  // 办法的热度,JIT 的重要参阅
  uint16_t hotness_count_;
  struct PtrSizedFields {
    // 非常重要!
    void* data_;
    // 办法的 Code 进口
    // 假如没有编译,则
    // art_quick_to_interpreter_bridge
    // art_quick_generic_jni_trampoline
    // 假如 JIT/AOT 则为编译后的 native 进口
    void* entry_point_from_quick_compiled_code_;
  } ptr_sized_fields_;

这儿有一个对咱们非常有协助的成员,便是data_ ,它存放着函数执行的地址,关于JNI函数,它的解释是:

native method: pointer to the JNI function registered to this method

关于jni函数来说,其实便是本地办法的地址。那么假如咱们可以把函数调用的地址改写成需求hook的函数地址,那咱们其实就完成了关于jni函数调用的hook操作。

思路很明晰,可是完成起来却没有那么简略,刚刚咱们也讲到,体系在正常版别中,其实在java层中找不到ArtMethod自身了,可是它也留了一个口儿,以android13为比方,咱们在java层中的Executable类的artMethod特点,其实就保存着当时办法所对应的native层的ArtMethod的地址。

JNI函数 Hook实战

咱们的目标是修正ArtMethod中函数调用的地址,那么怎样利用java层的这个artMethod特点(代表着真实的ArtMethod的地址)达到意图。

这儿有两种思路:

思路1:经过指针,然后计算出来ArtMethod的巨细,经过(指针+ArtMethod巨细),强制转化这部本分存为一个ArtMethod目标,然后再进行函数地址的修正。可是这儿会有几个问题,咱们所了解的ArtMethod的内存布局,其实都是基于AOSP的ArtMethod,可是关于Android设备厂商,ArtMethod是很有可能被改动的,比方下图,

JNI函数 Hook实战
厂商可以随意定制自界说的特点与顺序,比方早期的阿里热修复Andfix其实也是用到了ArtMethod修正,可是也遇到了特点不一致的问题。因而,咱们不能直接去复原ArtMethod,即便复原了,也不能确保函数进口这个字段的顺序产生改动。

思路2:这也是btrace供给的一个比较别致的主意,便是拿一个咱们预先生成好的jni办法,然后拿到java层的artMethod函数地址后,然后经过有限的遍历,去匹配当时的native函数地址。由于预先生成的jni办法,咱们可以拿到java层的Method,一起也可以拿到函数的地址。假如匹配成功,那么咱们其实就把ArtMethod中关于函数地址这个特点的偏移给确定下来了,由于ArtMethod即便有更改,当时也是全局一致的。

}
void **target_art_method = get_art_method(env, method);
// 找到了jni_entrance_index
if (target_art_method[jni_entrance_index] == 当时native jni函数地址) {
    找到了index
}

因而,咱们只需求找到函数进口的index,然后以此类推,每个需求hook的jni函数固定好偏移,然后修正这个地址为hook函数即可

完成篇

咱们刚刚也说到,咱们要获取到java层ArtMethod的地址,才能做之后的更改。这儿在android api低于30的时分,咱们之间经过FromReflectedMethod这个jni办法,就能获取java中Method所对应的ArtMethod。高于或者等于30api的时分,咱们需求经过Executable去拿到artMethod,如下:

static void **get_art_method(JNIEnv *env, jobject foo) {
    void **fooArtMethod;
    if (android_get_device_api_level() >= 30) {
        jclass Executable = (*env)->FindClass(env, "java/lang/reflect/Executable");
        jfieldID artMethodField = (*env)->GetFieldID(env, Executable, "artMethod", "J");
        fooArtMethod = (void **) (*env)->GetLongField(env, foo, artMethodField);
    } else {
        fooArtMethod = (void **) (*env)->FromReflectedMethod(env, foo);
    }
    return fooArtMethod;
}

接着,拿到Artmethod这个地址之后,咱们经过在原理篇的讲解,咱们需求一个固定的jni函数,去获取ArtMethod中的关于函数地址的偏移,这儿咱们可以固定写死一个jni函数jniPlaceHolder,然后获取到jniPlaceHolder对应的Method目标,执行get_art_method函数获取到ArtMethod目标的地址

fun jniHookInit() {
    // 这儿咱们预先执行一次jniPlaceHolder,读者可以考虑为什么,答案写在评论
    jniPlaceHolder()
    val placeHolder = JniHook::class.java.getDeclaredMethod("jniPlaceHolder")
    jniHookInitByHolder(placeHolder)
}
private external fun jniPlaceHolder()
JNIEXPORT void JNICALL
Java_com_pika_jnihook_JniHook_jniPlaceHolder(JNIEnv *env, jclass clazz) {
}

咱们经过有限遍历,这儿选取了50(尽量选取刚好等于ArtMethod特点数量巨细的值),由于咱们可以经过指针的偏移方式去获取ArtMethod的特点,然后咱们对比它的地址是不是等于Java_com_pika_jnihook_JniHook_jniPlaceHolder这个咱们固定好的本地办法地址,假如等于,咱们就找到了jni_entrance_index

Java_com_pika_jnihook_JniHook_jniPlaceHolder 便是fooJNI
static void init_jni_hook(JNIEnv *env, jobject foo, void *fooJNI) {
    void **fooArtMethod = get_art_method(env, foo);
    for (int i = 0; i < 50; ++i) {
        if (fooArtMethod[i] == fooJNI) {
            jni_entrance_index = i;
            break;
        }
    }
}

找到index之后,后续咱们想要去hook一个jni办法,咱们只需求经过这个index去找到地址,然后修正地址的内容即可

int hook_jni(JNIEnv *env, jobject method, void *new_entrance, void **origin_entrance) {
    if (jni_entrance_index == -1) {
        return -1;
    }
    void **target_art_method = get_art_method(env, method);
    if (target_art_method[jni_entrance_index] == new_entrance) {
        return 0;
    }
    保存原函数,防止修正后丢掉原函数
    *origin_entrance = target_art_method[jni_entrance_index];
    修正函数调用地址为hook函数地址
    target_art_method[jni_entrance_index] = new_entrance;
    return 1;
}

假如咱们想要取消hook,只需求把原函数的地址从头赋值给ArtMethod中的函数地址即可

void unhook_jni(JNIEnv *env, jobject method, void *origin_entrance) {
    void **target_art_method = get_art_method(env, method);
    if (target_art_method[jni_entrance_index] == origin_entrance) {
        return;
    }
    target_art_method[jni_entrance_index] = origin_entrance;
}

使用篇

回到刚才的一个比方jni函数testJniHook

external fun testJniHook()
JNIEXPORT void JNICALL
Java_com_example_jnihook_MainActivity_testJniHook(JNIEnv *env, jobject thiz) {
    __android_log_print(ANDROID_LOG_ERROR, "hello", "%s", "test_jni_hook");
}

咱们想要对它进行hook,只需求调用hook_jni,传入testJniHook对应的Method目标即可

JNIEXPORT void JNICALL
Java_com_example_jnihook_MainActivity_hooktest(JNIEnv *env, jobject thiz, jobject method) {
    hook_jni(env, method, (void *) test_jni_hook_proxy, (void **) &test_jni_original);
}

完好比方在github.com/TestPlanB/J… 我们也不必忧虑没有课后作业啦!

总结

上面所以讲到的内容,我都已经开源了,JniHook,期望这个库可以协助开发者更加简略的去用上jni hook。假如对你有协助,还请留下你的star噢!

参阅

btrace