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结果
测试长、短跳转两种方案
{width=300px}
::right::
{width=270px}
带递归的函数测试
说明没有破坏调用栈、能实现插桩的效果
{width=600px}
::right::
{width=400px}
存在的问题
使用长跳转的方式修改目标函数 时,若存在“跳转目标为前4条指令”的指令,则无法实现正确调用原函数
参考
- Dobby : https://github.com/jmpews/Dobby
- ShadowHook : https://github.com/bytedance/android-inline-hook
- 以及一些arm手册
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 函数。 |
conflict | ImtConflictTable(接口方法表冲突表)。 |
abstract/interface | 如果存在单一实现(single-implementation),则指向该实现。 |
proxy | 原始的接口方法或构造函数。 |
default conflict | null |
| 其他方法 | 在 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_
{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_.data、ptr_sized_fields_.entry_point_from_quick_compiled_code_
目标2:将该方法标记为Native,设置jni入口点和跳板函数,备份原ArtMethod 结构
- 修改
access_flags_,加上kAccNative | kAccCompileDontBother,删掉kAccCriticalNative | kAccFastNative | kAccNterpEntryPointFastPathFlagkAccCompileDontBother防止函数调用次数多,被判定为热点函数后编译。- 删掉一些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;测试
写了两个函数测试
第一个测试字符串对象,第二个带参数测试

存在的问题
测试覆盖不全面,且缺乏持续调用的稳定性测试
参考
- 写一个Android Hook小框架:https://note.lynnette.uk/article/girlHook
- ART上的动态Java方法hook框架:https://blog.canyie.top/2020/04/27/dynamic-hooking-framework-on-art/
- frida-java-bridge:https://github.com/frida/frida-java-bridge
- Aliucord/hook:https://github.com/Aliucord/hook
- LSPlant: https://github.com/Aliucord/LSPlant (https://github.com/LSPosed/LSPlant)