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
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容