Android jni 方法 hook 的实现方案

简介

本文主要是简述一下 jni 方法的调用流程,然后讨论下对jni 方法hook的实现方案。

JNI 即 Java Native Interface,作为Java代码和native代码之间的中间层,方便Java代码调用native api以及native 代码调用Java api。

以 Android 上Java 代码启动线程为例,调用 Thread.start 方法时,会调到 nativeCreate 进而调用到他的 native peer Thread_nativeCreate,最后创建相应的 pthread。

那么我们说的jni hook主要做的就是可以修改 Java native method 的 native peer,以上面创建线程为例,hook前,nativeCreate 的 native peer 是 Thread_nativeCreate,通过jni hook,我们可以将native peer改为我们指定的 Thread_nativeCreate_proxy,这样后面调用 nativeCreate 就会执行到 Thread_nativeCreate_proxy

要实现 jni hook,主要需要做2点:

  1. 修改 native peer 为我们指定的 proxy 方法
  2. 获取原来的方法地址,因为很多时候在proxy方法中都需要调用原方法

在实现hook之前,我们先来看看jni方法的链接和调用过程。

jni 方法的链接

jni 方法链接有两种方式:

  1. 通过 RegisterNatives主动注册
  2. 按照 jni 的规范命名,由虚拟机在运行时自动查找和绑定,Java native method 和 jni native method的命名映射规范可以参考:Resolving Native Method Names

主动注册的流程

以Android 12 的代码为例:RegisterNatives 的实现是在 ClassLinker 中,会通过ArtMethod::SetEntryPointFromJni将我们的jni方法地址存储到 ArtMethod 的 data_ 字段中。

c++ 复制代码
struct PtrSizedFields {
    // Depending on the method type, the data is
    //   - native method: pointer to the JNI function registered to this method
    //                    or a function to resolve the JNI function,
    //   - resolution method: pointer to a function to resolve the method and
    //                        the JNI function for @CriticalNative.
    //   - conflict method: ImtConflictTable,
    //   - abstract/interface method: the single-implementation if any,
    //   - proxy method: the original interface method or constructor,
    //   - other methods: during AOT the code item offset, at runtime a pointer
    //                    to the code item.
    void* data_;

    // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
    // the interpreter.
    void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;

从注释中也可以看到对于Java native 方法,data_里面存储的是jni方法地址(或者是查找目标jni方法的stub方法地址,对应于上面提到的第二种方式)

隐式注册的流程

"隐式注册"是指不用我们主动调用RegisterNatives,而是由虚拟机自己去查找jni方法的符号地址。而这个查找jni方法符号的辅助方法的地址也是存储在 ArtMethod 的 data_ 中的。这个赋值逻辑是在方法链接的过程中进行的。

当加载一个类的时候,通常会走以下几个步骤:

  1. loading:寻找指定类的字节码,并按照 class file format 进行解析
  2. linking:将从字节码中加载的数据处理成虚拟机运行时需要的数据结构,主要有以下几步:
    1. verification:验证字节码的正确性,发现问题的话会抛出 VerifyError
    2. preparation:为类(或接口)创建静态字段并初始化为默认值(或者 ConstantValue Attribute指定的值,如果有这个属性的话)
    3. resolution:将符号引用转换为内存中对应数据结构的引用(在字节码中,比如对某个类的引用,实际是常量池中 CONSTANT_Class 对应的索引)
  3. initialization:执行 <clinit>

在linking阶段,也会对类中方法体(code attribute)进行链接,具体代码是在 class_linker.cc 的 LinkCode中,下面摘一下为data_赋值查找jni方法符号的辅助方法的逻辑(Android 12代码为例):

c++ 复制代码
static void LinkCode(ClassLinker* class_linker,
	ArtMethod* method,
	const OatFile::OatClass* oat_class,
	uint32_t class_def_method_index) REQUIRES_SHARED(Locks::mutator_lock_) {
	// ...
	if (method->IsNative()) {
		method->SetEntryPointFromJni(
					method->IsCriticalNative() ? GetJniDlsymLookupCriticalStub() : GetJniDlsymLookupStub());
					// ...
	}
}

可以看到对于Java native method,会把 data_字段赋值为 GetJniDlsymLookupStub返回的查找jni方法符号地址的stub地址。(备注:本文不考虑 FastNative & CriticalNative,这类方法实现要求快速、不能阻塞,CriticalNative限制会更多,所以实现一般都比较简单,还未遇到hook他们的需求)

jni方法符号查找的主要流程(Android 12为例):

c++ 复制代码
art_jni_dlsym_lookup_stub
	-> artFindNativeMethod
		-> artFindNativeMethodRunnable
			-> 1. 通过 class_linker.GetRegisteredNative 查看是否有其他线程已经完成了注册,如果有,直接返回(Android 12 之前的版本没有这个逻辑)
			   2. 调用 JavaVMExt::FindCodeForNativeMethod
				   -> FindNativeMethod
					   1. 根据 JNI 规范中定义的 Java native method 和 jni method 名称映射规范生成 jni_short_name & jni_long_name
					   2. 调用 FindNativeMethodInternal,通过 dlsym 查找符号
			   3. 将返回的符号地址通过 class_linker->RegisterNative 进行注册,下次就不必查找了

jni 方法的调用过程

