前言
- 搞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;
// 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自己的开发带来很多困惑,由此也会出现一些逻辑漏洞,这方面的问题值得我们继续关注。
前言
- 搞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;
// 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);
- 所以系统应用(SCAN_AS_SYSTEM)的范围是
/vendor/overlay
/product/overlay
/product_services/overlay
/odm/overlay
/oem/overlay
/system/framework
/system/priv-app
/system/app
/vendor/priv-app
/vendor/app
/odm/priv-app
/odm/app
/oem/app
/product/priv-app
/product/app
/product_services/priv-app
/product_services/app
- 而特权应用(SCAN_AS_PRIVILEGED)的范围是
/system/framework
/system/priv-app
/vendor/priv-app
/odm/priv-app
/product/priv-app
/product_services/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自己的开发带来很多困惑,由此也会出现一些逻辑漏洞,这方面的问题值得我们继续关注。