深入解析 Android 隐藏 API:从 `@hide` 到 Greylist 和 Blacklist

74 阅读8分钟

Android 系统作为一套复杂的开源操作系统,提供了丰富的功能接口满足开发者的需求。但为了维护系统稳定性、安全性和向后兼容性,从Android9开始,官方对部分功能接口进行了隐藏。这些隐藏 API(通过 @hide 标注)虽然在源码中存在,但被排除在官方 SDK 的公共接口之外。开发者可以通过一些技术手段调用隐藏 API,但这可能导致兼容性问题,甚至触发系统的访问限制。

image.png

本文将详细解析隐藏 API 的定义、分类及其访问限制,并从源码角度揭示隐藏 API 的生成、检测和调用方式。同时,也会探讨 greylistblacklist 策略,以及开发者在面对隐藏 API 时的解决方案。

背景

什么是隐藏 API?

隐藏 API 是 Android 系统中不对第三方开发者公开的方法或字段,通常在源代码中通过 @hide 注解标识。这些 API 封装了底层实现细节、硬件访问功能或敏感的系统操作,旨在限制开发者对系统内部的直接依赖。

隐藏 API 的设计初衷

隐藏 API 的存在主要基于以下考虑:

  1. 系统稳定性:隐藏可能会因系统升级而发生变化的接口,防止开发者依赖非稳定的实现。
  2. 安全性:防止敏感功能被滥用,例如直接操作硬件或访问底层资源。
  3. 兼容性:减少系统接口的碎片化,避免设备厂商或开发者通过非公开方法破坏统一性。
  4. 开发灵活性:为系统内部功能保留灵活调整的空间,不受外部依赖的约束。

隐藏 API 的现状

从 Android 9 (Pie) 开始,Google 引入了隐藏 API 执行策略(Hidden API Enforcement Policy),对隐藏 API 的访问进行了更严格的限制,普通应用只能访问特定的灰名单 API,而黑名单 API 则完全禁止。

隐藏 API 的分类与访问策略

隐藏 API 的分类

Google 根据隐藏 API 的用途和敏感性,制定了如下分类规则:

分类访问权限适用范围
Public公开 API,无限制。普通开发者可以访问,包含在 Android SDK 中。
Whitelist白名单 API,通常无限制。系统公开的基础接口,官方鼓励开发者使用。
Light Greylist灰名单 API,普通应用访问时触发警告日志。系统为了兼容性暂时保留的旧 API,可能在未来版本中移除。
Dark Greylist深灰名单 API,仅限系统应用访问。对普通应用隐藏,但系统级应用仍可以使用(如预装的厂商应用)。
Blacklist黑名单 API,完全禁止访问。涉及底层安全性或敏感操作的 API,调用时会抛出 NoSuchMethodErrorIllegalAccessError
Core platform核心平台 API,仅允许 Platform 签名的应用访问。与系统底层运行密切相关的 API,例如核心服务或硬件操作。

隐藏 API 的访问限制

Android 系统通过两种主要机制限制对隐藏 API 的访问:

(1) Hidden API Enforcement

Android 9 开始,系统引入了隐藏 API 执行策略,默认情况下禁止普通应用直接访问隐藏 API。如果尝试访问隐藏 API,会根据 API 的分类触发不同的行为:

  • 灰名单 API:运行时记录警告日志,但允许访问。
  • 黑名单 API:直接抛出异常,应用运行失败。

(2) 签名权限限制

隐藏 API 的访问权限还与应用的签名关系密切。根据应用签名的不同,访问权限分为以下几种:

  1. Platform 签名
    • 最高权限,允许访问所有隐藏 API。
    • 使用系统的 Platform Key 签名,例如 framework.jar
  2. 系统签名
    • 允许访问灰名单 API,部分黑名单 API 被限制。
    • 使用系统密钥签名(非 Platform Key)。
  3. 普通签名
    • 仅能访问白名单和少量灰名单 API。
    • 大部分黑名单和深灰名单 API 被限制。

隐藏 API 的生成与源码分析

@hide 注解的作用

在 Android 源码中,方法或字段可以通过 @hide 注解标记为隐藏。标记后,这些 API 在 SDK 编译时会被剔除,开发者无法直接通过 SDK 调用。

如:

/**
 * @hide
 */
public static String get(String key) {
    return native_get(key);
}

Hidden API 配置文件

隐藏 API 的分类信息在ROM编译过程中由配置文件控制,这些文件位于源码的以下路径:

  • hiddenapi-light-greylist.txt:轻灰名单。
  • hiddenapi-dark-greylist.txt:深灰名单。
  • hiddenapi-blacklist.txt:黑名单。

在这些文件中,每一行都记录了一个方法或字段的完整描述,如:

