在前两篇文章中,我们已经做到了attach和spawn两种模式的注入,你是否还记得,我们在做传统spawn注入的时候用到了一个叫做dobby的框架,当时并没有深入介绍,从这一篇文章开始,我们就将进入真正的Hook部分,这里先从Java世界开始。有描述不对的或者值得改进的欢迎在评论区提出!
项目地址:a76K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5x3h3q4G2L8U0q4F1k6#2)9J5c8V1&6G2L8$3D9`.
暂时我们只关心Java Hook的核心部分实现,因此这里的成品只是一个粗糙的hook框架,其他的后面会慢慢再补充上来。在读完这篇文章后,相信你可以问答下面这些问题:
我们从一个非常简单的Java方法来理解Java Hook
如果我们想要把他的返回值从111改为999,表面上看起来非常简单,但从运行视角来看,它背后发生了这些事情:
在这个过程中需要解决的是两个核心问题:
首先是方法接管
即解决怎么让目标方法执行时先到我这里来的问题
然后是调用还原
即解决“来到我这里”之后,我怎么知道原方法的参数、返回值怎么处理、原方法还能不能继续调用的问题
所以在框架外看起来可能只是一个简单的数字替换,在内部实现往往会牵扯到ArtMethod、trampoline、ABI、参数解析、backup、调用桥接等问题,解决这些问题是实现Java Hook的最低门槛。
上文我们已经知道了实现一次hook需要经历哪些流程,由此我们可以总结出一个最小可用的Java Hook框架至少需要下面这些部件。
方法定位
第一步首先是需要先找到目标方法,他需要解决的问题是如何从类名、方法名、签名定位到Java方法,又如何从Java方法进一步拿到ART内部的方法。即:
用户需要给出class_name, method_name, signature, is_static;首先需要JNIEnv,因为后面无论是FindClass还是GetMethodID都依赖于JNIEnv*,
然后是FindClass,思路是先直接通过env->FindClass,失败后通过ActivityThread.currentApplication()拿到当前应用(这里选择android.app.ActivityThread,因为他是系统类,能拿到当前进程里的Application对象),找到currentApplication方法,调用它拿到当前Application,此时我们拿到了当前目标进程里的Application实例,他是App运行时环境的核心对象之一,通过它可以继续拿到真正属于这个App的ClassLoader,最后调用ClassLoader.loadClass()方法真正加载目标类。
再接着是FindMethod,其实就是获取methodID,我们此时已经拿到了jclass,并且知道了方法名、方法签名,直接通过env->GetMethodID获取即可,顺便把签名转化为一种更简单的格式,后面记作shorty。
在ART中,jmethodID只是一个中间桥梁,真正需要改写的目标是ArtMethod。
运行时结构识别
我们最终要改的不是Java对象,而是ART内部结构和入口字段,因此需要识别出ArtMethod大小、access_flags偏移、entry_point_from_compiled_code偏移等关键信息
这个运行时结构信息我们一部分通过解析libart符号获取,一部分通过运行时探测结构偏移获取,最后把这些结构统一存储到ArtInternals中供后续Hook使用
我们可以设计几个结构体来存储相关运行时布局信息
其中ArtRuntimeSpecOffsets描述的是Runtime里几个关键成员的偏移,比如heap、threadList、classLinker等;ClassLinkerSpecOffsets描述的是ClassLinker里几个关键trampoline的偏移,ArtMethodSpec描述的是ArtMethod里真正要改写的字段偏移:access_flags,entry_jni,entry_quick,ArtMethod大小。
ArtInternals的命名空间记录了结果的存储
大致是靠这几种方法找到的:
原方法备份
如果我们没有做原方法的备份,仅仅只是做了修改目标方法入口,当后面再想调用原方法的时候,可能就无从找起了,因此需要提前做好backup。
先读原始字段
然后放入HookInfo中
然后按探测到的ArtMethod大小分配一块新内存,把当前目标ArtMethod整块复制过去
但我们最终想要的backup不是目标方法某一时刻的机械拷贝,而是一份可以代表原方法执行路径、并且可以被Invoke稳定调用的ArtMethod,所以需要调用recover_artmethod方法,恢复原始access_flags、entry_quick、entry_jni,并且还加上了一个kAccCompileDontBother标志,可以让其更稳定,减少运行时/JIT对他做额外处理
入口改写
这里是真正让hook生效的一步,Java Hook的本质就是接管执行入口,这一步通常有两种方案,一个是replacement,即直接修改ArtMethod里的入口字段,一个是inline hook式的,直接patch编译代码入口处的机器码。我们这里先尝试replacement方案,读出原始access_flags/entry_jni/entry_quick,然后构造HookInfo,最后把目标ArtMethod改写掉。
这里改的核心就三点:flag + entry_jni + entry_quick。用access_flags把方法伪装成native,用entry_jni塞进自己的trampoline,用entry_quick接到ART的quick JNI bridge,这样形成一个完整路径
trampoline
即中间跳板,框架不会直接把目标方法的入口改成某个高层回调函数,而是通常会先进入一小段我们自己控制的机器码,这段机器码做的是“现场接管”的工作:保存关键寄存器,带上当前Hook标识、跳到统一的native handler。他是链接ART调用现场和Hook逻辑的桥梁
ABI参数解析
一个Hook回调想要好用,最终回是这样的一个类似效果
但是ART在调用方法时,并不会主动帮我们把参数打包成args[],他只会按照ABI规则把参数放进x0-x7、v0-v7、栈空间当中,因此框架必须自己完成一次还原,先知道目标方法参数类型,再按ABI规则从寄存器/栈中读回来,最后整理成统一的回调参数表示。
比如一个方法
他在解析时候就需要知道:参数个数为2,两个参数都是对象,对象参数在ART调用现场要按对象引用规则处理。
先看签名转换
然后是参数还原,其实就是从寄存器取,超过8个就从栈里读。比较特殊的就对象参数和this,从寄存器或栈里拿出来的不能直接作为jobject,需要转换成JNI本地引用/jobject
Hook回调分发
当trampoline把执行流交给统一handler之后框架还要回答一个问题:当前这次的调用属于哪一个Hook?因为一个进程中可能同时安装了多个Hook。所以框架通常需要维护一张运行时记录表,把每个目标方法和它对应的Hook记录关联起来,这里记录表应该有以下内容:目标方法信息,callback指针、原始入口/原始flags、backup method、参数签名信息。当handler收到一次调用之后,就根据hook id或者当前分发地址查到这条记录,然后决定后续怎么走,是修改返回值还是调用原方法。
先查表找到HookInfo(通过之前生成trampoline时存的hookid)
然后调用用户注册的callback
简单总结一下这里的Hook回调分发机制:安装Hook时为每一个Hook分配唯一hook_id,并把包含callback的HookInfo存入HookStore,运行时由trampoline把hook_id带入统一hook_handler,再由hook_handler按hook_id查出对应的HookInfo,最终指向这条Hook绑定的callback。
原方法调用
这一步和上面的原方法备份是强相关的,它是backup的“使用方式”。
有了backup之后,框架还需要解决:参数如何编码成ART能接受的形式,如何调用原始ArtMethod,返回值如何转回Hook层可以接受的形式。这里可以借ART自己的Invoke能力,这样可以尽量复用已有的调用机制。
首先需要获取GetCurrentThread()(这个在初始化阶段已经解析出来了),后续ArtMethod::Invoke调用需要当前Art Thread指针(这里原方法调用的方案不是重新回到Java层反射调用,而是直接在ART内部按运行时调用约定指向backup ArtMethod),然后将参数编码为ART Invoke能接受的格式,其实就是和上面参数解析执行相反的操作流程,上一步时从ART对象引用变为JNI本地引用,这里是JNI本地引用转换为ART对象引用。最后调用Invoke方法即可。
这里至少有着三层转换:callback拿到的是jobject,Invoke需要的是运行时对象和按shorty排布的参数块,调完之后还要把jvalue result转换为Hook层返回值
小结
到这里我们已经完成了一个虽然非常粗糙但已经初具雏形的Hook框架了,接下来回答刚开始的几个问题:
package cn.n1ng.javatest;
public class JavaHookTest {
public int get_num_from_java_method() {
return 111;
}
}
std::string slashName;
for (const char* p = className; *p; ++p) {
slashName += (*p == '.') ? '/' : *p;
}
jclass clazz = env->FindClass(slashName.c_str());
if (clazz) return clazz;
if (env->ExceptionCheck()) env->ExceptionClear();
jclass activityThreadClass = env->FindClass("android/app/ActivityThread");
if (!activityThreadClass) {
if (env->ExceptionCheck()) env->ExceptionClear();
return nullptr;
}
jmethodID currentAppMethod = env->GetStaticMethodID(
activityThreadClass, "currentApplication", "()Landroid/app/Application;");
if (!currentAppMethod) {
if (env->ExceptionCheck()) env->ExceptionClear();
return nullptr;
}
jobject application = env->CallStaticObjectMethod(activityThreadClass, currentAppMethod);
if (!application || env->ExceptionCheck()) {
if (env->ExceptionCheck()) env->ExceptionClear();
return nullptr;
}
jclass applicationClass = env->GetObjectClass(application);
jmethodID getClassLoaderMethod = env->GetMethodID(
applicationClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
env->DeleteLocalRef(applicationClass);
if (!getClassLoaderMethod) {
env->DeleteLocalRef(application);
if (env->ExceptionCheck()) env->ExceptionClear();
return nullptr;
}
jobject classLoader = env->CallObjectMethod(application, getClassLoaderMethod);
env->DeleteLocalRef(application);
if (!classLoader || env->ExceptionCheck()) {
if (env->ExceptionCheck()) env->ExceptionClear();
return nullptr;
}
jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
if (!classLoaderClass) {
env->DeleteLocalRef(classLoader);
if (env->ExceptionCheck()) env->ExceptionClear();
return nullptr;
}
jmethodID loadClassMethod = env->GetMethodID(
classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
if (!loadClassMethod) {
env->DeleteLocalRef(classLoader);
env->DeleteLocalRef(classLoaderClass);
if (env->ExceptionCheck()) env->ExceptionClear();
return nullptr;
}
std::string dotName;
for (const char* p = className; *p; ++p) {
dotName += (*p == '/') ? '.' : *p;
}
jstring classNameStr = env->NewStringUTF(dotName.c_str());
jclass loadedClass = (jclass)env->CallObjectMethod(classLoader, loadClassMethod, classNameStr);
env->DeleteLocalRef(classNameStr);
env->DeleteLocalRef(classLoader);
env->DeleteLocalRef(classLoaderClass);
if (loadedClass && !env->ExceptionCheck()) {
LOGI("FindClass via ActivityThread success: %s", className);
return loadedClass;
}
std::string methodSignature = shorty ? shorty : "";
std::string detectedShorty;
if (!signature_to_shorty(methodSignature.c_str(), &detectedShorty)) {
LOGE("Invalid method signature for shorty conversion: %s", methodSignature.c_str());
return {nullptr, ""};
}
jmethodID methodID = isStatic ?
env->GetStaticMethodID(clazz, methodName, methodSignature.c_str()) :
env->GetMethodID(clazz, methodName, methodSignature.c_str());
jclass clazz = FindClass(env, className);
auto [methodID, detectedShorty] = FindMethod(env, clazz, methodName, shorty, isStatic);
void* artMethod = ArtInternals::DecodeFunc(ArtInternals::jniIDManager, methodID);
if (!artMethod) {
LOGE("Failed to decode method ID");
return -1;
}
typedef struct {
intptr_t heap;
intptr_t threadList;
intptr_t internTable;
intptr_t classLinker;
intptr_t jniIdManager;
} ArtRuntimeSpecOffsets;
typedef struct {
intptr_t quickResolutionTrampoline;
intptr_t quickImtConflictTrampoline;
intptr_t quickGenericJniTrampoline;
intptr_t quickToInterpreterBridgeTrampoline;
} ClassLinkerSpecOffsets;
struct ArtMethodSpec {
size_t offset_access_flags;
size_t offset_entry_jni;
size_t offset_entry_quick;
size_t art_method_size;
size_t interpreterCode;
};
DecodeMethodIdFn DecodeFunc = nullptr;
ArtMethodInvoke Invoke = nullptr;
CurrentFromGDB GetCurrentThread = nullptr;
DecodeJObjectFn DecodeJObject = nullptr;
ScopedGCSection SGCFn = nullptr;
destroyScopedGCSection DestroyGCFn = nullptr;
ScopedSuspendAll ScopedSuspendAllFn = nullptr;
destroyScopedSuspendAll destroyScopedSuspendAllFn = nullptr;
newlocalref newlocalrefFn = nullptr;
uintptr_t RuntimeInstance = 0;
void* jniIDManager = nullptr;
ArtMethodSpec ArtMethodLayout = {0};
ArtRuntimeSpecOffsets RunTimeSpec = {0};
ClassLinkerSpecOffsets ClassLinkerSpec = {0};
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!