吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1429|回复: 11
上一主题 下一主题
收起左侧

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

[复制链接]
跳转到指定楼层
楼主
n1ng 发表于 2026-3-25 15:32 回帖奖励

前言

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

项目地址:https://github.com/x1aon1ng/Nook

目标

暂时我们只关心Java Hook的核心部分实现,因此这里的成品只是一个粗糙的hook框架,其他的后面会慢慢再补充上来。在读完这篇文章后,相信你可以问答下面这些问题:

  1. Android上的Java方法最终是如何被执行的
  2. Java Hook到底hook的是什么
  3. Java Hook的核心原理是什么
  4. 一个最小可用的Java Hook框架至少需要哪些部件
  5. Hook框架和注入器分别解决的是什么问题

具体原理和实现

知道这些基础你会更好的理解下文

  1. JNI:即Java Native Interface,Java和native代码交互的标准接口,定义了Java怎么调用C/C++,以及C/C++怎么访问Java对象、类、方法
  2. JNIEnv:是JNI提供给native线程的接口表句柄,其实就是“当前线程操作JVM/Java对象的入口”,所有的JNI调用基本都要通过它,比如FindClass、GetMethodID
  3. Application:在Android中一个app启动后,系统会在他的进程中创建一个Application对象,这个Application是这个app在当前进程的全局入口对象
  4. ClassLoader:Java中的类不是天然就在内存中的,是需要时由类加载器加载进来的,这个类的装载器即ClassLoader,不同的ClassLoader决定了类是否能被找到、属于哪个类空间
  5. ArtMethod:ART运行时中“一个Java方法”的底层描述结构,不是Java层的Method对象,而是ART内部真正决定方法如何执行的native结构,里面会保存方法标志入口地址等关键信息
  6. access_flags:方法或类的访问标志位,描述这个方法有哪些属性比如public、static、native,在Hook中我们往往会修改这个字段来改变方法执行语义
  7. JIT/AOT:JIT即Just-In-Time,运行时即时编译,方法执行时热点代码会在运行过程中被编译为机器码;AOT即Ahead-Of-Time,提前编译,安装或构建阶段就将代码编译为机器码;对Hook来说JIT/AOT会影响方法最后走解释器、quick code还是桥接入口
  8. entry_point_from_jni:ArtMethod里记录JNI/Native路径入口的字段,可以理解为:如果这个方法按JNI/Native语义执行,最后该跳到的native地址
  9. entry_point_from_compiled_code:ArtMethod里记录编译后代码/quick路径入口的字段,可以理解为:方法正常执行时ART从那段compiled/quick代码开始进入
  10. ABI:这里指Application Binary Interface,二进制调用约定
  11. trampoline:跳板代码,一小段中转代码,通常不负责业务逻辑,在Hook中它是原始执行路径和框架处理逻辑之间的桥梁

原理和方案实现

原理介绍

我们从一个非常简单的Java方法来理解Java Hook

package cn.n1ng.javatest;

public class JavaHookTest {
    public int get_num_from_java_method() {
        return 111;
    }
}

如果我们想要把他的返回值从111改为999,表面上看起来非常简单,但从运行视角来看,它背后发生了这些事情:

  1. 找到JavaHookTest.get_num_from_java_method()这一个方法
  2. 找到它在ART里的ArtMethod
  3. 保留原始实现,做好备份
  4. 改写方法入口让他先进入我们自己的trampoline
  5. 在trampoline里把调用现场给接住
  6. 解析参数和返回值类型
  7. 进入我们的hook回调
  8. 在回调里修改返回值

在这个过程中需要解决的是两个核心问题:

首先是方法接管

即解决怎么让目标方法执行时先到我这里来的问题

然后是调用还原

即解决“来到我这里”之后,我怎么知道原方法的参数、返回值怎么处理、原方法还能不能继续调用的问题

