Android ART hook 框架 YAHFA 的 Bug 修复以及改进建议

YAHFA

目前简单找了一下 Android 上的 ART Hook Epic 和 YAHFA 较为流行,支持的版本也比较好 5.0 - 9.0,所以看了一下 YAHFA 源码并且跑了一下 demo。

Bug

使用中发现在 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 数组。好处在于不需要处理偏移了,不存在兼容问题。

  • 5.0 上时 ArtMethod 数组
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}
  • 6.0 上则是 ArtMethod 在 Native 层的 mirror 的指针

实际调试出来是 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 我们可以静态的算出某个方法对应的值,这样就可以根据值搜索到偏移

。。。。

你可能感兴趣的:(Android,Android技术)