前言
- 搞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自己的开发带来很多困惑,由此也会出现一些逻辑漏洞,这方面的问题值得我们继续关注。