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 | 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;
测试
写了两个函数测试
第一个测试字符串对象,第二个带参数测试
存在的问题
测试覆盖不全面,且缺乏持续调用的稳定性测试
参考
- 写一个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)