ART hook 框架 - YAHFA 源码分析

一些参考资料

YAHFA 作者写了两篇文章,可以作为参考:
YAHFA--ART环境下的Hook框架
在Android N上对Java方法做hook遇到的坑

backupAndHookNative

backupAndHookNative 是 YAHFA 执行 hook 操作的主入口:

private static native boolean backupAndHookNative(Object target, Method hook, Method backup);
// HookMain.c
jboolean Java_lab_galaxy_yahfa_HookMain_backupAndHookNative(JNIEnv *env, jclass clazz,
                                                            jobject target, jobject hook,
                                                            jobject backup) {
    if (!doBackupAndHook(
            getArtMethod(env, target),
            getArtMethod(env, hook),
            getArtMethod(env, backup)
    )) {
        (*env)->NewGlobalRef(env, hook); // keep a global ref so that the hook method would not be GCed
        if(backup) (*env)->NewGlobalRef(env, backup);
        return JNI_TRUE;
    } else {
        return JNI_FALSE;
    }
}
  • getArtMethod 通过 method 对象,拿到对应的 ArtMethod。
  • doBackupAndHook 执行入口点的替换:
    1. 调用 target 的时候,直接执行的是 hook
    2. 调用 backup 的时候实际执行的是 target

doBackupAndHook 方法执行后,就完成了对 target 方法的 hook。getArtMethod 和 doBackupAndHook 下文再分别叙述。

getArtMethod

// HookMain.c
static void *getArtMethod(JNIEnv *env, jobject jmethod) {
    void *artMethod = NULL;

    if(jmethod == NULL) {
        return artMethod;
    }

    if(SDKVersion == __ANDROID_API_R__) {
        artMethod = (void *) (*env)->GetLongField(env, jmethod, fieldArtMethod);
    }
    else {
        artMethod = (void *) (*env)->FromReflectedMethod(env, jmethod);
    }

    LOGI("ArtMethod: %p", artMethod);
    return artMethod;

}
  • Android 11 以下,jmethodID 就是 ArtMethod 的地址,所以直接 FromReflectedMethod 就可以得到 ArtMethod
  • Android 11 引入了 index id,FromReflectedMethod 返回的可能是一个 index。这里转为拿 Executable(Java) 的 artMethod 字段的值:
public abstract class Executable extends AccessibleObject
    implements Member, GenericDeclaration {

    // ...

    /**
     * The ArtMethod associated with this Executable, required for dispatching due to entrypoints
     * Classloader is held live by the declaring class.
     */
    @SuppressWarnings("unused") // set by runtime
    private long artMethod;
}

public final class Method extends Executable  { ... }

doBackupAndHook

static int doBackupAndHook(void *targetMethod, void *hookMethod, void *backupMethod) {
    LOGI("target method is at %p, hook method is at %p, backup method is at %p",
         targetMethod, hookMethod, backupMethod);

    int res = 0;

    // set kAccCompileDontBother for a method we do not want the compiler to compile
    // so that we don't need to worry about hotness_count_
    if (SDKVersion >= __ANDROID_API_N__) {
        setNonCompilable(targetMethod);
//        setNonCompilable(hookMethod);
        if(backupMethod) setNonCompilable(backupMethod);
    }

    if (backupMethod) {// do method backup
        // we use the same way as hooking target method
        // hook backup method and redirect back to the original target method
        // the only difference is that the entry point is now hardcoded
        // instead of reading from ArtMethod struct since it's overwritten
        res += replaceMethod(backupMethod, targetMethod, 1);
    }

    res += replaceMethod(targetMethod, hookMethod, 0);

    LOGI("hook and backup done");
    return res;
}
  • setNonCompilable 用于给 ArtMethod 的 access_flag 设置 kAccCompileDontBother标志。这样可以禁止 ART 对 target method 进行 JIT 编译,不然 JIT 的时候会发现我们把方法替换了。
  • replaceMethod 把第一个参数对应的方法“替换”成第二个参数。(第三个参数是 isBackup)。参考下文

replaceMethod

replaceMethod 把 from 的入口点,替换成 to