所以在框架外看起来可能只是一个简单的数字替换,在内部实现往往会牵扯到ArtMethod、trampoline、ABI、参数解析、backup、调用桥接等问题,解决这些问题是实现Java Hook的最低门槛。

具体实现

上文我们已经知道了实现一次hook需要经历哪些流程,由此我们可以总结出一个最小可用的Java Hook框架至少需要下面这些部件。

方法定位

第一步首先是需要先找到目标方法,他需要解决的问题是如何从类名、方法名、签名定位到Java方法,又如何从Java方法进一步拿到ART内部的方法。即:

  1. FindClass
  2. GetMethodID
  3. jmethodID -> ArtMethod

用户需要给出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()方法真正加载目标类。

    // 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;
    }

再接着是FindMethod,其实就是获取methodID,我们此时已经拿到了jclass,并且知道了方法名、方法签名,直接通过env->GetMethodID获取即可,顺便把签名转化为一种更简单的格式,后面记作shorty。

    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());

在ART中,jmethodID只是一个中间桥梁,真正需要改写的目标是ArtMethod。

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;
}

运行时结构识别

我们最终要改的不是Java对象,而是ART内部结构和入口字段,因此需要识别出ArtMethod大小、access_flags偏移、entry_point_from_compiled_code偏移等关键信息

这个运行时结构信息我们一部分通过解析libart符号获取,一部分通过运行时探测结构偏移获取,最后把这些结构统一存储到ArtInternals中供后续Hook使用

我们可以设计几个结构体来存储相关运行时布局信息

  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;
  };

其中ArtRuntimeSpecOffsets描述的是Runtime里几个关键成员的偏移,比如heap、threadList、classLinker等;ClassLinkerSpecOffsets描述的是ClassLinker里几个关键trampoline的偏移,ArtMethodSpec描述的是ArtMethod里真正要改写的字段偏移:access_flags,entry_jni,entry_quick,ArtMethod大小。

ArtInternals的命名空间记录了结果的存储

  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};

大致是靠这几种方法找到的:

  1. 符号解析:从libart.so直接拿函数/全局符号,比如Runtime::instance_,DecodeMethodId,ArtMethod::Invoke
  2. 锚点扫描:用已知对象值作为锚点反推结构偏移,比如JavaVM*是外部已知值,如果Runtime某一段内存里有个成员正好等于它,那这个点就能作为结构定位锚点反推Runtime
  3. 样本方法特征识别:用一个已知方法的flags/JNI入口特征取探测ArtMethod布局
        void *art_method = ArtInternals::DecodeFunc(ArtInternals::jniIDManager, mid);
        if (!art_method) {
            LOGE("Failed to decode art_method");
            return false;
        }

        uintptr_t base = reinterpret_cast<uintptr_t>(art_method);
        uintptr_t entry_jni_offset = 0;
        uintptr_t access_flags_offset = 0;
        size_t found = 0;

        const uint32_t expected_flags =
                kAccPublic | kAccStatic | kAccFinal | kAccNative;
        const uint32_t flags_mask = 0x0000FFFF;

        for (size_t offset = 0; offset < 64; offset += 4) {
            uintptr_t addr = base + offset;

            // 1. check if it's a pointer into libandroid_runtime.so
            void *maybe_ptr = *reinterpret_cast<void **>(addr);
            if (tool::is_in_module(maybe_ptr, "libandroid_runtime.so")) {
                entry_jni_offset = offset;
                found++;
                LOGI("Found: entry_jni_offset = 0x%lx", offset);
            }

            // 2. check if it looks like access_flags
            uint32_t maybe_flags = *reinterpret_cast<uint32_t *>(addr);
            if ((maybe_flags & flags_mask) == expected_flags) {
                access_flags_offset = offset;
                found++;
                LOGI("Found: access_flags_offset = 0x%lx (flags = 0x%x)", offset, maybe_flags);
            }

            if (found == 2) break;
        }

        if (found != 2) {
            LOGE("Failed to detect ArtMethod field layout");
            return false;
        }

        // 3. quick_code entry offset is next pointer
        uintptr_t entry_quick_offset = entry_jni_offset + pointer_size;

        output->offset_entry_jni = entry_jni_offset;
        output->offset_access_flags = access_flags_offset;
        output->offset_entry_quick = entry_quick_offset;
        output->art_method_size = entry_quick_offset + pointer_size;
        output->interpreterCode = output->offset_entry_jni - pointer_size;

