“系统应用”与CVE-2020-0391

前言

  • 搞Android应用安全,总会遇到一个概念叫“系统应用”。相比于这个概念,实际上更容易理解的是priv_app、platform_app以及system_app,之前的文章也有介绍这个问题。不过今天这篇文章就是关于系统应用的。

一、系统应用和特权应用到底是什么

  • 其实系统应用这个名称,是从英文System App翻译过来的,注意系统应用(System App)这个概念和SELinux标签system_app并没有什么关系。在源码中系统应用指的是带有FLAG_SYSTEM标记的应用:
    // frameworks/base/core/java/android/content/pm/ApplicationInfo.java
    public boolean isSystemApp() {
    return (flags & ApplicationInfo.FLAG_SYSTEM) != 0;
    }
  • 具体哪些应用会带有FLAG_SYSTEM标记,继续看源码:
    // frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
    mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,
        ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,
        ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.log", LOG_UID,
        ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.nfc", NFC_UID,
        ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH_UID,
        ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.shell", SHELL_UID,
        ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.se", SE_UID,
        ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.networkstack", NETWORKSTACK_UID,
        ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
  • 可以看出第一部分是具备特权UID的应用,这些应用会成为系统应用。
    // frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
    // 
    // Collect vendor/product/system_ext overlay packages. (Do this before scanning
    // any apps.)
    // For security and version matching reason, only consider overlay packages if they
    // reside in the right directory.
    for (int i = mDirsToScanAsSystem.size() - 1; i >= 0; i--) {
    final ScanPartition partition = mDirsToScanAsSystem.get(i);
    if (partition.getOverlayFolder() == null) {
        continue;
    }
    scanDirTracedLI(partition.getOverlayFolder(), systemParseFlags,
            systemScanFlags | partition.scanFlag, 0,
            packageParser, executorService);
    }
    scanDirTracedLI(frameworkDir, systemParseFlags,
        systemScanFlags | SCAN_NO_DEX | SCAN_AS_PRIVILEGED, 0,
        packageParser, executorService);
    if (!mPackages.containsKey("android")) {
    throw new IllegalStateException(
            "Failed to load frameworks package; check log for warnings");
    }
    for (int i = 0, size = mDirsToScanAsSystem.size(); i < size; i++) {
    final ScanPartition partition = mDirsToScanAsSystem.get(i);
    if (partition.getPrivAppFolder() != null) {
        scanDirTracedLI(partition.getPrivAppFolder(), systemParseFlags,
                systemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag, 0,
                packageParser, executorService);
    }
    scanDirTracedLI(partition.getAppFolder(), systemParseFlags,
            systemScanFlags | partition.scanFlag, 0,
            packageParser, executorService);
    }
  • 对于安装到mDirsToScanAsSystem里面的应用会设置systemScanFlags,这个标记实际上是SCAN_AS_SYSTEM,该标记仅在PMS内部使用,后续会对应上ApplicationInfo.FLAG_SYSTEM
    final int systemScanFlags = scanFlags | SCAN_AS_SYSTEM;
  • mDirsToScanAsSystem的来源:
    // frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
    /**
    * The list of all system partitions that may contain packages in ascending order of
    * specificity (the more generic, the earlier in the list a partition appears).
    */
    @VisibleForTesting(visibility = Visibility.PRIVATE)
    public static final List<ScanPartition> SYSTEM_PARTITIONS = Collections.unmodifiableList(
        PackagePartitions.getOrderedPartitions(ScanPartition::new));
    //...
    final List<ScanPartition> scanPartitions = new ArrayList<>();
    final List<ApexManager.ActiveApexInfo> activeApexInfos = mApexManager.getActiveApexInfos();
    for (int i = 0; i < activeApexInfos.size(); i++) {
    final ScanPartition scanPartition = resolveApexToScanPartition(activeApexInfos.get(i));
    if (scanPartition != null) {
        scanPartitions.add(scanPartition);
    }
    }
    mDirsToScanAsSystem = new ArrayList<>();
    mDirsToScanAsSystem.addAll(SYSTEM_PARTITIONS);
    mDirsToScanAsSystem.addAll(scanPartitions);
  • 这个SYSTEM_PARTITIONS最后会追溯到PackagePartitions里面:
    // frameworks/base/core/java/android/content/pm/PackagePartitions.java
    /**
    * The list of all system partitions that may contain packages in ascending order of
    * specificity (the more generic, the earlier in the list a partition appears).
    */
    private static final ArrayList<SystemPartition> SYSTEM_PARTITIONS =
        new ArrayList<>(Arrays.asList(
                new SystemPartition(Environment.getRootDirectory(), PARTITION_SYSTEM,
                        true /* containsPrivApp */, false /* containsOverlay */),
                new SystemPartition(Environment.getVendorDirectory(), PARTITION_VENDOR,
                        true /* containsPrivApp */, true /* containsOverlay */),
                new SystemPartition(Environment.getOdmDirectory(), PARTITION_ODM,
                        true /* containsPrivApp */, true /* containsOverlay */),
                new SystemPartition(Environment.getOemDirectory(), PARTITION_OEM,
                        false /* containsPrivApp */, true /* containsOverlay */),
                new SystemPartition(Environment.getProductDirectory(), PARTITION_PRODUCT,
                        true /* containsPrivApp */, true /* containsOverlay */),
                new SystemPartition(Environment.getSystemExtDirectory(), PARTITION_SYSTEM_EXT,
                        true /* containsPrivApp */, true /* containsOverlay */)));
  • 所以系统应用的范围是
    /system/framework
    /system/app
    /system/priv-app
    /vendor/app
    /vendor/priv-app
    /vendor/overlay
    /odm/app
    /odm/priv-app
    /odm/overlay
    /oem/app
    /oem/overlay
    /product/app
    /product/priv-app
    /product/overlay
    /system_ext/app
    /system_ext/priv-app
    /system_ext/overlay
  • 而特权应用的范围是
    /system/framework
    /system/priv-app
    /vendor/priv-app
    /odm/priv-app
    /product/priv-app
    /system_ext/priv-app

