目前简单找了一下 Android 上的 ART Hook Epic 和 YAHFA 较为流行,支持的版本也比较好 5.0 - 9.0,所以看了一下 YAHFA 源码并且跑了一下 demo。
使用中发现在 5.0 - 6.0 的 64 位机器上会挂掉,调试了一下发现 YAHFA 在试图获取 dexCacheResolvedMethods 列表时读到了非法地址。
因为 ArtMethod -> dex_cache_resolved_methods 中的内容只是一个指针,在 5.0 - 6.0 指向 GCRoot 包裹着的 ArtMethod 数组,而在 64 位下,YAHFA 没有算出正确的 GCRoot 到数组开始的偏移,导致了问题的发生。
也有人发现了同样的问题:https://github.com/rk700/YAHFA/issues/61
我的 Pr 已经被 merge,详见:
https://github.com/rk700/YAHFA/pull/93
https://github.com/rk700/YAHFA/pull/94
在阅读源码的时候发现 ArtMethod -> dex_cache_resolved_methods 目的是缓存该 Dex 下的 ArtMethod,以加速方法跳转。
当方法调用发生时,被编译成 native code 的跳转代码会从 Caller 的 ArtMethod -> dex_cache_resolved_methods[Callee 的 ArtMethod -> dex_method_index] 获得调用者的 ArtMethod。
而这个 dex_cache_resolved_methods 每个 Dex 唯有一份,来自于 Dex 的 DexCache 类,而 DexCache 是 Java 层也有 DexCache 的镜像,所以,我们可以在 java 层操作 dex_cache_resolved_methods 数组。好处在于不需要处理偏移了,不存在兼容问题。
final class DexCache {
43 /** Lazily initialized dex file wrapper. Volatile to avoid double-check locking issues. */
44 private volatile Dex dex;
45
46 /** The location of the associated dex file. */
47 String location;
48
49 /**
50 * References to fields as they become resolved following interpreter semantics. May refer to
51 * fields defined in other dex files.
52 */
53 ArtField[] resolvedFields;
54
55 /**
56 * References to methods as they become resolved following interpreter semantics. May refer to
57 * methods defined in other dex files.
58 */
59 ArtMethod[] resolvedMethods;
60}
实际调试出来是 long[]
final class DexCache {
41 /** Lazily initialized dex file wrapper. Volatile to avoid double-check locking issues. */
42 private volatile Dex dex;
43
44 /** The location of the associated dex file. */
45 String location;
46
47 /**
48 * References to methods as they become resolved following interpreter semantics. May refer to
49 * methods defined in other dex files.
50 */
51 Object resolvedMethods;
public class HookMethodResolver {
public static Class artMethodClass;
public static Field resolvedMethodsField;
public static Field dexCacheField;
public static Field dexMethodIndexField;
public static Field artMethodField;
public static boolean canResolvedInJava = false;
public static boolean isArtMethod = false;
public static long resolvedMethodsAddress = 0;
public static int dexMethodIndex = 0;
public static Method testMethod;
public static Object testArtMethod;
public static void init() {
checkSupport();
}
private static void checkSupport() {
try {
testMethod = HookMethodResolver.class.getDeclaredMethod("init");
artMethodField = getField(Method.class, "artMethod");
testArtMethod = artMethodField.get(testMethod);
if (hasJavaArtMethod() && testArtMethod.getClass() == artMethodClass) {
checkSupportForArtMethod();
isArtMethod = true;
} else if (testArtMethod instanceof Long) {
checkSupportForArtMethodId();
isArtMethod = false;
} else {
canResolvedInJava = false;
}
} catch (Exception e) {
e.printStackTrace();
}
}
// may 5.0
private static void checkSupportForArtMethod() throws Exception {
dexMethodIndexField = getField(artMethodClass, "dexMethodIndex");
dexCacheField = getField(Class.class, "dexCache");
Object dexCache = dexCacheField.get(testMethod.getDeclaringClass());
resolvedMethodsField = getField(dexCache.getClass(), "resolvedMethods");
if (resolvedMethodsField.get(dexCache) instanceof Object[]) {
canResolvedInJava = true;
}
}
// may 6.0
private static void checkSupportForArtMethodId() throws Exception {
dexMethodIndexField = getField(Method.class, "dexMethodIndex");
dexMethodIndex = (int) dexMethodIndexField.get(testMethod);
dexCacheField = getField(Class.class, "dexCache");
Object dexCache = dexCacheField.get(testMethod.getDeclaringClass());
resolvedMethodsField = getField(dexCache.getClass(), "resolvedMethods");
Object resolvedMethods = resolvedMethodsField.get(dexCache);
if (resolvedMethods instanceof Long) {
canResolvedInJava = false;
resolvedMethodsAddress = (long) resolvedMethods;
} else if (resolvedMethods instanceof long[]) {
canResolvedInJava = true;
}
}
public static void resolveMethod(Method hook, Method backup) {
if (canResolvedInJava && artMethodField != null) {
// in java
try {
resolveInJava(hook, backup);
} catch (Exception e) {
// in native
resolveInNative(hook, backup);
}
} else {
// in native
resolveInNative(hook, backup);
}
}
private static void resolveInJava(Method hook, Method backup) throws Exception {
Object dexCache = dexCacheField.get(hook.getDeclaringClass());
if (isArtMethod) {
Object artMethod = artMethodField.get(backup);
int dexMethodIndex = (int) dexMethodIndexField.get(artMethod);
Object resolvedMethods = resolvedMethodsField.get(dexCache);
((Object[])resolvedMethods)[dexMethodIndex] = artMethod;
} else {
int dexMethodIndex = (int) dexMethodIndexField.get(backup);
Object resolvedMethods = resolvedMethodsField.get(dexCache);
long artMethod = (long) artMethodField.get(backup);
((long[])resolvedMethods)[dexMethodIndex] = artMethod;
}
}
private static void resolveInNative(Method hook, Method backup) {
HookMain.ensureMethodCached(hook, backup);
}
public static Field getField(Class topClass, String fieldName) throws NoSuchFieldException {
while (topClass != null && topClass != Object.class) {
try {
Field field = topClass.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (Exception e) {
}
topClass = topClass.getSuperclass();
}
throw new NoSuchFieldException(fieldName);
}
public static boolean hasJavaArtMethod() {
if (artMethodClass != null)
return true;
try {
artMethodClass = Class.forName("java.lang.reflect.ArtMethod");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
YAHFA 主要原理即是操作 ArtMethod 结构体,而问题在于内部每个元素的偏移都是根据每个版本的 AOSP 写死的,这样是有兼容风险的。
较好的实现方式可以动态的去搜索结构体中的内容。
比如 ArtMethod 结构体的大小可以使用两个相邻的方法相减得出,因为他们在内存中是相临的。
accessFlag 我们可以静态的算出某个方法对应的值,这样就可以根据值搜索到偏移
。。。。