原方法备份

如果我们没有做原方法的备份,仅仅只是做了修改目标方法入口,当后面再想调用原方法的时候,可能就无从找起了,因此需要提前做好backup。

先读原始字段

  uint64_t* quickCode = (uint64_t*)((char*)artMethod +
  ArtInternals::ArtMethodLayout.offset_entry_quick);
  uint32_t orgFlag = *(uint32_t*)((char*)artMethod +
  ArtInternals::ArtMethodLayout.offset_access_flags);
  uint64_t* jni = (uint64_t*)((char*)artMethod + ArtInternals::ArtMethodLayout.offset_entry_jni);

然后放入HookInfo中

  HookInfo info = {
      className,
      methodName,
      detectedShorty,
      isStatic,
      artMethod,
      nullptr,
      trampoline,
      *quickCode,
      *jni,
      orgFlag,
      0,
      0,
      0,
      false,
      ArtInternals::ArtMethodLayout,
      methodID,
      callback,
      true
  };

然后按探测到的ArtMethod大小分配一块新内存,把当前目标ArtMethod整块复制过去

static void* allocate_backup_artmethod(const HookInfo& hookInfo) {
    auto backup = new uint8_t[hookInfo.layout.art_method_size];
    if (!backup) {
        LOGE("Failed to allocate backup ArtMethod");
        return nullptr;
    }

    memcpy(backup, hookInfo.artMethod, hookInfo.layout.art_method_size);
    return backup;
}

static void sync_backup_artmethod(HookInfo& hookInfo) {
    if (!hookInfo.backupValid || !hookInfo.backupArtMethod) {
        return;
    }

    auto currentFlag = *reinterpret_cast<uint32_t*>((char*)hookInfo.artMethod + hookInfo.layout.offset_access_flags);
    auto currentQuick = *reinterpret_cast<uint64_t*>((char*)hookInfo.artMethod + hookInfo.layout.offset_entry_quick);
    auto currentJni = *reinterpret_cast<uint64_t*>((char*)hookInfo.artMethod + hookInfo.layout.offset_entry_jni);

    if (currentQuick != 0 && currentQuick != hookInfo.hookedEntryPoint) {
        hookInfo.orgEntryPoint = currentQuick;
    }
    if (currentJni != 0 && currentJni != hookInfo.hookedJNIEntry) {
        hookInfo.orgJNIEntry = currentJni;
    }
    if (currentFlag != hookInfo.hookedFlag) {
        hookInfo.orgFlag = currentFlag;
    }

    recover_artmethod(hookInfo.backupArtMethod, hookInfo, true);
}

但我们最终想要的backup不是目标方法某一时刻的机械拷贝,而是一份可以代表原方法执行路径、并且可以被Invoke稳定调用的ArtMethod,所以需要调用recover_artmethod方法,恢复原始access_flags、entry_quick、entry_jni,并且还加上了一个kAccCompileDontBother标志,可以让其更稳定,减少运行时/JIT对他做额外处理

  static void recover_artmethod(void* ArtmethodToRecover, HookInfo& hookInfo, bool tempRecover =
  false) {
      if (tempRecover) {
          *reinterpret_cast<uint32_t*>((char*)ArtmethodToRecover +
  hookInfo.layout.offset_access_flags)
              = hookInfo.orgFlag | kAccCompileDontBother;
      } else {
          *reinterpret_cast<uint32_t*>((char*)ArtmethodToRecover +
  hookInfo.layout.offset_access_flags)
              = hookInfo.orgFlag;
      }
      *reinterpret_cast<uint64_t*>((char*)ArtmethodToRecover + hookInfo.layout.offset_entry_quick)
          = hookInfo.orgEntryPoint;
      *reinterpret_cast<uint64_t*>((char*)ArtmethodToRecover + hookInfo.layout.offset_entry_jni)
          = hookInfo.orgJNIEntry;
  }