三、保护广播(protected-broadcast)

  • 讲完了系统应用,来看保护广播的概念,保护广播标签是可以在AndroidManifest.xml中的标签。它的功能是保护特定Action的广播,限制其只能被特定条件的发送者发送,这个广播广泛被Android框架使用。

    <protected-broadcast android:name="com.example.action.EXAMPLE" />
  • 该标签并不在Android SDK里面,普通三方应用没有使用该标签的意义,下面会进行解释。

  • 谁能发送保护广播?只有7个UID和isPersistent=true的应用可以发送保护广播,详见下面的源代码:

// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
// Verify that protected broadcasts are only being sent by system code,
// and that system code is only sending protected broadcasts.

final boolean isProtectedBroadcast;
try {
    isProtectedBroadcast = AppGlobals.getPackageManager().isProtectedBroadcast(action);
} catch (RemoteException e) {
    Slog.w(TAG, "Remote exception", e);
    return ActivityManager.BROADCAST_SUCCESS;
}
final boolean isCallerSystem;
switch (UserHandle.getAppId(callingUid)) {
    case ROOT_UID:
    case SYSTEM_UID:
    case PHONE_UID:
    case BLUETOOTH_UID:
    case NFC_UID:
    case SE_UID:
    case NETWORK_STACK_UID:
        isCallerSystem = true;
        break;
    default:
        isCallerSystem = (callerApp != null) && callerApp.isPersistent();
        break;
}
// First line security check before anything else: stop non-system apps from
// sending protected broadcasts.
if (!isCallerSystem) {
    if (isProtectedBroadcast) {
        String msg = "Permission Denial: not allowed to send broadcast "
                + action + " from pid="
                + callingPid + ", uid=" + callingUid;
        Slog.w(TAG, msg);
        throw new SecurityException(msg);
    } else if () //...
    //...
  • 谁能定义保护广播?这个问题就和CVE-2020-0391漏洞有关了,先看正常的情况:
// frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

/**
 * Applies policy to the parsed package based upon the given policy flags.
 * Ensures the package is in a good state.
 * <p>
 * Implementation detail: This method must NOT have any side effect. It would
 * ideally be static, but, it requires locks to read system state.
 */
private static void applyPolicy(ParsedPackage parsedPackage, final @ParseFlags int parseFlags,
        final @ScanFlags int scanFlags, AndroidPackage platformPkg,
        boolean isUpdatedSystemApp) {
    if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
        parsedPackage.setSystem(true);
        // TODO(b/135203078): Can this be done in PackageParser? Or just inferred when the flag
        //  is set during parse.
        if (parsedPackage.isDirectBootAware()) {
            parsedPackage.setAllComponentsDirectBootAware(true);
        }
        if (compressedFileExists(parsedPackage.getCodePath())) {
            parsedPackage.setStub(true);
        }
    } else {
        parsedPackage
                // Non system apps cannot mark any broadcast as protected
                .clearProtectedBroadcasts()
                // non system apps can't be flagged as core
                .setCoreApp(false)
                // clear flags not applicable to regular apps
                .setPersistent(false)
                .setDefaultToDeviceProtectedStorage(false)
                .setDirectBootAware(false)
                // non system apps can't have permission priority
                .capPermissionPriorities();
    }
    //...
}
  • 可以看出,在(scanFlags & SCAN_AS_SYSTEM) != 0判断的else分支,调用了clearProtectedBroadcasts方法,意思就是,非系统应用不能定义保护广播,那反过来就可以说,只有系统应用才能定义保护广播。

