1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.appsecurity.cts 18 19 import android.appsecurity.cts.PackageSetInstallerConstants.CHANGE_ID 20 import android.appsecurity.cts.PackageSetInstallerConstants.PERMISSION_HARD_RESTRICTED 21 import android.appsecurity.cts.PackageSetInstallerConstants.PERMISSION_IMMUTABLY_SOFT_RESTRICTED 22 import android.appsecurity.cts.PackageSetInstallerConstants.PERMISSION_KEY 23 import android.appsecurity.cts.PackageSetInstallerConstants.PERMISSION_NOT_RESTRICTED 24 import android.appsecurity.cts.PackageSetInstallerConstants.SHOULD_SUCCEED_KEY 25 import android.appsecurity.cts.PackageSetInstallerConstants.SHOULD_THROW_EXCEPTION_KEY 26 import android.appsecurity.cts.PackageSetInstallerConstants.TARGET_APK 27 import android.appsecurity.cts.PackageSetInstallerConstants.TARGET_PKG 28 import android.appsecurity.cts.PackageSetInstallerConstants.WHITELIST_APK 29 import android.appsecurity.cts.PackageSetInstallerConstants.WHITELIST_PKG 30 import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters 31 import android.cts.host.utils.DeviceJUnit4Parameterized 32 import com.google.common.truth.Truth.assertThat 33 import com.google.common.truth.Truth.assertWithMessage 34 import org.junit.After 35 import org.junit.Before 36 import org.junit.Test 37 import org.junit.runner.RunWith 38 import org.junit.runners.Parameterized 39 import java.lang.AssertionError 40 41 /** 42 * This test verifies protection for an exploit where any app could set the installer package 43 * name for another app if the installer was uninstalled or never set. 44 * 45 * It mimics both the set installer logic and checks for a permission bypass caused by this exploit, 46 * where an app could take installer for itself and whitelist itself to receive protected 47 * permissions. 48 */ 49 @RunWith(DeviceJUnit4Parameterized::class) 50 @Parameterized.UseParametersRunnerFactory( 51 DeviceJUnit4ClassRunnerWithParameters.RunnerFactory::class) 52 class PackageSetInstallerTest : BaseAppSecurityTest() { 53 54 companion object { 55 56 @JvmStatic 57 @Parameterized.Parameters(name = "{1}") parametersnull58 fun parameters() = arrayOf( 59 arrayOf(false, "throwException"), 60 arrayOf(true, "failSilently") 61 ) 62 } 63 64 @JvmField 65 @Parameterized.Parameter(0) 66 var failSilently = false 67 68 @Parameterized.Parameter(1) 69 lateinit var testName: String 70 71 @Before 72 @After 73 fun uninstallTestPackages() { 74 device.uninstallPackage(TARGET_PKG) 75 device.uninstallPackage(WHITELIST_PKG) 76 } 77 78 @After resetChangesnull79 fun resetChanges() { 80 device.executeShellCommand("am compat reset $CHANGE_ID $TARGET_PKG") 81 device.executeShellCommand("am compat reset $CHANGE_ID $WHITELIST_PKG") 82 } 83 84 @Test notRestrictednull85 fun notRestricted() { 86 runTest(removeWhitelistShouldSucceed = false, 87 permission = PERMISSION_NOT_RESTRICTED, 88 finalState = GrantState.TRUE) 89 } 90 91 @Test hardRestrictednull92 fun hardRestricted() { 93 runTest(removeWhitelistShouldSucceed = true, 94 permission = PERMISSION_HARD_RESTRICTED, 95 finalState = GrantState.FALSE) 96 } 97 98 @Test immutablySoftRestrictedGrantednull99 fun immutablySoftRestrictedGranted() { 100 runTest(removeWhitelistShouldSucceed = null, 101 permission = PERMISSION_IMMUTABLY_SOFT_RESTRICTED, 102 finalState = GrantState.TRUE_EXEMPT) 103 } 104 105 @Test immutablySoftRestrictedRevokednull106 fun immutablySoftRestrictedRevoked() { 107 runTest(removeWhitelistShouldSucceed = null, 108 permission = PERMISSION_IMMUTABLY_SOFT_RESTRICTED, 109 restrictPermissions = true, 110 finalState = GrantState.TRUE_NOT_EXEMPT) 111 } 112 runTestnull113 private fun runTest( 114 removeWhitelistShouldSucceed: Boolean?, 115 permission: String, 116 restrictPermissions: Boolean = false, 117 finalState: GrantState 118 ) { 119 // Verifies throwing a SecurityException or failing silently for backwards compatibility 120 val testArgs: Map<String, String?> = mapOf( 121 PERMISSION_KEY to permission 122 ) 123 124 // First, install both packages and ensure no installer is set 125 InstallMultiple(false, false) 126 .addFile(TARGET_APK) 127 .allowTest() 128 .forUser(mPrimaryUserId) 129 .apply { 130 if (restrictPermissions) { 131 restrictPermissions() 132 } 133 } 134 .run() 135 136 InstallMultiple(false, false) 137 .addFile(WHITELIST_APK) 138 .allowTest() 139 .forUser(mPrimaryUserId) 140 .run() 141 142 setChangeState() 143 144 assertPermission(false, permission) 145 assertTargetInstaller(null) 146 147 // Install the installer whitelist app and take over the installer package. This methods 148 // adopts the INSTALL_PACKAGES permission and verifies that the new behavior of checking 149 // this permission is applied. 150 Utils.runDeviceTests(device, WHITELIST_PKG, ".PermissionWhitelistTest", 151 "setTargetInstallerPackage", mPrimaryUserId, 152 testArgs.plus(SHOULD_THROW_EXCEPTION_KEY to (!failSilently).toString())) 153 assertTargetInstaller(WHITELIST_PKG) 154 155 // Verify that without whitelist restriction, the target app can be granted the permission 156 grantPermission(permission) 157 assertPermission(true, permission) 158 revokePermission(permission) 159 assertPermission(false, permission) 160 161 val whitelistArgs = testArgs 162 .plus(SHOULD_SUCCEED_KEY to removeWhitelistShouldSucceed?.toString()) 163 .filterValues { it != null } 164 165 // Now restrict the permission from the target app using the whitelist app 166 Utils.runDeviceTests(device, WHITELIST_PKG, ".PermissionWhitelistTest", 167 "removeWhitelistRestrictedPermission", mPrimaryUserId, whitelistArgs) 168 169 // Now remove the installer and verify the installer is wiped 170 device.uninstallPackage(WHITELIST_PKG) 171 assertTargetInstaller(null) 172 173 // Verify whitelist restriction retained by attempting and failing to grant permission 174 assertPermission(false, permission) 175 grantPermission(permission) 176 assertGrantState(finalState, permission) 177 revokePermission(permission) 178 179 // Attempt exploit to take over installer package and have target whitelist itself 180 Utils.runDeviceTests(device, TARGET_PKG, ".PermissionRequestTest", 181 "setSelfAsInstallerAndWhitelistPermission", mPrimaryUserId, 182 testArgs.plus(SHOULD_THROW_EXCEPTION_KEY to (!failSilently).toString())) 183 184 // Assert nothing changed about whitelist restriction 185 assertTargetInstaller(null) 186 grantPermission(permission) 187 assertGrantState(finalState, permission) 188 } 189 setChangeStatenull190 private fun setChangeState() { 191 val state = if (failSilently) "disable" else "enable" 192 device.executeShellCommand("am compat $state $CHANGE_ID $TARGET_PKG") 193 device.executeShellCommand("am compat $state $CHANGE_ID $WHITELIST_PKG") 194 } 195 assertTargetInstallernull196 private fun assertTargetInstaller(installer: String?) { 197 assertThat(device.executeShellCommand("pm list packages -i | grep $TARGET_PKG").trim()) 198 .isEqualTo("package:$TARGET_PKG installer=$installer") 199 } 200 assertPermissionnull201 private fun assertPermission(granted: Boolean, permission: String) { 202 assertThat(getPermissionString(permission)).contains("granted=$granted") 203 } 204 grantPermissionnull205 private fun grantPermission(permission: String) { 206 device.executeShellCommand("pm grant $TARGET_PKG $permission") 207 } 208 revokePermissionnull209 private fun revokePermission(permission: String) { 210 device.executeShellCommand("pm revoke $TARGET_PKG $permission") 211 } 212 assertGrantStatenull213 private fun assertGrantState(state: GrantState, permission: String) { 214 val output = getPermissionString(permission) 215 216 when (state) { 217 GrantState.TRUE -> { 218 assertThat(output).contains("granted=true") 219 assertThat(output).doesNotContain("RESTRICTION") 220 assertThat(output).doesNotContain("EXEMPT") 221 } 222 GrantState.TRUE_EXEMPT -> { 223 assertThat(output).contains("granted=true") 224 assertThat(output).contains("RESTRICTION_INSTALLER_EXEMPT") 225 } 226 GrantState.TRUE_NOT_EXEMPT -> { 227 assertThat(output).contains("granted=true") 228 assertThat(output).doesNotContain("EXEMPT") 229 } 230 GrantState.FALSE -> { 231 assertThat(output).contains("granted=false") 232 } 233 } 234 } 235 <lambda>null236 private fun getPermissionString(permission: String) = retry { 237 device.executeShellCommand("dumpsys package $TARGET_PKG") 238 .lineSequence() 239 .dropWhile { !it.startsWith("Packages:") } // Wait for package header 240 .drop(1) // Drop the package header itself 241 .takeWhile { it.isEmpty() || it.first().isWhitespace() } // Until next header 242 .dropWhile { !it.trim().startsWith("User $mPrimaryUserId:") } // Find user 243 .drop(1) // Drop the user header itself 244 .takeWhile { !it.trim().startsWith("User") } // Until next user 245 .filter { it.contains("$permission: granted=") } 246 .single() 247 } 248 retrynull249 private fun <T> retry(block: () -> T?): T { 250 repeat(10) { 251 try { 252 block()?.let { return it } 253 } catch (e : Exception) { 254 // do nothing 255 } 256 Thread.sleep(1000) 257 } 258 259 throw AssertionError("Never succeeded") 260 } 261 262 enum class GrantState { 263 // Granted in full, unrestricted 264 TRUE, 265 266 // Granted in full by exemption 267 TRUE_EXEMPT, 268 269 // Granted in part 270 TRUE_NOT_EXEMPT, 271 272 // Not granted at all 273 FALSE 274 } 275 } 276