inline hook & ART hook

arm64 Linux(Android) inline hook

介绍

Inline Hook是一种在软件开发和逆向工程中广泛使用的强大技术,它通过直接修改目标函数的内存代码,来实现对该函数执行流程的拦截和改变。 其核心思想是在目标函数的起始位置(通常是函数头部)插入一个跳转指令,使得当该函数被调用时,程序的执行流会跳转到我们预先定义好的“钩子”(Hook)函数中。

替换目标函数之后,还提供调用原函数的功能

应用场景

  • 软件调试与分析、自动化测试/Mock
  • API监控:日志、沙箱
  • 热补丁
  • 外挂与作弊程序
  • 恶意软件

功能、输入输出

函数以函数指针的形式输入输出

hook

  • 输入:目标函数hook函数
  • 作用:目标函数hook函数替换
  • 输出:指向一个与目标函数效果相同的函数的指针(原函数

unhook

  • 输入:目标函数
  • 作用:将目标函数 恢复原样
  • 输出:无

原理与实现

目标函数 的替换

目标函数的起始地址添加跳转指令,使其最终跳转到hook函数

arm64:32位定长指令

b的范围: //(26+2位有符号数,4字节对齐后两位是0)的区域

跳转到任意位置需要借助跳板

  • 目标函数 附近为跳板分配空间(mmap)
  • 若距离小于128MB,则使用短跳转(占用1条指令)
  • 否则使用长跳转(占用4条指令,其中包含8字节存储地址的空间)

目标函数 的替换

; 短跳
b .+offset
; 长跳
; 可以使用的寄存器:
; 根据调用约定x9~x15由调用者保存
; x16 (ip0) 和 x17 (ip1) 是过程间临时寄存器。
; 这意味着它们仅在过程序言(Prologue)期间用作临时寄存器。
; 除此之外,它们基本上几乎不被使用。
ldr x9, .+8
br x9
.quad {trampoline}

跳板

; 不用节省空间,统一使用长跳转
ldr x17, +8
br x17
.quad {new_func}

指令修复

  • arm64下,很多指令默认是相对pc寻址的
  • 发生移动崩溃

为了实现调用原函数,需要在复制指令后进行修复。 改造成绝对寻址。

  • 指令长度不够使用64位立即数:先将地址载入寄存器中(类似前面的长跳转)。

例如ldr:

; 原指令
ldr {reg}, .+offset
 
; 修复后
ldr {reg}, .+12
ldr {reg}, [{reg}]
b .+12
.quad {addr}

unhook

hook时备份目标函数 ,unhook时还原

同时记得unmap掉为跳板和“原函数”分配的内存

测试

带相对pc寻址的指令的函数测试

测试hook结果、调用原函数结果、unhook结果

测试长、短跳转两种方案

test1{width=300px}

::right::

test1{width=270px}

带递归的函数测试

说明没有破坏调用栈、能实现插桩的效果

test1{width=600px}

::right::

test1{width=400px}

存在的问题

使用长跳转的方式修改目标函数 时,若存在“跳转目标为前4条指令”的指令,则无法实现正确调用原函数

参考

ART method hook

介绍

类似inline hook,但对象是ART虚拟机中的Java方法。

应用场景

  • 应用性能监控
  • 软件开发与调试:针对第三方SDK的动态日志注入、线上问题诊断与热修复
  • 破解与绕过、游戏外挂
  • 应用加固与反逆向

功能与输入输出

目标方法 以方法签名的形式输入,hook方法以jni函数指针形式输入

hook

  • 输入:目标方法hook方法
  • 作用:目标方法hook方法替换
  • 输出:无(优化了,改成通过函数签名调用原方法)

unhook

  • 输入:目标方法
  • 作用:将目标方法 恢复原样
  • 输出:无

call_orig_method

  • 输入:目标方法、this指针(静态函数为null)、参数表
  • 作用:调用原方法
  • 输出:原方法返回值

原理与实现

可能的实现方法

ART虚拟机中的Java方法:ArtMethod结构

  • 关键入口点:ptr_sized_fields_.entry_point_from_quick_compiled_code_ ,指向一段跳板,根据方法类型,进入到不同的处理过程。
  • 调用数据:ptr_sized_fields_.data,可能是函数指针、code_item、…
方法类型data
native指向注册到此方法的 JNI 函数的指针,或一个用于解析 JNI 函数的函数。
resolution指向一个函数的指针,该函数用于解析方法以及 @CriticalNative 的 JNI 函数。
conflictImtConflictTable(接口方法表冲突表)。
abstract/interface如果存在单一实现(single-implementation),则指向该实现。
proxy原始的接口方法或构造函数。
default conflictnull
其他方法在 AOT(预先编译)期间是代码项的偏移量;在运行时是指向代码项的指针。
  • inline hook entry_point_from_quick_compiled_code_ 指向的函数,根据调用约定解析参数后,再调用hook方法。
  • 目标方法 设置为native,借用vm自带的跳板函数处理参数,要求hook方法是一个符合规范的jni函数/方法。

不自己处理参数的话不好做xposed兼容的api,但这暂时不是我们的目标

通过修改方法对应的ArtMethod的数据,使其变为native方法,把跳板换成jni方法的跳板,再修改其native入口点为hook函数

  • 目标1:找到目标方法 对应的ArtMethod 结构
  • 目标2:将该方法标记为Native,设置jni入口点和跳板函数,备份原ArtMethod 结构
  • 目标3:提供调用备份的ArtMethod 的方法

符号解析与定位

从elf文件中读取符号表,可得到符号偏移

再从/proc/self/maps获得模块在内存中的实际起始地址

(注意Android 15以上符号表可能被xz压缩后存在了.gnu_debugdata 节)

调用libart.so中的函数时,需要先定位。

获得JavaVM

调用libart.so:JNI_GetCreatedJavaVMs 获得JavaVM

获得JNIEnv

若当前线程未附加到JVM,则需要先附加

javaVm->GetEnv(&env, JNI_VERSION_1_6);
javaVm->AttachCurrentThread(&env, nullptr);

art::Runtime::instance

art::Runtime 是一个单例,并且导出了符号。后续其他未到处的结构的定位将基于它来进行。

探测art::Runtime 的结构

art::Runtime 中存了一个JavaVM ,与之前的JavaVM 是同一个(Android应用中每个进程最多对应一个vm),可定位到JavaVM 的偏移。

之后,根据相对位置,可以得到:jni_id_manager_intern_table_class_linker_

runtime{width=60%}

目标1:找到目标方法 对应的ArtMethod 结构

获取method_id

  • 不能直接使用env->FindClass,因为不确定当前线程的上下文的classloader能否加载目标类。

  • 需要使用:android.app.ActivityThread.currentApplication().getClassLoader().loadClass()

  • 再使用env->GetMethodID 得到jmethodID类型的方法id

获取ArtMethod

libart中的类JniIdManager 有如下方法将方法id转换为ArtMethod

ArtMethod* JniIdManager::DecodeMethodId(jmethodID method)
  • 利用符号信息定位DecodeMethodId的地址
  • 利用前面得到的jni_id_manager_ 实例
  • jni_id_manager_上调用DecodeMethodId即可得到ArtMethod 结构

探测ClassLinker 的结构

  • ClassLinker 中保存了各种跳板的地址
  • 存在和之前相同的intern_table_
  • 定位到偏移后根据相对位置得到quick_generic_jni_trampoline_

探测ArtMethod 的结构

  • 利用已知签名系统方法,来定位字段。
  • 例如记录android.os.process.getElapsedCpuTime的access_flags为public static native final
  • native方法的ptr_sized_fields_.data指向libandroid_runtime.so内的地址。

最终结合相对位置我们得到了:access_flags_ptr_sized_fields_.dataptr_sized_fields_.entry_point_from_quick_compiled_code_

目标2:将该方法标记为Native,设置jni入口点和跳板函数,备份原ArtMethod 结构

  • 修改access_flags_,加上kAccNative | kAccCompileDontBother ,删掉kAccCriticalNative | kAccFastNative | kAccNterpEntryPointFastPathFlag
    • kAccCompileDontBother 防止函数调用次数多,被判定为热点函数后编译。
    • 删掉一些flags防止走奇怪的调用路径
  • ptr_sized_fields_.data 指向hook函数
  • ptr_sized_fields_.entry_point_from_quick_compiled_code_ 设为quick_generic_jni_trampoline_
  • 在过程开始时创建ScopedSuspendAll 对象(暂停所有线程),结束时销毁
scopedSuspendAll = reinterpret_cast<void (*)(void *, const char *, bool)>(
    elfImg.GetSymbolAddress("_ZN3art16ScopedSuspendAllC2EPKcb"));
scopedSuspendAllD = reinterpret_cast<void (*)(void *)>(
    elfImg.GetSymbolAddress("_ZN3art16ScopedSuspendAllD2Ev"));
char tmp[128] = {}; //开辟栈上空间
scopedSuspendAll(tmp, "Hook install", false); //构造函数
scopedSuspendAllD(tmp); //析构

目标3:提供调用备份的ArtMethod 的方法

调用备份的ArtMethod 的invoke方法

void Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result, const char* shorty)