Landroid/os/SystemProperties;->get(Ljava/lang/String;)Ljava/lang/String;,light-greylist

Hidden API 的运行时行为

Android 系统在运行时根据上述分类文件判断应用对隐藏 API 的访问权限。如果尝试访问超出权限范围的 API,会触发以下行为:

  • 记录警告日志: Accessing hidden method Landroid/os/SystemProperties;->get(Ljava/lang/String;)Ljava/lang/String; (greylist, linking, allowed)
  • 甚至是抛出异常: java.lang.NoSuchMethodError...

如何应对

作为开发者,我们应当积极响应官方的要求,同时也是让我们的App更加健壮,尽量避免使用隐藏API。

检测隐藏 API 的调用

  1. Hidden API 日志

    • logcat 中查看隐藏 API 的访问记录。
    • 示例: Accessing hidden method Landroid/os/SystemProperties;->get(Ljava/lang/String;) (blacklist, blocked)
  2. Hidden API Flags 文件

    • 分析 hiddenapi-flags.csv 文件,确认某方法的分类。
    • 示例: Landroid/app/ActivityManager;->getService()Landroid/app/IActivityManager;,blacklist
  3. 还可以利用 StrictMode API 来测试App是否使用了非 SDK 接口。使用 detectNonSdkApiUsage 方法来启用此 API。启用 StrictMode API 后,可以使用 penaltyListener 来接收每次使用隐藏接口时触发的回调。

如何调用隐藏 API

如果实在有必要,也可以有一些黑科技可以调用到Hide的API

临时方案

开发过程中可以用下面命令解除限制:

adb shell settings put global hidden_api_policy_pre_p_apps  1
adb shell settings put global hidden_api_policy_p_apps 1

几个值含义如下:

  • 0: Disable all detection of non-SDK interfaces. Using this setting disables all log messages for non-SDK interface usage and prevents you from testing your app using the StrictMode API. This setting is not recommended.
  • 1: Enable access to all non-SDK interfaces, but print log messages with warnings for any non-SDK interface usage. Using this setting also allows you to test your app using the StrictMode API.
  • 2: Disallow usage of non-SDK interfaces that belong to either the blacklist or the greylist and are restricted for your target API level.
  • 3: Disallow usage of non-SDK interfaces that belong to the blacklist, but allow usage of interfaces that belong to the greylist and are restricted for your target API level.

RestrictionBypass

原理介绍:ANDROID API RESTRICTION BYPASS FOR ALL ANDROID VERSIONS

简单说就是让系统判断反射调用是来自系统而不是APP。因为安卓源码是通过回溯调用栈,通过调用者的Class来判断是否是系统代码的调用(所有系统的代码都通过BootClassLoader加载,判断ClassLoader即可)。RestrictionBypass库的做法是通过在jni层新建个线程,在这个线程里去反射,去除掉了java调用的信息,从而让安卓系统以为这个是系统调用。

FreeReflection

原理:借助系统的类去反射

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

如:

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

按照这个思路,我们反射setHiddenApiExemptions方法,这个方法在dalvik/system/VMRuntime.java中,可以解除访问限制。

一个优化:apk正常的类加载的loader是PathClassLpader,这在高版本安卓系统上可能会失败。因为系统的类的loader都是BootClassLoader,所以将反射的代码的loader改成BootClassLoader,那么成功率会大幅提高。

  1. 将源代码打包成dex文件
  2. 将dex内容读出来并通过Base64转码,hardcode写到代码里
  3. 将上面的代码decode出来,写入到文件中
  4. 通过DexFile加载上面的文件
  5. 通过loadClass加载class,并调用对应的方法

BypassHiddenApiRestriction

其原理简单说就是调用mirror::Class::FindClassMethod函数获取到方法对应的ArtMethod指针,这个指针就是此方法的jmethodId。调用CallObjectMethod并传入这个jmethodId便可实现非SDK接口的调用。其文档中有详细的探究和解释,感兴趣可以仔细看看。

系统厂商

最后甚至可以寻求厂商给App加白名单……

总结

隐藏 API 是 Android 系统为维护稳定性、安全性和兼容性而设计的重要机制。虽然开发者可以通过技术手段调用隐藏 API,但在实际开发中,避免依赖隐藏 API 是最安全的选择。对于需要访问隐藏功能的场景,应优先考虑公开 API 或与设备厂商合作获取支持。

在未来,随着 Android 系统的不断升级,Google 对隐藏 API 的限制会更加严格,这提醒开发者始终遵循官方规范,以确保应用的长期兼容性和稳定性。

最近的官方对其命名和行为有一些调整,但其本质基本不变。见官方最新文档 developer.android.com/guide/app-c…