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 执行入口点的替换:
- 调用 target 的时候,直接执行的是 hook
- 调用 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:
- 调用一个方法时,x0 寄存器存放 callee 的 ArtMethod。
ldr x0, 12
把 x0 替换回 target method -
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:
-
ldr x0, 12
加载 &toMethod 到 x0,也就是把 x0 从 target 换成 hook method -
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
}