吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 18812|回复: 126
收起左侧

[Android 原创] 一种通用超简单的Android Java Native方法Hook,无需依赖任何Hook框架

    [复制链接]
beichen 发表于 2021-3-2 12:15
本帖最后由 beichen 于 2021-3-2 12:15 编辑

前言

目前 AndroidHook 的框架已经很多了,但是支持 Java Native 方法的 Hook 却很少,这些框架将 native 方法当普通方法 Hook,适配不同架构复杂等等。本文介绍一种 Android 版本通用的 Java Native Hook 方法并实现代码很少,下面进入我们的分析。

native 方法注册

目前 native 方法只有两种方式

  • 一种是采用导出符号 类名+方法名,然后调用时采用的动态查找方式,其格式如下
    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 最新源码实现位置在 art/runtime/jni/jni_internal.cc,其源码如下:

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[i].name;
    const char* sig = methods[i].signature;
    const void* fnPtr = methods[i].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) << "[Registering JNI native method " << m->PrettyMethod() << "]";

    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) 来完成函数注册,接着分析

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)

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方法

// 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 代码实现

  • 所有代码都可以在我的 fake-linker 中找到

  • 经过上面分析要 Hook 则只需要调用 RegisterNatives 方法,而我们还要备份原方法方便后面可以调用,而原方法的地址在 ArtMethod 对象中的 jni 入口指针中保存,因此需要查找 ArtMethod,为了简单适配不同我们自己手动注册一个函数,然后再拿这个地址跟 ArtMethod 对象中去比较获取偏移量

    1. 获取方法的 ArtMethod 指针,在 Android 11 以下 jmethodID 就是实际的 ArtMethod 指针,Android 11 以上不返回真实的 ArtMethod 指针,但在 Java Method 对象中有一个 private long artMethod 保存着 ArtMethod 指针,其它 Hook 框架都有介绍就不分析了,因此获取 ArtMethod 指针如下
      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 入口指针偏移,指针对齐
      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[i]) == native) {
           jni_offset = i;
           success = true;
           LOGD("found art method entrypoint jni offset: %d", i);
           break;
       }
      }
    3. 读取方法原注册地址
      static 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 字节对齐的,这样避免误判

    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

    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[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.isDebuggerConnectedThrowable.nativeGetStackTraceClass.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 集成 JNIJava native 函数 HookLD_PRELOAD 模式的 PLT HookAndroid 7以上绕过命名空间限制等等

数据滤镜 用于分析恶意软件,提供高度自由的数据过滤,文件重定向,maps 文件过滤,动态符号查找过滤等等

原文链接点击 这里

免费评分

参与人数 58吾爱币 +52 热心值 +52 收起 理由
A310 + 1 + 1 我很赞同!
call_this_name + 1 + 1 用心讨论,共获提升!
shili180 + 1 + 1 谢谢@Thanks!
52xiaojie + 1 很值得学习 谢谢分享
wanleya + 1 + 1 我很赞同!
HEHE139 + 1 + 1 热心回复!
pelephone + 1 + 1 我很赞同!
zyww + 1 + 1 谢谢@Thanks!
18030900400 + 1 + 1 我很赞同!
jing1000 + 1 + 1 谢谢@Thanks!
ccms + 1 我很赞同!
anlovedong + 1 + 1 谢谢@Thanks!
yesoh86 + 1 热心回复!
别说再见 + 1 + 1 谢谢@Thanks!
小脚jio + 1 + 1 楼主np
Q1998 + 1 + 1 我很赞同!
Chaos233 + 1 + 1 谢谢@Thanks!
mfl666 + 1 + 1 用心讨论,共获提升!
努力加载中 + 1 + 1 用心讨论,共获提升!
abset + 1 我很赞同!
zhoumeto + 1 + 1 用心讨论,共获提升!
MikeQu + 1 用心讨论,共获提升!
qaz003 + 1 + 1 HOHO。。醍醐灌顶脑洞大开
lookerJ + 1 用心讨论,共获提升!
不谙世事的骚年 + 1 + 1 我很赞同!
victos + 1 + 1 谢谢@Thanks!
411183343 + 1 谢谢@Thanks!
yunjy + 1 我很赞同!
酷爱腾讯 + 1 + 1 用心讨论,共获提升!
nevinhappy + 1 + 1 我很赞同!
泉水叮咚石头 + 1 + 1 我很赞同!
zycode + 1 + 1 谢谢@Thanks!
brIckZ + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
stardust21 + 1 用心讨论,共获提升!
笙若 + 1 + 1 谢谢@Thanks!
cpj1203 + 1 + 1 谢谢@Thanks!
evea + 1 + 1 我很赞同!
pansophy + 1 + 1 用心讨论,共获提升!
azcolf + 1 + 1 热心回复!
fengbolee + 2 + 1 用心讨论,共获提升!
shark_chilli + 1 + 1 我很赞同!
thornfish + 1 热心回复!
微风吹胖次 + 1 + 1 我很赞同!
KylinYang + 1 + 1 我很赞同!
Lucifer233 + 1 + 1 热心回复!
独行风云 + 1 + 1 谢谢@Thanks!
ttao88 + 1 我很赞同!
Swift_ + 1 + 1 热心回复!
5ud0 + 1 + 1 我很赞同!
Ah0NoR + 1 + 1 谢谢@Thanks!
大轩轩 + 1 + 1 谢谢@Thanks!
nullable + 1 热心回复!
i66235 + 1 + 1 热心回复!
Li1y + 1 + 1 我很赞同!
yulewuzui + 1 + 1 热心回复!
zhuzhuxia111 + 1 + 1 我很赞同!
JMBQ + 1 + 1 我很赞同!
penz + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| beichen 发表于 2021-3-2 13:47
JMBQ 发表于 2021-3-2 13:34
和看雪同时发的?

都发一遍,看雪也是我的账号
 楼主| beichen 发表于 2021-3-3 08:44
微风吹胖次 发表于 2021-3-3 02:24
大佬 请问一下 config_path是用来干什么的

那个是我之前废弃的,本来之前是直接从某个路径读取配置,后面都改成Java传递了
penz 发表于 2021-3-2 12:53
mzpyxsc 发表于 2021-3-2 12:57
通用超简单!!
ftogtong 发表于 2021-3-2 13:07
虽然看不太懂,但慢慢学
dongdiyu789 发表于 2021-3-2 13:11
很不错啊,学习一下啊
小雨杉 发表于 2021-3-2 13:20
学到了,就是还得学习啊
JMBQ 发表于 2021-3-2 13:34
和看雪同时发的?
mymvsl 发表于 2021-3-2 13:48

很好的教程,谢谢分享
dogscar 发表于 2021-3-2 13:49
学习了,很不错!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-15 11:45

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表