入口改写

这里是真正让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,这样形成一个完整路径

  目标方法调用
      ->
  entry_quick
      ->
  quickGenericJniTrampoline
      ->
  entry_jni
      ->
  Nook trampoline
      ->
  hook_handler
uint64_t* quickCode = (uint64_t*)((char*)artMethod + ArtInternals::ArtMethodLayout.offset_entry_quick);
uint32_t orgFlag = *(uint32_t*)((char*)artMethod + ArtInternals::ArtMethodLayout.offset_access_flags);
uint64_t* jni = (uint64_t*)((char*)artMethod + ArtInternals::ArtMethodLayout.offset_entry_jni);

info.hookedFlag = getModifiedFlag(orgFlag);
info.hookedJNIEntry = (uint64_t)trampoline;
info.hookedEntryPoint = (uint64_t)quickEntry;

write_hooked_artmethod(info.artMethod,
                       info,
                       info.hookedFlag,
                       info.hookedJNIEntry,
                       info.hookedEntryPoint);
  static void write_hooked_artmethod(void* artMethod,
                                     const HookInfo& hookInfo,
                                     uint32_t hookedFlag,
                                     uint64_t hookedJniEntry,
                                     uint64_t hookedQuickEntry) {
      *reinterpret_cast<uint32_t*>((char*)artMethod + hookInfo.layout.offset_access_flags) =
  hookedFlag;
      *reinterpret_cast<uint64_t*>((char*)artMethod + hookInfo.layout.offset_entry_jni) =
  hookedJniEntry;
      *reinterpret_cast<uint64_t*>((char*)artMethod + hookInfo.layout.offset_entry_quick) =
  hookedQuickEntry;
  }

trampoline

即中间跳板,框架不会直接把目标方法的入口改成某个高层回调函数,而是通常会先进入一小段我们自己控制的机器码,这段机器码做的是“现场接管”的工作:保存关键寄存器,带上当前Hook标识、跳到统一的native handler。他是链接ART调用现场和Hook逻辑的桥梁

static uint8_t* GenerateTrampoline(uint64_t hook_id, void* handler_addr) {
    uint8_t* code = (uint8_t*)tool::allocate_exec_mem(TRAMPOLINE_SIZE);
    if (!code) {
        LOGE("Failed to allocate trampoline memory");
        return nullptr;
    }

    uint32_t* inst = (uint32_t*)code;
    int i = 0;

    // stp x0, x1, [sp, #-16]!  - 保存参数
    inst[i++] = 0xA9BF07E0;
    // movz x0, #hook_id
    inst[i++] = 0xD2800000 | ((hook_id & 0xFFFF) << 5);
    // ldr x1, #8
    inst[i++] = 0x58000041;
    // br x1
    inst[i++] = 0xD61F0020;
    // handler_addr 字面量
    void** addr_ptr = (void**)&inst[i++];
    *addr_ptr = handler_addr;

    return code;
}

__attribute__((naked))
void hook_trampoline_ex() {
    asm volatile(
        "mov x15, x0\n"
        "ldp x0, x1, [sp], #16\n"
        "b hook_handler\n"
    );
}

ABI参数解析

一个Hook回调想要好用,最终回是这样的一个类似效果

callback(env, thiz, args, arg_count, result)

但是ART在调用方法时,并不会主动帮我们把参数打包成args[],他只会按照ABI规则把参数放进x0-x7、v0-v7、栈空间当中,因此框架必须自己完成一次还原,先知道目标方法参数类型,再按ABI规则从寄存器/栈中读回来,最后整理成统一的回调参数表示。

