一种通用超简单的Android Java Native方法Hook,无需依赖任何Hook框架
本帖最后由 beichen 于 2021-3-2 12:15 编辑## 前言
目前 `Android` 上 `Hook` 的框架已经很多了,但是支持 `Java Native` 方法的 `Hook` 却很少,这些框架将 `native` 方法当普通方法 `Hook`,适配不同架构复杂等等。本文介绍一种 `Android` 版本通用的 `Java Native Hook` 方法并实现代码很少,下面进入我们的分析。
## native 方法注册
目前 `native` 方法只有两种方式
- 一种是采用导出符号 类名+方法名,然后调用时采用的动态查找方式,其格式如下
```cpp
extern "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` 方法
1. 自己实现另一个方法并且同样导出该方法的签名,但是需要保证动态查找时要优先查找你自己函数,且高版本还有命名空间限制
2. 既然调用 `RegisterNatives` 就能注册函数,那假如我们再次调用是不是覆盖掉之前的呢,答案是可以,本文正是通过再次调用 `RegisterNatives` 重新注册函数从而达到 `Hook` 的效果,下面进入具体分析。
## RegisterNatives 源码分析
`RegisterNatives` 最新源码实现位置在 (https://cs.android.com/android/platform/superproject/+/master:art/runtime/jni/jni_internal.cc;l=2310?q=jni_internal.cc&ss=android%2Fplatform%2Fsuperproject),其源码如下:
```cpp
static jint RegisterNatives(JNIEnv* env,
jclass java_class,
const JNINativeMethod* methods,
jint method_count) {
if (UNLIKELY(method_count < 0)) {
JavaVmExtFromEnv(env)->JniAbortF("RegisterNatives", "negative method count: %d",
method_count);
return JNI_ERR;// Not reached except in unit tests.
}
CHECK_NON_NULL_ARGUMENT_FN_NAME("RegisterNatives", java_class, JNI_ERR);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
ScopedObjectAccess soa(env);
StackHandleScope<1> hs(soa.Self());
Handle<mirror::Class> c = hs.NewHandle(soa.Decode<mirror::Class>(java_class));
if (UNLIKELY(method_count == 0)) {
LOG(WARNING) << "JNI RegisterNativeMethods: attempt to register 0 native methods for "
<< c->PrettyDescriptor();
return JNI_OK;
}
CHECK_NON_NULL_ARGUMENT_FN_NAME("RegisterNatives", methods, JNI_ERR);
for (jint i = 0; i < method_count; ++i) {
const char* name = methods.name;
const char* sig = methods.signature;
const void* fnPtr = methods.fnPtr;
if (UNLIKELY(name == nullptr)) {
ReportInvalidJNINativeMethod(soa, c.Get(), "method name", i);
return JNI_ERR;
} else if (UNLIKELY(sig == nullptr)) {
ReportInvalidJNINativeMethod(soa, c.Get(), "method signature", i);
return JNI_ERR;
} else if (UNLIKELY(fnPtr == nullptr)) {
ReportInvalidJNINativeMethod(soa, c.Get(), "native function", i);
return JNI_ERR;
}
bool is_fast = false;
if (*sig == '!') {
is_fast = true;
++sig;
}
// 上面是一些参数验证
ArtMethod* m = nullptr;
bool warn_on_going_to_parent = down_cast<JNIEnvExt*>(env)->GetVm()->IsCheckJniEnabled();
for (ObjPtr<mirror::Class> current_class = c.Get();
current_class != nullptr;
current_class = current_class->GetSuperClass()) {
// 查询方法对应的 ArtMethod 对象
m = FindMethod<true>(current_class, name, sig);
if (m != nullptr) {
break;
}
// Search again comparing to all methods, to find non-native methods that match.
m = FindMethod<false>(current_class, name, sig);
if (m != nullptr) {
break;
}
if (warn_on_going_to_parent) {
LOG(WARNING) << "CheckJNI: method to register \"" << name << "\" not in the given class. "
<< "This is slow, consider changing your RegisterNatives calls.";
warn_on_going_to_parent = false;
}
}
if (m == nullptr) {
c->DumpClass(LOG_STREAM(ERROR), mirror::Class::kDumpClassFullDetail);
LOG(ERROR)
<< "Failed to register native method "
<< c->PrettyDescriptor() << "." << name << sig << " in "
<< c->GetDexCache()->GetLocation()->ToModifiedUtf8();
ThrowNoSuchMethodError(soa, c.Get(), name, sig, "static or non-static");
return JNI_ERR;
} else if (!m->IsNative()) {
// 非 native 方法是不能注册的
LOG(ERROR)
<< "Failed to register non-native method "
<< c->PrettyDescriptor() << "." << name << sig
<< " as native";
ThrowNoSuchMethodError(soa, c.Get(), name, sig, "native");
return JNI_ERR;
}
VLOG(jni) << "";
if (UNLIKELY(is_fast)) {
LOG(WARNING) << "!bang JNI is deprecated. Switch to @FastNative for " << m->PrettyMethod();
is_fast = false;
// TODO: make this a hard register error in the future.
}
// 最终调用 class_linker->RegisterNative 来实际进行注册
const void* final_function_ptr = class_linker->RegisterNative(soa.Self(), m, fnPtr);
UNUSED(final_function_ptr);
}
return JNI_OK;
}
```
这个方法主要进行各种验证并查找方法对应的 `ArtMethod`,其中 `FastNative` 在 Android8.0 以后已经采用注解的方式了,最终调用 `class_linker->RegisterNative(soa.Self(), m, fnPtr)` 来完成函数注册,接着分析
```cpp
const void* ClassLinker::RegisterNative(
Thread* self, ArtMethod* method, const void* native_method) {
CHECK(method->IsNative()) << method->PrettyMethod();
CHECK(native_method != nullptr) << method->PrettyMethod();
void* new_native_method = nullptr;
Runtime* runtime = Runtime::Current();
// 这里 JVMTI 响应注册事件
runtime->GetRuntimeCallbacks()->RegisterNativeMethod(method,
native_method,
/*out*/&new_native_method);
if (method->IsCriticalNative()) {
MutexLock lock(self, critical_native_code_with_clinit_check_lock_);
// Remove old registered method if any.
auto it = critical_native_code_with_clinit_check_.find(method);
if (it != critical_native_code_with_clinit_check_.end()) {
critical_native_code_with_clinit_check_.erase(it);
}
// To ensure correct memory visibility, we need the class to be visibly
// initialized before we can set the JNI entrypoint.
if (method->GetDeclaringClass()->IsVisiblyInitialized()) {
method->SetEntryPointFromJni(new_native_method);
} else {
critical_native_code_with_clinit_check_.emplace(method, new_native_method);
}
} else {
method->SetEntryPointFromJni(new_native_method);
}
return new_native_method;
}
```
有关 `JVMTI` 大家可以网上搜索,通过它能做到很多黑科技并且这里也使用了它修改后的 `new_native_method`,因此通过 `JVMTI` 也能达到 `Hook`。 ,这里判断 `CriticalNative` 如果没有初始化类则先要初始化类,然后再注册。最终实现注册的是 `method->SetEntryPointFromJni(new_native_method)`
```cpp
void SetEntryPointFromJni(const void* entrypoint)
REQUIRES_SHARED(Locks::mutator_lock_) {
// The resolution method also has a JNI entrypoint for direct calls from
// compiled code to the JNI dlsym lookup stub for @CriticalNative.
DCHECK(IsNative() || IsRuntimeMethod());
SetEntryPointFromJniPtrSize(entrypoint, kRuntimePointerSize);
}
```
最终只是设置 `ArtMethod` 对象中的 `jni` 入口点指针为我们的注册函数,上面是主分支代码分析,`Android 11` 及以下都是调用的 `ArtMethod::RegisterNative`方法
```cpp
// Android 9 ~ 11
const void* ArtMethod::RegisterNative(const void* native_method) {
CHECK(IsNative()) << PrettyMethod();
CHECK(native_method != nullptr) << PrettyMethod();
void* new_native_method = nullptr;
Runtime::Current()->GetRuntimeCallbacks()->RegisterNativeMethod(this,
native_method,
/*out*/&new_native_method);
SetEntryPointFromJni(new_native_method);
return new_native_method;
}
// Android 9 以下
const void* ArtMethod::RegisterNative(const void* native_method, bool is_fast) {
CHECK(IsNative()) << PrettyMethod();
// 多了一个 FastNative 检测,在已经注册为 FastNative 后不能再次注册
CHECK(!IsFastNative()) << PrettyMethod();
CHECK(native_method != nullptr) << PrettyMethod();
if (is_fast) {
AddAccessFlags(kAccFastNative);
}
void* new_native_method = nullptr;
Runtime::Current()->GetRuntimeCallbacks()->RegisterNativeMethod(this,
native_method,
/*out*/&new_native_method);
SetEntryPointFromJni(new_native_method);
return new_native_method;
}
```
这里可以看到在 `Android 9` 以上直接调用即可覆盖掉,`Android 9` 以下需要清理 `FastNative` 标志
## Java Native 代码实现
- 所有代码都可以在我的 (https://github.com/sanfengAndroid/fake-linker/blob/main/src/main/cpp/linker/art/hook_jni_native_interface_impl.cpp) 中找到
- 经过上面分析要 `Hook` 则只需要调用 `RegisterNatives` 方法,而我们还要备份原方法方便后面可以调用,而原方法的地址在 `ArtMethod` 对象中的 `jni` 入口指针中保存,因此需要查找 `ArtMethod`,为了简单适配不同我们自己手动注册一个函数,然后再拿这个地址跟 `ArtMethod` 对象中去比较获取偏移量
1. 获取方法的 `ArtMethod` 指针,在 `Android 11` 以下 `jmethodID` 就是实际的 `ArtMethod` 指针,`Android 11` 以上不返回真实的 `ArtMethod` 指针,但在 Java Method 对象中有一个 `private long artMethod` 保存着 `ArtMethod` 指针,其它 `Hook` 框架都有介绍就不分析了,因此获取 `ArtMethod` 指针如下
```cpp
static void *GetArtMethod(JNIEnv *env, jclass clazz, jmethodID methodId) {
#if __ANDROID_API__ >= __ANDROID_API_R__
if (IsIndexId(methodId)){
jobject method = env->ToReflectedMethod(clazz, methodId, true);
return reinterpret_cast<void *>(env->GetLongField(method, field_art_method));
}
#endif
return methodId;
}
```
2. 查找 `jni` 入口指针偏移,指针对齐
```cpp
uintptr_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) == native) {
jni_offset = i;
success = true;
LOGD("found art method entrypoint jni offset: %d", i);
break;
}
}
```
3. 读取方法原注册地址
```cpp
static void *GetOriginalNativeFunction(const uintptr_t *art_method) {
if (__predict_false(art_method == nullptr)) {
return nullptr;
}
return (void *) art_method;
}
```
- 在 `Android 9` 以下如果原方法是 `FastNative` 类型则还需要清除标志,因此还要查找 `uint32_t accessFlags` 成员的偏移,这里我们又采用查找的方式,来确定偏移,使用 `uint32_t` 对齐,我这里选择的是 `0x109` 标志也就是 `public static native`,这里最好要有 `public` 标志 `0x1`,因为正常指针都是 4/8 字节对齐的,这样避免误判
```cpp
if (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`
```cpp
int 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;
for (int i = 0; i < len; ++i) {
JNINativeMethod hook = items.hook_method;
const char *sign = hook.signature;
if (sign == '!') {
sign++;
}
// 第一步:查找方法的 jmethodID,这里要去除 FastNative标志的签名,否者查找不成功
jmethodID methodId = items.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.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.is_static);
continue;
}
if (items.backup_method != nullptr) {
*(items.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.is_static);
continue;
}
// 第四步:Android 9 以下如果有 FastNative 标志则还需要清除
bool restore = ClearFastNativeFlag(reinterpret_cast<char *>(artMethod));
if (api >= __ANDROID_API_O__) {
hook.signature = sign;
}
methods = 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.is_static);
JNIHelper::PrintAndClearException(env);
if (restore) {
AddAccessFlag(reinterpret_cast<char *>(artMethod), kAccFastNative);
}
if (items.backup_method != nullptr) {
*(items.backup_method) = nullptr;
}
}
}
return success;
}
```
## 实际应用
- 参考我的 [数据滤镜](https://github.com/sanfengAndroid/FakeXposed/blob/main/app/src/main/cpp/hook/hook_java_native.cpp) 内部完成了 `VMDebug.isDebuggerConnected`,`Throwable.nativeGetStackTrace`,`Class.classForName` 等等函数
- 上面我们采用重新注册来替换原来的地址,那么自然第一种动态查找的 `native` 方法我们也采取动态注册的方法占坑,然后系统就不会再走查找流程,如果我们要调用原函数则自己动态查找符号即可
- 上面的方式需要在原方法已经注册后再 Hook,不然拿到的地址只是动态查找入口地址,那么假如我们不知道它什么时候注册该怎么办呢,或者我们先注册后面又被它自己重新注册呢。 这种情况我们可以 `Hook` `RegisterNative` 函数,其具体实现也在我的 (https://github.com/sanfengAndroid/fake-linker/blob/main/src/main/cpp/linker/art/hook_jni_native_interface_impl.cpp) 中,`Hook JNI` 函数更加简单,本质上只是一些函数指针,直接替换为我们的即可,但是要注意读写权限
## 注意事项
- 在 `Android 9` 以上不能反射隐藏类,通过 `jni` 函数也不能获取到,因此需要先过掉反射限制
- `Android 8` 以下使用的是 `!bang JNI`,8 及以上使用 `FastNative`,参考 [变更](https://source.android.google.cn/devices/tech/dalvik/improvements?hl=zh-cn),因此低版本注册 `FastNative` 方法时签名需要 `!` 开头
- 在源码中注册都是加了锁的而我们没有加锁,因此理论上可能会出错,但实际上 `ArtMethod` 位置并不会改变,并且注册以后通常其值也不会改变,因此没有必要加锁
## 后记
- 通过上面的方法调用 `RegisterNatives` 来交给系统帮我们 `Hook` 这样减少很大的适配,而我们只需获取原方法地址和访问标志适配代码量大大减少且通用性更强。在 `Android` 中比较关键的函数基本都是在 `Native` 方法中完成,因此适用性还是挺高的。
- 避免被检测性加强,因为我们 `Hook` 更底层的 `Native` 函数,整个过程唯一变动的就是 `ArtMethod` 中的 `jni` 入口指针,这样检测难度稍微增加
- 不必依赖任何框架,更加减少更检测到的可能性
- 缺点就是 `Java Hook` 变成了 `native hook` 了,编写代码的难度上升了一些,当然你可以注册一个通用的 `native` 函数做跳板,然后跳转回 `Java` 处理
## 我的项目参考
(https://github.com/sanfengAndroid/fake-linker) 集成 `JNI`,`Java native` 函数 `Hook`,`LD_PRELOAD` 模式的 `PLT Hook`,`Android 7`以上绕过命名空间限制等等
[数据滤镜](https://github.com/sanfengAndroid/FakeXposed) 用于分析恶意软件,提供高度自由的数据过滤,文件重定向,maps 文件过滤,动态符号查找过滤等等
# 原文链接点击 [这里](https://sanfengandroid.github.io/2021/02/28/simple-java-native-hook/) JMBQ 发表于 2021-3-2 13:34
和看雪同时发的?
都发一遍,看雪也是我的账号 微风吹胖次 发表于 2021-3-3 02:24
大佬 请问一下 config_path是用来干什么的
那个是我之前废弃的,本来之前是直接从某个路径读取配置,后面都改成Java传递了 赞,楼主威武 通用超简单!! 虽然看不太懂,但慢慢学 很不错啊,学习一下啊 学到了,就是还得学习啊 和看雪同时发的?
很好的教程,谢谢分享 学习了,很不错!