前言

搞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 SYSTEM_PARTITIONS = Collections.unmodifiableList(

PackagePartitions.getOrderedPartitions(ScanPartition::new));

//...

final List scanPartitions = new ArrayList<>();

final List 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 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框架使用。

该标签并不在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.

*

* 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.

*

* 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,显然这不是一个特权应用,所以它定义的保护广播会失效,那么它定义了哪些没被保护的保护广播呢?其中之一是:

一看这名字就不好了,不会是一个执行代码的功能吧,猜对了,其实真的是这样,而且这个应用还是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", );

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