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是很有可能被改变的,比如下图,
厂商可以随便定制自定义的属性与顺序,比如早期的阿里热修复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噢!
参考
作者:Pika
链接:https://juejin.cn/post/7268894037464367140
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。