  1. ArtMethod::invoke 方法可以看到,对于 Java native method,调用会通过 art_quick_invoke_stub 或者 art_quick_invoke_static_stub 来进行,我们下面以static方法的流程来看
  2. arm64 架构上 art_quick_invoke_static_stub 是以汇编代码实现的,主要的工作:
    1. 部分寄存器的暂存(比如 lr、fp等)
    2. 参数的预处理:对于 AACPS64 calling convention 参数是存放到 x0 ~ x7 中的,另外(hardfp)浮点参数 float、double 是存放到 s/d 寄存器中的,所以会根据参数类型进行分组
    3. 另外 jni 方法(非 CriticalNative)会增加 JNIEnv、jobject/jclass 参数,此处也会在栈上预留空间等
    4. 通过 blr 跳转到 ART_METHOD_QUICK_CODE_OFFSET_64 处执行,对应的地址是:art::ArtMethod::EntryPointFromQuickCompiledCodeOffset(art::PointerSize::k64).Int32Value() ,也就是 ArtMethod 的 entry_point_from_quick_compiled_code_
    5. jni 方法返回后,根据返回值的类型从x0、d0、s0从取出返回值

上面提到 Java native method 调用会跳转到 ArtMethod entry_point_from_quick_compiled_code_ 所指的内存处执行,那么 entry_point_from_quick_compiled_code_ 对应的代码是什么呢?

上面的 entry_point_from_quick_compiled_code_ 就是在 linking 过程中赋值的,具体逻辑在 class_linker.cc LinkCode 中:

c++ 复制代码
// ...
if (quick_code == nullptr) {
	method->SetEntryPointFromQuickCompiledCode(
method->IsNative() ? GetQuickGenericJniStub() : GetQuickToInterpreterBridge());
else if (/*xxx*/) {
//...
} else {
	method->SetEntryPointFromQuickCompiledCode(quick_code);
}

从上面的代码可以看到:对于 native method entry_point_from_quick_compiled_code_ 赋的是:art_quick_generic_jni_trampoline(quick_code 是 jit compiler生成的,对于native方法,他生成的跟 art_quick_generic_jni_trampoline 的功能应该是一致的)

现在我们继续看调用流程:

  1. art_quick_generic_jni_trampoline:这里主要做了以下几点:
    1. 调用 artQuickGenericJniTrampoline ,这里会切换线程状态到 kNative,这个状态是gc安全的,也就是如果要触发gc的话,不需要suspend kNative 的Java 线程。另外会通过 GetEntryPointFromJni获取 jni 方法的地址(准确的说,这个地址可能是jni 方法的地址,也可能是负责查找目标jni方法的stub方法地址)
    2. 通过 blr 到上面 GetEntryPointFromJni 的地址实现目标jni方法调用
    3. 调用 artQuickGenericJniEndTrampoline来处理frame,以及将线程状态切换到kRunnable

上面提到从 GetEntryPointFromJni 获取的jni 方法地址,也就是 ArtMethod 中的 data_字段。

jni 方法链接&调用小结

这里简单总结一下上面jni方法链接和调用的过程(Android 12为例):

  1. class_linker.ccLinkCode中将 jni entry point: ArtMethod::data_ 设置为 查找目标jni方法符号的stub地址:art_jni_dlsym_lookup_stub
  2. 在调用java native method时,会跳到ArtMethod::data_地址处执行
    • 如果在调用之前有通过 RegisterNatives 主动注册jni方法地址的话,那么执行的就是jni方法
    • 如果调用之前没有主动注册的话,那么此次data_处对应的就是查找jni方法的stub地址,该方法会按照Java native method 和 jni native method的命名映射规范:Resolving Native Method Names来查找目标jni方法符号的地址
      • 如果没有找到,则抛出UnsatisfiedLinkError
      • 如果找到了,则将其地址存入ArtMethod::data_中(后续调用就不必再查找了,流程跟plt延迟绑定很像),然后再跳转到该目标地址执行

无需调用原方法的jni hook实现

如果不需要调用原方法,那么jni hook的实现非常简单:直接通过RegisterNatives重新注册一下就好了。

这个方案有一个小点需要注意一下:对于fast native,重新注册之前要先去掉access_flags_中的fast native标志位,否则可能会crash。

以Android 8.0 为例,可以看到首次注册时会向access_flags_中添加fast标志,如果再调一次,在CHECK(!IsFastNative()) << PrettyMethod();处就会出错,所以要先清除对应的标志位。

c++ 复制代码
const void* ArtMethod::RegisterNative(const void* native_method, bool is_fast) {
    CHECK(IsNative()) << PrettyMethod();
    CHECK(!IsFastNative()) << PrettyMethod();
    CHECK(native_method != nullptr) << PrettyMethod();
    if (is_fast) {
      AddAccessFlags(kAccFastNative);
    }
    //...
}

需要调用原方法的jni hook实现

跟上面的实现主要的不同就是要获取原方法的地址。这就要分2中情况:

  1. 原方法已经注册,也即ArtMethod::data_中的值就是原方法地址,读出来即可
  2. 原方法未注册,也即ArtMethod::data_中存储的是查找jni方法的stub地址,我们需要自己去查找原方法的地址

如何获取原方法地址

那对于一个指定的ArtMethod,我们怎么判断data_中存储的是原方法地址还是查找jni方法的stub地址呢?之前看过有2个"简化方案":

  1. 不关心data_中存的什么,直接按照java native method 和 jni 方法命名映射规范去查找符号地址。

这个方案是有问题的,对于设计上主动通过RegisterNatives注册的case,通常我们不会按照默认的映射规范去命名jni方法(方法名太长了),所以查不到。而且即使能查到,如果之前通过RegisterNatives注册过,那么查到的也不是这个"原"方法。

  1. hook前先触发一下目标方法的执行,然后读取data_字段的值。

这个方法其实更不好,因为hook一个方法通常是不应该触发其执行的,这个不符合使用者的预期,而且比如我们是想通过hook来规避一个可能的crash,结果hook的时候先触发了他的执行,那不就。。。

那如何判断呢?

一个直观的想法:上面分析的时候提到:查找jni方法的stub符号是:art_jni_dlsym_lookup_stub & art_jni_dlsym_lookup_critical_stub

那我们先获取这2个符号的地址,然后看data_的值是否是其中之一就行了:

  • 如果是,那就可以自己查找目标jni方法符号地址来获取原方法地址
  • 如果不是,那data_的值就是原方法地址

然而有点麻烦的是art_jni_dlsym_lookup_stub & art_jni_dlsym_lookup_critical_stub这2个符号都没有导出:因此我们需要 section header table,symbol table,string table。他们不是运行时需要的,有可能被strip掉,即使没有也很可能没有map进内存。

在我自己的设备上测了一下,libart.so没有strip掉上面的信息,并且该文件app可读,所以能查到到上面2个符号的地址(当前so的 load bias + symbol.st_value 即是目标符号在当前进程的虚拟地址),所以这个方法可行,但并不可靠,因为可能某些设备上的libart.so是strip过的。

其实有更简单的方案:上面jni方法链接过程中提到:在LinkCode的时候会统一将data_字段赋值为查找jni方法的stub地址。因此我们可以在hook库中添加一个 java native method,并且不为其注册jni方法,那么它对应的ArtMethod中的data_字段的值就是stub方法的地址。

如何查找jni方法地址

如果data_中存储的值是查找jni方法的stub地址,那么原方法地址就需要我们自己查找:

  1. jni方法的名称是什么
  2. 如何根据方法名找到方法地址

获取jni方法名称可以有2种方案:

  1. 根据Resolving Native Method Names命名映射规范自己生成 jni short name & jni long name,这个方法简单可靠
  2. libart.so中导出了相关的符号,我们可以通过dlsym获取其地址,然后调用即可。(只是Android 7.0开始引入了linker namespace,某些so 比如 libart.so 我们可能无法dlopen,这个时候就需要我们自己解析elf,然后根据 dynamic segment: PT_DYNAMIC, 动态符号表: DT_SYMTAB, 动态字符串表: DT_STRTAB, sysv hash 表: DT_HASH, gnu hash 表: DT_GNU_HASH 来查找符号地址,可以参考:ELF 通过 Sysv Hash & Gnu Hash 查找符号的实现及对比
  • jni short name的符号:8.0以上:_ZN3art9ArtMethod12JniShortNameEv,以下:_ZN3art12JniShortNameEPNS_9ArtMethodE
  • jni long name的符号:8.0以上:_ZN3art9ArtMethod11JniLongNameEv,以下:_ZN3art11JniLongNameEPNS_9ArtMethodE

在拿到jni方法名后,可以借助dlsym来查找符号地址,如果是特殊的so无权限dlopen的话,可以像上面提到的自己解析elf获取地址。

怎么获取java native method对应的ArtMethod地址

  1. 对于Android 11及以上,art method的地址可以从Executable.artMethod获取:
java 复制代码
public abstract class Executable extends AccessibleObject
    implements Member, GenericDeclaration {
		// ...
	/**
     * The ArtMethod associated with this Executable, required for dispatching due to entrypoints
     * Classloader is held live by the declaring class.
     */
    @SuppressWarnings("unused") // set by runtime
    private long artMethod;
	// ...
}
  1. 对于 Android 11以下的,art method 的地址就是 jmethodID对应的值:env->FromReflectedMethod(javaMethod)

怎么获取 data_ 字段在 ArtMethod 中的偏移

不同版本 data_ 字段在 ArtMethod 中的偏移可能不同,而且其他rom也可能有改动,那怎么获取其偏移呢?

我们可以在hook库中增加一个java native method:A,为其主动注册一个jni方法 B,那么可知 A 对应的 ArtMethod A'中的 data_ 的值为 B 的地址。然后我们可以从 A' 开始搜索,看偏移多少的值与B的地址相同,那么该偏移就是 data_ 在 ArtMethod 中的偏移。(有没有可能恰好ArtMethod开头的某个数据跟B的地址相同导致偏移计算错了呢?有可能,但这个可能性极低)

hook流程的整体概述

hook library初始化流程:

  1. 计算 ArtMethod 中 data_ 字段的偏移量(在低版本的Android中这个字段名不是data_,但不影响,我们是动态搜索目标字段的偏移,后面读写都是用的偏移量)
  2. 计算查找jni方法的stub方法地址:stubAddr

方法hook的流程:

  1. 从目标方法 ArtMethod 的data_字段中读出value:oldAddr
  2. 如果 oldAddr != stubAddr,那么原方法地址就是 oldAddr
  3. 如果 oldAddr == stubAddr,那么根据命名映射规范生成 jni short name & jni long name,然后通过dlsym(或者自己解析elf)查找符号地址,该值便是原方法地址
  4. 将新方法地址写入 data_ 中
  5. 通过 __builtin___clear_cache 刷新指令缓存
相关推荐
csj5022 分钟前
安卓基础之《(28)—Service组件》
android
lhbian2 小时前
PHP、C++和C语言对比:哪个更适合你?
android·数据库·spring boot·mysql·kafka
catoop3 小时前
Android 最佳实践、分层架构与全流程解析(2025)
android
ZHANG13HAO4 小时前
Android 13 特权应用(Android Studio 开发)调用 AOSP 隐藏 API 完整教程
android·ide·android studio
田梓燊4 小时前
leetcode 142
android·java·leetcode
angerdream4 小时前
Android手把手编写儿童手机远程监控App之JAVA基础
android
菠萝地亚狂想曲5 小时前
Zephyr_01, environment
android·java·javascript
sTone873755 小时前
跨端框架通信机制全解析:从 URL Schema 到 JSI 到 Platform Channel
android·前端
sTone873755 小时前
Java 注解完全指南:从 "这是什么" 到 "自己写一个"
android·前端
catoop5 小时前
Kotlin 协程在 Android 开发中的应用:定义、优势与对比
android·kotlin