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