static int replaceMethod(void *fromMethod, void *toMethod, int isBackup) {
    LOGI("replace method from %p to %p", fromMethod, toMethod);

    // replace entry point
    void *newEntrypoint = NULL;
    if(isBackup) {
        void *originEntrypoint = readAddr((char *) toMethod + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod);
        // entry point hardcoded
        newEntrypoint = genTrampoline(toMethod, originEntrypoint);
    }
    else {
        // entry point from ArtMethod struct
        newEntrypoint = genTrampoline(toMethod, NULL);
    }

    LOGI("replace entry point from %p to %p",
         readAddr((char *) fromMethod + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod),
         newEntrypoint
    );
    if (newEntrypoint) {
        writeAddr((char *) fromMethod + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod,
                newEntrypoint);
    } else {
        LOGE("failed to allocate space for trampoline of target method");
        return 1;
    }

    if (OFFSET_entry_point_from_interpreter_in_ArtMethod != 0) {
        void *interpEntrypoint = readAddr((char *) toMethod + OFFSET_entry_point_from_interpreter_in_ArtMethod);
        writeAddr((char *) fromMethod + OFFSET_entry_point_from_interpreter_in_ArtMethod,
                interpEntrypoint);
    }

    // set the target method to native so that Android O wouldn't invoke it with interpreter
    if(SDKVersion >= __ANDROID_API_O__) {
        uint32_t access_flags = getFlags(fromMethod);
        uint32_t old_flags = access_flags;
        if (SDKVersion >= __ANDROID_API_Q__) {
            // On API 29 whether to use the fast path or not is cached in the ART method structure
            access_flags &= ~kAccFastInterpreterToInterpreterInvoke;
        }
        // MakeInitializedClassesVisiblyInitialized is called explicitly
        // entry of jni methods would not be set to jni trampoline after hooked
//        if (SDKVersion <= __ANDROID_API_Q__) {
            // We don't set kAccNative on R+ because they will try to load from real native method pointer instead of entry_point_from_quick_compiled_code_.
            // Ref: https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:art/runtime/art_method.h;l=844;bpv=1;bpt=1
            access_flags |= kAccNative;
//        }
        setFlags(fromMethod, access_flags);
        LOGI("change access flags from 0x%x to 0x%x", old_flags, access_flags);
    }

    return 0;

}
  • genTrampoline 生成一段跳板代码作为入口点
    • 如果是 backup 方法,入口点硬编码为 target 的入口点
    • 如果是 hook 的话,入口点将在运行时从 hook 的 ArtMethod 结构中获取
    • 详情参考下文
  • 替换 from 的 entry_point_from_quick_compiled_code
  • 替换 from 的 entry_point_from_interpreter(小于等于 Android 6.0)

由于我们会把 target method 的 entry point 替换成了 trampoline,这里先将 target method 的 entry point 硬编码到 backup method 对应的 trampoline,就相当于保存了 target 的 entry point。

另一方面,hook method 的 entry point 不会被替换,所以为调用 hook method 生成的 trampoline 可以动态地从它的 ArtMethod 读取 entry point。

genTrampleline

void *genTrampoline(void *toMethod, void *entrypoint) {
    size_t trampolineSize = entrypoint != NULL ? sizeof(trampolineForBackup) : sizeof(trampoline);

    // check available space for new trampoline
    if(currentTrampolineOff+trampolineSize > trampolineSpaceEnd) {
        currentTrampolineOff = allocTrampolineSpace();
        if (currentTrampolineOff == NULL) {
            return NULL;
        } else {
            trampolineSpaceEnd = currentTrampolineOff + TRAMPOLINE_SPACE_SIZE;
        }
    }

    unsigned char *targetAddr = currentTrampolineOff;

    if(entrypoint != NULL) {
        memcpy(targetAddr, trampolineForBackup, sizeof(trampolineForBackup));
    }
    else {
        memcpy(targetAddr, trampoline,
               sizeof(trampoline)); // do not use trampolineSize since it's a rounded size
    }

    // replace with the actual ArtMethod addr
#if defined(__i386__)
    if(entrypoint) {
        memcpy(targetAddr + 1, &toMethod, pointer_size);
        memcpy(targetAddr + 6, &entrypoint, pointer_size);
    }
    else {
        memcpy(targetAddr + 5, &toMethod, pointer_size);
    }

#elif defined(__x86_64__)
    if(entrypoint) {
        memcpy(targetAddr + 2, &entrypoint, pointer_size);
        memcpy(targetAddr + 13, &toMethod, pointer_size);
    }
    else {
        memcpy(targetAddr + 6, &toMethod, pointer_size);
    }

#elif defined(__arm__)
    if(entrypoint) {
        memcpy(targetAddr + 20, &entrypoint, pointer_size);
        memcpy(targetAddr + 16, &toMethod, pointer_size);
    }
    else {
        memcpy(targetAddr + 12, &toMethod, pointer_size);
    }

#elif defined(__aarch64__)
    if(entrypoint) {
        memcpy(targetAddr + 20, &entrypoint, pointer_size);
        memcpy(targetAddr + 12, &toMethod, pointer_size);
    }
    else {
        memcpy(targetAddr + 16, &toMethod, pointer_size);
    }
#else
#error Unsupported architecture
#endif

    // skip 4 bytes of code_size_
    if(entrypoint == NULL) {
        targetAddr += 4;
    }

    // keep each trampoline aligned
    currentTrampolineOff += roundUpToPtrSize(trampolineSize);

    return targetAddr;
}

