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

前言

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

一、系统应用到底是什么

  • 其实系统应用这个名称,是从英文System App翻译过来的,注意系统应用(System App)这个概念和SELinux标签system_app并没有什么关系,并且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 */)));
  • 上面是Android 11的代码,Android 10这里写的更容易懂一些,附上:
// Collect vendor/product/product_services 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.
scanDirTracedLI(new File(VENDOR_OVERLAY_DIR),
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_VENDOR,
        0);
scanDirTracedLI(new File(PRODUCT_OVERLAY_DIR),
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_PRODUCT,
        0);
scanDirTracedLI(new File(PRODUCT_SERVICES_OVERLAY_DIR),
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_PRODUCT_SERVICES,
        0);
scanDirTracedLI(new File(ODM_OVERLAY_DIR),
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_ODM,
        0);
scanDirTracedLI(new File(OEM_OVERLAY_DIR),
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_OEM,
        0);
mParallelPackageParserCallback.findStaticOverlayPackages();
// Find base frameworks (resource packages without code).
scanDirTracedLI(frameworkDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_NO_DEX
        | SCAN_AS_SYSTEM
        | SCAN_AS_PRIVILEGED,
        0);
if (!mPackages.containsKey("android")) {
    throw new IllegalStateException(
            "Failed to load frameworks package; check log for warnings");
}
// Collect privileged system packages.
final File privilegedAppDir = new File(Environment.getRootDirectory(), "priv-app");
scanDirTracedLI(privilegedAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_PRIVILEGED,
        0);
// Collect ordinary system packages.
final File systemAppDir = new File(Environment.getRootDirectory(), "app");
scanDirTracedLI(systemAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM,
        0);
// Collect privileged vendor packages.
File privilegedVendorAppDir = new File(Environment.getVendorDirectory(), "priv-app");
try {
    privilegedVendorAppDir = privilegedVendorAppDir.getCanonicalFile();
} catch (IOException e) {
    // failed to look up canonical path, continue with original one
}
scanDirTracedLI(privilegedVendorAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_VENDOR
        | SCAN_AS_PRIVILEGED,
        0);
// Collect ordinary vendor packages.
File vendorAppDir = new File(Environment.getVendorDirectory(), "app");
try {
    vendorAppDir = vendorAppDir.getCanonicalFile();
} catch (IOException e) {
    // failed to look up canonical path, continue with original one
}
scanDirTracedLI(vendorAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_VENDOR,
        0);
// Collect privileged odm packages. /odm is another vendor partition
// other than /vendor.
File privilegedOdmAppDir = new File(Environment.getOdmDirectory(),
            "priv-app");
try {
    privilegedOdmAppDir = privilegedOdmAppDir.getCanonicalFile();
} catch (IOException e) {
    // failed to look up canonical path, continue with original one
}
scanDirTracedLI(privilegedOdmAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_VENDOR
        | SCAN_AS_PRIVILEGED,
        0);
// Collect ordinary odm packages. /odm is another vendor partition
// other than /vendor.
File odmAppDir = new File(Environment.getOdmDirectory(), "app");
try {
    odmAppDir = odmAppDir.getCanonicalFile();
} catch (IOException e) {
    // failed to look up canonical path, continue with original one
}
scanDirTracedLI(odmAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_VENDOR,
        0);
// Collect all OEM packages.
final File oemAppDir = new File(Environment.getOemDirectory(), "app");
scanDirTracedLI(oemAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_OEM,
        0);
// Collected privileged /product packages.
File privilegedProductAppDir = new File(Environment.getProductDirectory(), "priv-app");
try {
    privilegedProductAppDir = privilegedProductAppDir.getCanonicalFile();
} catch (IOException e) {
    // failed to look up canonical path, continue with original one
}
scanDirTracedLI(privilegedProductAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_PRODUCT
        | SCAN_AS_PRIVILEGED,
        0);
// Collect ordinary /product packages.
File productAppDir = new File(Environment.getProductDirectory(), "app");
try {
    productAppDir = productAppDir.getCanonicalFile();
} catch (IOException e) {
    // failed to look up canonical path, continue with original one
}
scanDirTracedLI(productAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_PRODUCT,
        0);
// Collected privileged /product_services packages.
File privilegedProductServicesAppDir =
        new File(Environment.getProductServicesDirectory(), "priv-app");
try {
    privilegedProductServicesAppDir =
            privilegedProductServicesAppDir.getCanonicalFile();
} catch (IOException e) {
    // failed to look up canonical path, continue with original one
}
scanDirTracedLI(privilegedProductServicesAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_PRODUCT_SERVICES
        | SCAN_AS_PRIVILEGED,
        0);
// Collect ordinary /product_services packages.
File productServicesAppDir = new File(Environment.getProductServicesDirectory(), "app");
try {
    productServicesAppDir = productServicesAppDir.getCanonicalFile();
} catch (IOException e) {
    // failed to look up canonical path, continue with original one
}
scanDirTracedLI(productServicesAppDir,
        mDefParseFlags
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanFlags
        | SCAN_AS_SYSTEM
        | SCAN_AS_PRODUCT_SERVICES,
        0);
  • 简单总结一下,系统应用其实就是在特定路径下和特定UID的应用,基本上涵盖了所有预装应用的路径。
  • 同时需要注意的是,特权应用是系统应用的子集,因为特权应用只是那些特定UID的应用,以及在各个分区priv-app目录下的应用,最典型的是/system/priv-app。特权应用的SELinux标签是priv_app,而非特权的系统应用,也不是平台签名的话,其SELinux标签依旧是untrusted_app。

三、保护广播(protected-broadcast)

  • 讲完了系统应用,来看保护广播的概念,保护广播标签是可以在AndroidManifest.xml中的标签。它的功能是保护特定Action的广播,限制其只能被特定条件的发送者发送,这个广播广泛被Android框架使用。
<protected-broadcast android:name="com.example.action.EXAMPLE" />
  • 该标签并不在Android SDK里面,普通三方应用没有使用该标签的意义,下面会进行解释。
  • 谁能发送保护广播?只有7个UID可以发送保护广播,详见下面的源代码:
// 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把这个东西改掉了,变成了只有特权应用才可以定义保护广播,上面解释过了特权应用的范围比系统应用小很多,这样会造成非特权的系统应用定义这个标签无效,相应的广播也不会被保护。

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

  • 上面讲到,在Android 9和10中,非特权的系统应用,被系统禁止了定义保护广播的能力,那么会造成什么影响呢?可以来看CVE-2020-11164这个漏洞,这个漏洞存在于大部分的高通机型上。
  • 漏洞出现在一个名为Perfdump的系统应用上,路径是/system/app/Perfdump/Perfdump.apk,显然这不是一个特权应用(不在priv-app下面),所以它定义的保护广播会失效,那么它定义了哪些没被保护的保护广播呢?其中之一是:
<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);

六、难道system uid的应用不是特权应用?

  • 其实上面有个小疑问,就是system uid的应用应该是特权应用啊,为什么会走到清除protectedBroadcasts的分支呢?为什么它的scanFlags没有SCAN_AS_PRIVILEGED标记呢?
mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,
        ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
  • 这段代码虽然在PMS的构造函数里面,但是这里面还没有对具体的使用system uid的应用添加这个ApplicationInfo.PRIVATE_FLAG_PRIVILEGED标记,反而是在applyPolicy里面有这样一段逻辑
if ((scanFlags & SCAN_AS_PRIVILEGED) != 0) {
    pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
}
  • 再仔细看看scanFlags的设置,你会发现scanFlags只和你安装的目录有关,他不管你的UID。这块具体的代码有机会继续分析

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注