这里的args中,基本类型都按照其原始大小存储,对象类型,则只存低32位(vm保证对象地址前32位为0)。非静态方法第一个参数为this

args_size是字节数

shorty是返回类型与参数类型,写成jni那种每个类型由一个字母表示

调用前要创建一个ScopedGcCriticalSection,结束后销毁。与上面的ScopedSuspendAll类似。

对象类型的返回值需要创建一个LocalRef 来包装(必须是libart中的LocalRef ,不能是NDK导出的那个)

为了方便调用,创建一个函数解析std::vector<std::any> ,并封装args

int shorty_idx = 1;
for (const auto &p : args_vec) {
  if (shorty_idx >= shorty.length()) {
    break;
  }
  char type = shorty[shorty_idx];
  if (type == 'F') { // float
    if (p.type() == typeid(float)) {
      float value = std::any_cast<float>(p);
      memcpy((void *)((uintptr_t)args + current_offset), &value,
             sizeof(float));
    }
    current_offset += 4;
  } else if ......
  shorty_idx++;
}
 
jvalue result = call_orig_method_internal(method_sig, args, args_size);
delete[] args;
return result;

测试

写了两个函数测试

第一个测试字符串对象,第二个带参数测试

test1

存在的问题

测试覆盖不全面,且缺乏持续调用的稳定性测试

参考