比如一个方法

void loadAd(String type, String position)

他在解析时候就需要知道:参数个数为2,两个参数都是对象,对象参数在ART调用现场要按对象引用规则处理。

先看签名转换

static bool signature_to_shorty(const char* signature, std::string* outShorty) {
    size_t index = 1;
    while (index < length && signature[index] != ')') {
        if (!append_shorty_type(signature, length, &index, outShorty)) {
            return false;
        }
        index++;
    }

    std::string returnShorty;
    if (!append_shorty_type(signature, length, &index, &returnShorty) || returnShorty.empty()) {
        return false;
    }
    outShorty->insert(outShorty->begin(), returnShorty[0]);
    return true;
}

然后是参数还原,其实就是从寄存器取,超过8个就从栈里读。比较特殊的就对象参数和this,从寄存器或栈里拿出来的不能直接作为jobject,需要转换成JNI本地引用/jobject

    // 解析参数
    size_t paramCount = hookInfo.shorty.size() - 1; // shorty[0] 是返回值
    HookValue* args = new HookValue[paramCount];
    jobject* ownedLocalRefs = new jobject[paramCount];
    memset(ownedLocalRefs, 0, sizeof(jobject) * paramCount);

    int x_reg_count = 2;  // x0(env), x1(thiz) 已用
    int v_reg_count = 0;
    int stack_reg_count = 0;

    for (size_t i = 0; i < paramCount; i++) {
        char type = hookInfo.shorty[i + 1];

        switch (type) {
            case 'F':  // float
                if (v_reg_count < 8) {
                    double* vregs[] = {&v0, &v1, &v2, &v3, &v4, &v5, &v6, &v7};
                    args[i].f = *(float*)vregs[v_reg_count];
                } else {
                    args[i].f = *(float*)((uint64_t)args_in + stack_reg_count * 8);
                    stack_reg_count++;
                }
                v_reg_count++;
                break;

            case 'D':  // double
                if (v_reg_count < 8) {
                    double* vregs[] = {&v0, &v1, &v2, &v3, &v4, &v5, &v6, &v7};
                    args[i].d = *vregs[v_reg_count];
                } else {
                    args[i].d = *(double*)((uint64_t)args_in + stack_reg_count * 8);
                    stack_reg_count++;
                }
                v_reg_count++;
                break;

            case 'L':  // 对象引用
                {
                    uint64_t obj_ptr;
                    if (x_reg_count <= 7) {
                        uint64_t* xregs[] = {&x2, &x3, &x4, &x5, &x6, &x7};
                        obj_ptr = *xregs[x_reg_count - 2];
                    } else {
                        obj_ptr = *(uint64_t*)((uint64_t)args_in + stack_reg_count * 8);
                        stack_reg_count++;
                    }
                    ownedLocalRefs[i] = create_local_ref_from_stack_ref(env, obj_ptr);
                    args[i].l = ownedLocalRefs[i];
                    x_reg_count++;
                }
                break;

            default:  // 其他整数和指针类型
                if (x_reg_count <= 7) {
                    uint64_t* xregs[] = {&x2, &x3, &x4, &x5, &x6, &x7};
                    args[i].u = *xregs[x_reg_count - 2];
                } else {
                    args[i].u = *(uint64_t*)((uint64_t)args_in + stack_reg_count * 8);
                    stack_reg_count++;
                }
                x_reg_count++;
        }
    }

Hook回调分发

当trampoline把执行流交给统一handler之后框架还要回答一个问题:当前这次的调用属于哪一个Hook?因为一个进程中可能同时安装了多个Hook。所以框架通常需要维护一张运行时记录表,把每个目标方法和它对应的Hook记录关联起来,这里记录表应该有以下内容:目标方法信息,callback指针、原始入口/原始flags、backup method、参数签名信息。当handler收到一次调用之后,就根据hook id或者当前分发地址查到这条记录,然后决定后续怎么走,是修改返回值还是调用原方法。

