前言
目前 Android
上 Hook
的框架已经很多了,但是支持 Java Native
方法的 Hook
却很少,这些框架将 native
方法当普通方法 Hook
,适配不同架构复杂等等。本文介绍一种 Android
版本通用的 Java Native Hook
方法并实现代码很少,下面进入我们的分析。
native 方法注册
目前 native
方法只有两种方式
- 一种是采用导出符号 类名+方法名,然后调用时采用的动态查找方式,其格式如下
1
2
3
4extern "C"
JNIEXPORT void JNICALL Java_com_sanfengandroid_fakelinker_FakeLinker_setLogLevel(JNIEnv *env, jclass clazz, jint level) {
g_log_level = level;
} - 另一种方式采用动态注册调用
JNI
函数RegisterNatives
动态注册函数
native 方法 Hook 思路
根据上面两种方式很自然想到了两种 Hook
方法
- 自己实现另一个方法并且同样导出该方法的签名,但是需要保证动态查找时要优先查找你自己函数,且高版本还有命名空间限制
- 既然调用
RegisterNatives
就能注册函数,那假如我们再次调用是不是覆盖掉之前的呢,答案是可以,本文正是通过再次调用RegisterNatives
重新注册函数从而达到Hook
的效果,下面进入具体分析。
RegisterNatives 源码分析
RegisterNatives
最新源码实现位置在 art/runtime/jni/jni_internal.cc,其源码如下:
1 | static jint RegisterNatives(JNIEnv* env, |
这个方法主要进行各种验证并查找方法对应的 ArtMethod
,其中 FastNative
在 Android8.0 以后已经采用注解的方式了,最终调用 class_linker->RegisterNative(soa.Self(), m, fnPtr)
来完成函数注册,接着分析
1 | const void* ClassLinker::RegisterNative( |
有关 JVMTI
大家可以网上搜索,通过它能做到很多黑科技并且这里也使用了它修改后的 new_native_method
,因此通过 JVMTI
也能达到 Hook
。 ,这里判断 CriticalNative
如果没有初始化类则先要初始化类,然后再注册。最终实现注册的是 method->SetEntryPointFromJni(new_native_method)
1 | void SetEntryPointFromJni(const void* entrypoint) |
最终只是设置 ArtMethod
对象中的 jni
入口点指针为我们的注册函数,上面是主分支代码分析,Android 11
及以下都是调用的 ArtMethod::RegisterNative
方法
1 | // Android 9 ~ 11 |
这里可以看到在 Android 9
以上直接调用即可覆盖掉,Android 9
以下需要清理 FastNative
标志
Java Native 代码实现
所有代码都可以在我的 fake-linker 中找到
经过上面分析要
Hook
则只需要调用RegisterNatives
方法,而我们还要备份原方法方便后面可以调用,而原方法的地址在ArtMethod
对象中的jni
入口指针中保存,因此需要查找ArtMethod
,为了简单适配不同我们自己手动注册一个函数,然后再拿这个地址跟ArtMethod
对象中去比较获取偏移量- 获取方法的
ArtMethod
指针,在Android 11
以下jmethodID
就是实际的ArtMethod
指针,Android 11
以上不返回真实的ArtMethod
指针,但在 Java Method 对象中有一个private long artMethod
保存着ArtMethod
指针,其它Hook
框架都有介绍就不分析了,因此获取ArtMethod
指针如下1
2
3
4
5
6
7
8
9static void *GetArtMethod(JNIEnv *env, jclass clazz, jmethodID methodId) {
if (IsIndexId(methodId)){
jobject method = env->ToReflectedMethod(clazz, methodId, true);
return reinterpret_cast<void *>(env->GetLongField(method, field_art_method));
}
return methodId;
} - 查找
jni
入口指针偏移,指针对齐1
2
3
4
5
6
7
8
9
10
11uintptr_t *artMethod = static_cast<uintptr_t *>(GetArtMethod(env, clazz, methodId));
bool success = false;
for (int i = 0; i < 30; ++i) {
// 直接与我们自己注册的 native 方法地址相比较
if (reinterpret_cast<void *>(artMethod[i]) == native) {
jni_offset = i;
success = true;
LOGD("found art method entrypoint jni offset: %d", i);
break;
}
} - 读取方法原注册地址
1
2
3
4
5
6static void *GetOriginalNativeFunction(const uintptr_t *art_method) {
if (__predict_false(art_method == nullptr)) {
return nullptr;
}
return (void *) art_method[jni_offset];
}
- 获取方法的
在
Android 9
以下如果原方法是FastNative
类型则还需要清除标志,因此还要查找uint32_t accessFlags
成员的偏移,这里我们又采用查找的方式,来确定偏移,使用uint32_t
对齐,我这里选择的是0x109
标志也就是public static native
,这里最好要有public
标志0x1
,因为正常指针都是 4/8 字节对齐的,这样避免误判1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25if (api >= __ANDROID_API_Q__){
// 非内部隐藏类
flags |= 0x10000000;
}
char *start = reinterpret_cast<char *>(artMethod);
for (int i = 1; i < 18; ++i) {
uint32_t value = *(uint32_t *) (start + i * 4);
if (value == flags) {
access_flags_art_method_offset = i * 4;
LOGD("found art method match access flags offset: %d", i * 4);
success &= true;
break;
}
}
if (access_flags_art_method_offset < 0){
if (api >= __ANDROID_API_N__) {
access_flags_art_method_offset = 4;
}else if (api == __ANDROID_API_M__){
access_flags_art_method_offset = 12;
}else if (api == __ANDROID_API_L_MR1__){
access_flags_art_method_offset = 20;
}else if (api == __ANDROID_API_L__){
access_flags_art_method_offset = 56;
}
}准备工作完成了就该实现具体的
Hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66int RegisterNativeAgain(JNIEnv *env, jclass clazz, HookRegisterNativeUnit *items, size_t len) {
if (clazz == nullptr || items == nullptr || len < 1) {
LOGE("Registration class or method cannot be empty");
return -1;
}
int success = 0;
JNINativeMethod methods[1];
for (int i = 0; i < len; ++i) {
JNINativeMethod hook = items[i].hook_method;
const char *sign = hook.signature;
if (sign[0] == '!') {
sign++;
}
// 第一步:查找方法的 jmethodID,这里要去除 FastNative标志的签名,否者查找不成功
jmethodID methodId = items[i].is_static ? env->GetStaticMethodID(clazz, hook.name, sign) : env->GetMethodID(clazz, hook.name, sign);
if (methodId == nullptr) {
LOGE("Find method failed, name: %s, signature: %s, is static: %d", hook.name, hook.signature, items[i].is_static);
JNIHelper::PrintAndClearException(env);
continue;
}
// 第二步:获取方法对应的 ArtMethod 指针
void *artMethod = GetArtMethod(env, clazz, methodId);
// 第三步:备份原方法
void *backup = GetOriginalNativeFunction(static_cast<uintptr_t *>(artMethod));
if (backup == hook.fnPtr) {
LOGE("The same native method has been registered, name: %s, signature: %s, address: %p, is static: %d",
hook.name, hook.signature, hook.fnPtr, items[i].is_static);
continue;
}
if (items[i].backup_method != nullptr) {
*(items[i].backup_method) = backup;
}
if (!HasAccessFlag(reinterpret_cast<char *>(artMethod), kAccNative)) {
LOGE("You are hooking a non-native method, name: %s, signature: %s, is static: %d", hook.name, hook.signature, items[i].is_static);
continue;
}
// 第四步:Android 9 以下如果有 FastNative 标志则还需要清除
bool restore = ClearFastNativeFlag(reinterpret_cast<char *>(artMethod));
if (api >= __ANDROID_API_O__) {
hook.signature = sign;
}
methods[0] = hook;
// 第五步:重新注册为我们的 Hook 方法
if (env->RegisterNatives(clazz, methods, 1) == JNI_OK) {
success++;
/*
* Android 8.0 ,8.1 必须清除 FastNative 标志才能注册成功,所以如果原来包含 FastNative 标志还得恢复,
* 否者调用原方法可能会出现问题
* */
// 第六步:如果需要则恢复原函数的 FastNative 标志
if (restore && (api == __ANDROID_API_O__ || api == __ANDROID_API_O_MR1__)) {
AddAccessFlag(reinterpret_cast<char *>(artMethod), kAccFastNative);
}
} else {
LOGE("register native function failed, method name: %s, sign: %s, is static: %d", hook.name, hook.signature, items[i].is_static);
JNIHelper::PrintAndClearException(env);
if (restore) {
AddAccessFlag(reinterpret_cast<char *>(artMethod), kAccFastNative);
}
if (items[i].backup_method != nullptr) {
*(items[i].backup_method) = nullptr;
}
}
}
return success;
}实际应用
参考我的 数据滤镜 内部完成了
VMDebug.isDebuggerConnected
,Throwable.nativeGetStackTrace
,Class.classForName
等等函数上面我们采用重新注册来替换原来的地址,那么自然第一种动态查找的
native
方法我们也采取动态注册的方法占坑,然后系统就不会再走查找流程,如果我们要调用原函数则自己动态查找符号即可上面的方式需要在原方法已经注册后再 Hook,不然拿到的地址只是动态查找入口地址,那么假如我们不知道它什么时候注册该怎么办呢,或者我们先注册后面又被它自己重新注册呢。 这种情况我们可以
Hook
RegisterNative
函数,其具体实现也在我的 fake-linker 中,Hook JNI
函数更加简单,本质上只是一些函数指针,直接替换为我们的即可,但是要注意读写权限
注意事项
- 在
Android 9
以上不能反射隐藏类,通过jni
函数也不能获取到,因此需要先过掉反射限制 Android 8
以下使用的是!bang JNI
,8 及以上使用FastNative
,参考 变更,因此低版本注册FastNative
方法时签名需要!
开头- 在源码中注册都是加了锁的而我们没有加锁,因此理论上可能会出错,但实际上
ArtMethod
位置并不会改变,并且注册以后通常其值也不会改变,因此没有必要加锁
后记
- 通过上面的方法调用
RegisterNatives
来交给系统帮我们Hook
这样减少很大的适配,而我们只需获取原方法地址和访问标志适配代码量大大减少且通用性更强。在Android
中比较关键的函数基本都是在Native
方法中完成,因此适用性还是挺高的。 - 避免被检测性加强,因为我们
Hook
更底层的Native
函数,整个过程唯一变动的就是ArtMethod
中的jni
入口指针,这样检测难度稍微增加 - 不必依赖任何框架,更加减少更检测到的可能性
- 缺点就是
Java Hook
变成了native hook
了,编写代码的难度上升了一些,当然你可以注册一个通用的native
函数做跳板,然后跳转回Java
处理
我的项目参考
fake-linker 集成 JNI
,Java native
函数 Hook
,LD_PRELOAD
模式的 PLT Hook
,Android 7
以上绕过命名空间限制等等
数据滤镜 用于分析恶意软件,提供高度自由的数据过滤,文件重定向,maps 文件过滤,动态符号查找过滤等等