JNI函数 Hook实战

46 篇文章 1 订阅

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_;

这里有一个对我们非常有帮助的成员,就是entry_point_from_quick_compiled_code_ (这里勘误为data_),它存放着函数执行的地址,对于jni函数来说,其实就是本地方法的地址。那么如果我们能够把函数调用的地址改写成需要hook的函数地址,那我们其实就实现了对于jni函数调用的hook操作。

思路很清晰,但是实现起来却没有那么简单,刚刚我们也讲到,系统在正常版本中,其实在java层中找不到ArtMethod本身了,但是它也留了一个口子,以android13为例子,我们在java层中的Executable类的artMethod属性,其实就保存着当前方法所对应的native层的ArtMethod的地址。

图片

我们的目标是修改ArtMethod中函数调用的地址,那么怎么利用java层的这个artMethod属性(代表着真实的ArtMethod的地址)达到目的。

这里有两种思路:

思路1:通过指针,然后计算出来ArtMethod的大小,通过(指针+ArtMethod大小),强制转换这部分内存为一个ArtMethod对象,然后再进行函数地址的修改。但是这里会有几个问题,我们所了解的ArtMethod的内存布局,其实都是基于AOSP的ArtMethod,但是对于Android设备厂商,ArtMethod是很有可能被改变的,比如下图,

image.png

厂商可以随便定制自定义的属性与顺序,比如早期的阿里热修复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

作者:Pika
链接:https://juejin.cn/post/7268894037464367140
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值