先查表找到HookInfo(通过之前生成trampoline时存的hookid)

uint64_t tmpHookid;
asm volatile("mov %0, x15" : "=r"(tmpHookid));
uint32_t hookID = (uint32_t)tmpHookid;

HookInfo hookInfo = HookStore<HookInfo>::Instance().CopyByIndex(hookID);
if (!hookInfo.valid) {
    LOGE("Hook %d is invalid", hookID);
    return 0;
}

然后调用用户注册的callback

HookValue directRet = {0};
bool callOriginal = hookInfo.callback(env, callbackThis, args, paramCount, &directRet);

简单总结一下这里的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方法即可。

void* thread = ArtInternals::GetCurrentThread();
auto argsArray = new uint32_t[(paramCount + 2) * 8];
uint32_t argsize = 0;

if (!hookInfo.isStatic) {
    uint32_t compressed_this = 0;
    encode_jobject_to_invoke_ref(thread, callbackThis, &compressed_this);
    argsArray[0] = compressed_this;
    argsize += 4;
}

ArtInternals::Invoke(invokeArtMethod, thread, argsArray, argsize, &result, hookInfo.shorty.c_str());

这里至少有着三层转换:callback拿到的是jobject,Invoke需要的是运行时对象和按shorty排布的参数块,调完之后还要把jvalue result转换为Hook层返回值

小结

到这里我们已经完成了一个虽然非常粗糙但已经初具雏形的Hook框架了,接下来回答刚开始的几个问题:

  1. Android上的Java方法最终是如何被执行的:最终是靠ART运行时中的方法描述结构来执行的,这个核心结构就是ArtMethod,比如当Java层发起一次方法调用时,ART会先拿到这个方法对应的ArtMethod,再根据其中保存的运行时信息去决定执行路径
  2. Java Hook到底hook的是什么:是ART运行时里对这个方法的执行入口
  3. Java Hook的核心原理是什么:接管目标方法在ART中的执行入口,并在必要时保留一条可以回到原始实现的路径
  4. 一个最小可用的Java Hook框架至少需要哪些部件:方法定位、运行时结构识别、原方法备份、入口改写、trampoline、参数解析、Hook回调、原方法调用
  5. Hook框架和注入器分别解决的是什么问题:注入器解决的时怎么把so放进目标进程并执行的问题,Hook框架解决的是进入目标进程之后怎么接管Java方法执行的问题。

TODO

  1. inline式的方案
  2. 更完善的backup
  3. 更简便的脚本化使用模块

免费评分

参与人数 5吾爱币 +5 热心值 +5 收起 理由
chengdragon + 1 + 1 感谢分享
laozhang4201 + 1 + 1 热心回复!
beibeibei + 1 + 1 我很赞同!
WOSHIXIAOAI + 1 + 1 我很赞同!
ehu4ever + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
liyicha 发表于 2026-3-28 09:49
大佬,如果用框架把目标应用和hook模块修补后安装,其他应用识别不到目标应用这个有办法解决吗?
沙发
Trico 发表于 2026-3-25 15:45
3#
hyperx 发表于 2026-3-25 15:58
4#
skynetHX 发表于 2026-3-25 16:33
感谢分享,学习了
5#
smpeaer 发表于 2026-3-25 17:35
学习一下,感谢提供干货
6#
wangdongjiang 发表于 2026-3-25 19:24
学习一下,感谢提供干货
7#
Tsuki0402 发表于 2026-3-25 20:13
最近就在找这个教程
8#
lb00426 发表于 2026-3-25 21:49
学习一下,每日补脑
9#
xingqwq 发表于 2026-3-26 08:21
感谢大佬分享
10#
孤木落 发表于 2026-3-27 08:29
jvmti(instrumentation)也是一种java hook技术的基础哦,可以补充一下
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-4-11 22:09

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表