0%

Android中对Framework层的隐藏Java对象实现方法调用hook

前言

一般情况下,在Java层可以使用动态代理实现方法hook,如下:

interface Animal {
    void eat();
}

Object proxy = Proxy.newProxyInstance(getClassLoader(), new Class[]{Animal.class},
        new InvocationHandler() {
    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        return null;
    }
});
// 反射替换为动态代理对象proxy
//

这种方法只能对接口有效,因为newProxyInstance只能创建针对某个interface的动态代理对象,而无法生成针对某个class的动态代理对象。

对于class对象的方法hook,我们可以继承自该class然后重写对应的方法,然后直接创建新对象去把旧对象替换掉就可以。

如果class是公开的那就没问题,但很多情况下class并不是公开的,比如该class是在Framework层里,虽然它也是public但被@hide标记,导致应用层开发无法引用到,因此需要想方法解决该问题。

针对@hide的class对象实现方法调用hook

比如我们调用Context的getExternalFilesDir方法,会返回APP对应的外部File目录,而它里面具体的调用流程如下:

Context(getExternalFilesDir)

ContextImpl(getExternalFilesDir)

Environment(buildExternalStorageAppFilesDirs)

UserEnvironment(buildExternalStorageAppFilesDirs)

UserEnvironment(buildExternalStorageAppFilesDirs)

UserEnvironment(getExternalDirs)

在Environment类里,有个UserEnvironment对象sCurrentUser,所有的路径其实都是由这个sCurrentUser生成的:

private static UserEnvironment sCurrentUser;

/** {@hide} */
public static class UserEnvironment {

    @UnsupportedAppUsage
    public File[] getExternalDirs() {
        final StorageVolume[] volumes = StorageManager.getVolumeList(mUserId,
                StorageManager.FLAG_FOR_WRITE);
        final File[] files = new File[volumes.length];
        for (int i = 0; i < volumes.length; i++) {
            files[i] = volumes[i].getPathFile();
        }
        return files;
    }
    ......
}

如果我们现在需要getExternalDirs的返回路径,比如默认是sd卡根目录,现在需要返回sd卡根目录下的123文件夹目录,就需要hook这个UserEnvironment的getExternalDirs方法,但这里有两个问题,一个就是UserEnvironment是class而不是interface,无法用动态代理,另一个是UserEnvironment是@hide的App中无法直接引用到。

为了解决@hide这个问题,可以先做一个system.jar,里面放Environment和UserEnvironment:

package android.os;

import java.io.File;

public class Environment {

    public static class UserEnvironment {

        public File[] getExternalDirs() {
            return null;
        }
    }

}

编译成system.jar,需要在App工程中引入,只需要在编译时即可:

compileOnly fileTree(dir: 'libs', includes: ['system.jar'])

按照正常的思路,接着应该在App工程中定义CustomUserEnvironment类,继承自android.os.Environment.UserEnvironment,然后去实现getExternalDirs方法即可,但这里会有个问题:Android系统也有android.os.Environment,会默认去找这个,而不是找我们system.jar里的Environment,因此也就找不到里面的UserEnvironment,因此出现编译失败。

为了解决这个问题,需要再引入一个bridge.jar,里面方法UserEnvironmentBridge:

package com.hook.bridge;

import android.os.Environment;

import java.io.File;

public class UserEnvironmentBridge extends Environment.UserEnvironment {

    @Override
    public File[] getExternalDirs() {
        return super.getExternalDirs();
    }
}

编译成bridge.jar,然后引入App工程,这个需要编译到apk中:

implementation fileTree(dir: 'libs', includes: ['bridge.jar'])

然后在App工程中定义CustomUserEnvironment类:

public class CustomUserEnvironment extends UserEnvironmentBridge {

    private static final String TAG = "CustomUserEnvironment";

    @Override
    public File[] getExternalDirs() {
        File[] files = super.getExternalDirs();
        if (files != null) {
            for (File file : files) {
                // files[0] = new File("/sdcard/", "123");
                Log.i(TAG, file.getAbsolutePath());
            }
            files[0] = new File("/storage/emulated/0/123");
        }
        return files;
    }
}

这样就不会有编译不过的问题了,这里的继承关系为:CustomUserEnvironment -> UserEnvironmentBridge -> android.os.Environment.UserEnvironment

最后创建一个CustomUserEnvironment对象然后把sCurrentUser替换掉即可:

try {
    Field field = Environment.class.getDeclaredField("sCurrentUser");
    field.setAccessible(true);
    field.set(null, new CustomUserEnvironment());
} catch (Throwable t) {
    Log.w(TAG, t);
}

替换之后,外部调用getExternalFilesDir方法,就会走到CustomUserEnvironment的getExternalDirs方法里,然后在这里实现自定义逻辑。

总结

对于某个Class对象,如果要hook其中的方法,可以创建一个继承自该Class的对象,然后把原有的对象替换掉就可以,而如果该Class在Framework层且应用层引用不到,那么就可以采用上述的方法实现替换。

相对于在native层的hook方案(比如Xposed),该方案都是在Java层实现,代码实现简单,不会有机型适配和兼容问题,因此实际项目中会优先使用该方案。