Android jni 方法 hook 的实现方案

本文详细阐述了JNI方法的调用流程,包括主动和隐式注册方式,以及JNI方法hook的实现原理。重点介绍了如何判断和获取原方法地址,以及hook过程中涉及到的内存管理和符号查找技术。
摘要由CSDN通过智能技术生成

简介

本文主要是简述一下 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_ 字段中。

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代码为例):

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为例):

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 中:

// ...
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();处就会出错,所以要先清除对应的标志位。

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 来查找符号地址。
  • 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获取:
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 刷新指令缓存

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值