首页
社区
课程
招聘
[原创]从0到1构建一个Hook工具之Java Hook篇(三)
发表于: 2026-3-25 15:31 1101

[原创]从0到1构建一个Hook工具之Java Hook篇(三)

2026-3-25 15:31
1101

在前两篇文章中,我们已经做到了attach和spawn两种模式的注入,你是否还记得,我们在做传统spawn注入的时候用到了一个叫做dobby的框架,当时并没有深入介绍,从这一篇文章开始,我们就将进入真正的Hook部分,这里先从Java世界开始。有描述不对的或者值得改进的欢迎在评论区提出!

项目地址:7e7K9s2c8@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;
    }
}
    // 1) Try normal FindClass
    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();

    // 2) Injection context: use Application ClassLoader
    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};

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 22
支持
分享
最新回复 (8)
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
666
2026-3-25 16:11
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
感谢分享
6天前
0
雪    币: 4290
活跃值: (4920)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
6
6天前
0
雪    币: 212
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
感谢分享
2天前
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
感谢分享
2天前
0
雪    币: 29
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
感谢分享
2天前
0
雪    币: 179
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
mark
1天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
66
16小时前
0
游客
登录 | 注册 方可回帖
返回