Android如何绕过反射黑名单

Google 从 Android P 开始引入了针对非公开 API 的限制,这一点可以从 Native 相关的源码中找到限制的原理,从而从中找到解决办法,不过非必要原因不太建议去挑战这种限制,毕竟不清楚在后续的版本中会不会做限制,维护起来挺麻烦的。
在 Native 层有几个访问级别:

class HiddenApiAccessFlags {
    public:
    enum ApiList {
        kWhitelist = 0,
        kLightGreylist,
        kDarkGreylist,
        kBlacklist,
    };
}

另外还有几个对应的响应级别:

enum Action {
    kAllow,         //通过
    kAllowButWarn,  //通过,但日志警告
    kAllowButWarnAndToast,  //通过,且日志警告和弹窗
    kDeny   //拒绝访问
};

这里介绍一下网上的一些解决方式,此外,还可以把我们调用了反射方法的类的类加载器设置为系统类加载器,这样就可以绕过 Native 层的限制了。

系统类伪装

黑名单在系统中有一个 fn_caller_is_trusted 的条件:如果调用者是系统类,那么就允许被调用。即如果我们能以系统类的身份去反射,那么就能畅通无阻:

  1. 首先通过反射 API 拿到 getDeclaredMethod 方法。getDeclaredMethod 是 public 的,不存在问题;这个通过反射拿到的方法网上称之为元反射方法。
  2. 然后通过刚刚的元反射方法去反射调用 getDeclardMethod。这里我们就实现了 以系统身份去反射
    的目的——反射相关的 API 都是系统类,因此我们的元反射方法也是被系统类加载的方法;所以我们的元反射方法调用的 getDeclardMethod 会被认为是系统调用的,可以反射任意的方法。

伪代码如下:

// 公开API,无问题
Method metaGetDeclaredMethod = Class.class.getDeclaredMethod("getDeclardMethod");
// 系统类通过反射使用隐藏 API,检查直接通过。
Method hiddenMethod = metaGetDeclaredMethod.invoke(hiddenClass, "hiddenMethod", "hiddenMethod参数列表");
// 正确找到 Method 直接反射调用
hiddenMethod.invoke

豁免条件

隐藏 API 的调用有「豁免」条件,即只要它是豁免的,则即使它在黑名单中,也会被放行。这种方式暴露给了 Java 层,因此可以通过 VMRuntime.setHiddenApiExemptions 方法来实现。再结合上面这个方法,我们只需要通过 「元反射」 来反射调用 VMRuntime.setHiddenApiExemptions 就能将我们自己要使用的隐藏 API 全部都豁免掉了。另外系统在检查豁免时是通过方法签名进行前缀匹配的,而 Java 方法签名都是 L 开头的,因此我们可以把直接传个 L 进去,那么所有的隐藏API全部被赦免了!

源码直接参考网上大佬的开源项目: FreeReflection

public final class BootstrapClass {

    private static final String TAG = "BootstrapClass";

    private static Object sVmRuntime;
    private static Method setHiddenApiExemptions;

    static {
        if (SDK_INT >= Build.VERSION_CODES.P) {
            try {
                Method forName = Class.class.getDeclaredMethod("forName", String.class);
                Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);

                Class vmRuntimeClass = (Class) forName.invoke(null, "dalvik.system.VMRuntime");
                Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null);
                setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});
                sVmRuntime = getRuntime.invoke(null);
            } catch (Throwable e) {
                Log.w(TAG, "reflect bootstrap failed:", e);
            }
        }
    }

    /**
     * make the method exempted from hidden API check.
     *
     * @param method the method signature prefix.
     * @return true if success.
     */
    public static boolean exempt(String method) {
        return exempt(new String[]{method});
    }

    /**
     * make specific methods exempted from hidden API check.
     *
     * @param methods the method signature prefix, such as "Ldalvik/system", "Landroid" or even "L"
     * @return true if success
     */
    public static boolean exempt(String... methods) {
        if (sVmRuntime == null || setHiddenApiExemptions == null) {
            return false;
        }

        try {
            setHiddenApiExemptions.invoke(sVmRuntime, new Object[]{methods});
            return true;
        } catch (Throwable e) {
            return false;
        }
    }

    /**
     * Make all hidden API exempted.
     *
     * @return true if success.
     */
    public static boolean exemptAll() {
        return exempt(new String[]{"L"});
    }
}