四、CVE-2020-0391的出现

  • 讲完了系统应用和保护广播,就可以说说这个漏洞了,它出现在Android 9和Android 10中,Android 8等更早期版本都不存在这个问题,Android 11中修复了此问题。可以看出来这个漏洞是“改出来的”漏洞。
  • 漏洞源码如下所示:
    /**
    * Applies policy to the parsed package based upon the given policy flags.
    * Ensures the package is in a good state.
    * <p>
    * Implementation detail: This method must NOT have any side effect. It would
    * ideally be static, but, it requires locks to read system state.
    */
    private static void applyPolicy(PackageParser.Package pkg, final @ParseFlags int parseFlags,
        final @ScanFlags int scanFlags, PackageParser.Package platformPkg) {
    if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
        //...
    } else {
        //...
    }
    if ((scanFlags & SCAN_AS_PRIVILEGED) == 0) {
        // clear protected broadcasts
        pkg.protectedBroadcasts = null;
        //...
    }
  • 虽然代码已经重构了很多,但是大家还是可以看出来,为什么清除保护广播定义的代码跑到了(scanFlags & SCAN_AS_PRIVILEGED) == 0的条件下了?也就是说,不是特权应用都不能定义保护广播了。
  • 其实漏洞就在这里了,OEM们预期的情况是,系统应用都可以定义保护广播,但是Android 9把这个东西改掉了,变成了只有特权应用才可以定义保护广播,上面解释过了特权应用的范围比系统应用小很多,这样会造成非特权的系统应用定义这个标签无效,相应的广播也不会被保护。总结来看以下目录的应用会受到影响:
    /vendor/overlay
    /product/overlay
    /product_services/overlay
    /odm/overlay
    /oem/overlay
    /system/app
    /vendor/app
    /odm/app
    /oem/app
    /product/app
    /product_services/app
  • 修复当然就是把代码再移回去,只要是SCAN_AS_SYSTEM的都可以定义保护广播。

五、CVE-2020-11164——在CVE-2020-0391实现system命令执行

  • 上面讲到,在Android 9和10中,非特权的系统应用,被系统禁止了定义保护广播的能力,那么会造成什么影响呢?可以来看CVE-2020-11164这个漏洞,这个漏洞存在于大部分的高通机型上。
  • 漏洞出现在一个名为Perfdump的系统应用上,路径是/system/app/Perfdump/Perfdump.apk,显然这不是一个特权应用,所以它定义的保护广播会失效,那么它定义了哪些没被保护的保护广播呢?其中之一是:
    <protected-broadcast android:name="android.perfdump.action.EXT_EXEC_SHELL"/>
  • 一看这名字就不好了,不会是一个执行代码的功能吧,猜对了,其实真的是这样,而且这个应用还是system uid,也就是说,普通应用可以发一个广播,然后以system的身份执行命令。
    Intent intent = new Intent("android.perfdump.action.EXT_EXEC_SHELL");
    intent.setClassName("com.qualcomm.qti.perfdump", "com.qualcomm.qti.perfdump.StaticReceiver");
    intent.putExtra("callerPackageName", "com.test");
    intent.putExtra("shellCommand", <command_to_execute>);
    sendBroadcast(intent);
  • 这里再总结一下,这个Perfdump.apk首先他是一个系统应用,但是他并没有被放在任何一个分区的priv-app目录下,所以在PMS进行包扫描的时候,是不会带上SCAN_AS_PRIVILEGED标签的,在有漏洞的系统上,这个应用所定义的受保护广播会被系统去除,这样一来任何应用都可以发送该广播实现命令执行。

六、总结

  • 从这个案例来看Android中对于系统应用、特权应用和system uid的应用的划分还是非常混乱的,我们有以下事实:
    • 系统应用是8个特定UID以及放在了系统只读路径下的应用
    • 特权应用是放在各个分区下priv-app目录中的应用,和UID、签名无关
    • 平台签名应用指的是应用的签名和android包一样的应用,和是否是系统应用、特权应用无关
    • system uid的应用指的是共享了android.uid.system的平台签名应用,并且一定是系统应用(因为是那8个UID之一),和是否是特权应用无关
  • 这样不良好的设计会给OEM开发者,以及Google自己的开发带来很多困惑,由此也会出现一些逻辑漏洞,这方面的问题值得我们继续关注。