trampolineForBackup

对于 aarch64,trampolineForBackup 如下:

// 60 00 00 58 ; ldr x0, 12
// 90 00 00 58 ; ldr x16, 16
// 00 02 1f d6 ; br x16
// 78 56 34 12
// 89 67 45 23 ; 0x2345678912345678 (addr of the hook method)
// 78 56 34 12
// 89 67 45 23 ; 0x2345678912345678 (original entry point of the target method)
unsigned char trampolineForBackup[] = {
        0x60, 0x00, 0x00, 0x58,
        0x90, 0x00, 0x00, 0x58,
        0x00, 0x02, 0x1f, 0xd6,
        0x78, 0x56, 0x34, 0x12,
        0x89, 0x67, 0x45, 0x23,
        0x78, 0x56, 0x34, 0x12,
        0x89, 0x67, 0x45, 0x23
};

对于 backup,执行完下面两行代码后,0x2345678912345678 分别变成了 toMethod 和 entrypoint 的地址

memcpy(targetAddr + 20, &entrypoint, pointer_size);
memcpy(targetAddr + 12, &toMethod, pointer_size);

ldr 在这里使用的是 PC-relative 寻址,第一个指令加载 &toMethod 到 x0,第二个加载 &entrypoint 到 x16,然后跳转到 x16 的

当我们调用 backup 的时候,虚拟机准备好方法的执行环境,然后跳转到 backup 的 entry point,也就是这一段 trampoline:

  1. 调用一个方法时,x0 寄存器存放 callee 的 ArtMethod。ldr x0, 12把 x0 替换回 target method
  2. ldr x16, 16把 target method 的 entry point 加载到 x16,跟着跳转到该地址去执行

这样一来,就相当于直接调用 target method。

trampoline

// 60 00 00 58 ; ldr x0, 12
// 10 00 40 F8 ; ldr x16, [x0, #0x00]
// 00 02 1f d6 ; br x16
// 78 56 34 12
// 89 67 45 23 ; 0x2345678912345678 (addr of the hook method)
unsigned char trampoline[] = {
        0x00, 0x00, 0x00, 0x00, // code_size_ in OatQuickMethodHeader
        0x60, 0x00, 0x00, 0x58,
        0x10, 0x00, 0x40, 0xf8,
        0x00, 0x02, 0x1f, 0xd6,
        0x78, 0x56, 0x34, 0x12,
        0x89, 0x67, 0x45, 0x23
};

对于 hook method,随后 trampoline 后面的 0x2345678912345678 会变成 toMethod 的地址:

memcpy(targetAddr + 16, &toMethod, pointer_size);

替换 entry point 后,调用 target method 会执行到这一段 trampoline:

  1. ldr x0, 12 加载 &toMethod 到 x0,也就是把 x0 从 target 换成 hook method
  2. ldr x16, [x0, #0x00] 加载 toMethod 的 entry point 到 x16,跟着跳转到这个地址。

慢着,这里加载 ArtMethod 的第一个 double word 作为目的地址,但 ArtMethod 的第一个字段是 declaring_class_。按道理,这里应该加载 hook method 的 entry point 才对。也就是说,这里的 offset 不应该是 0。

再看看代码,可以发现在初始化的时候我们调用了 setupTrampoline。offset 即是在这里设置的:

void Java_lab_galaxy_yahfa_HookMain_init(JNIEnv *env, jclass clazz, jint sdkVersion) {
    // ...

    setupTrampoline(OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod);
}

void setupTrampoline(uint8_t offset) {
#if defined(__i386__)
    trampoline[11] = offset;
#elif defined(__x86_64__)
    trampoline[16] = offset;
#elif defined(__arm__)
    trampoline[8] = offset;
#elif defined(__aarch64__)
    trampoline[9] |= offset << 4;
    trampoline[10] |= offset >> 4;
#else
#error Unsupported architecture
#endif
}

你可能感兴趣的:(ART hook 框架 - YAHFA 源码分析)