Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ce6174fe authored by Victor Hsieh's avatar Victor Hsieh
Browse files

Integrity test to recover allowlisted system app tampering

This change introduces two scenarios:

1. A system APK is updated to /data. At some point, the APK itself is
   tampered but V4 signature is not touched (thus invalid now).

2. A system APK is updated to /data. At some point, the APK itself is
   tampered with by an attacker and re-signed with a different key. The
   attacker also updates package manager's internal record for
   consistency.

The test requires root to run. The test involves injecting a testing app
as a system app. In the above scenarios, the test expects the victim
system app in /data is removed.

Bug: 277347456
Test: enable flag extend_vb_chain_to_updated_apk,
      `atest TamperedUpdatedSystemPackageTest` both passed
Test: disable flag extend_vb_chain_to_updated_apk,
      `atest TamperedUpdatedSystemPackageTest` both failed
Change-Id: I16b0ed853b9e6b706fddb6d50da2e8f082ee167a
parent 016e056c
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -34,8 +34,11 @@ java_test_host {
    ],
    static_libs: [
        "ApexInstallHelper",
        "android.security.flags-aconfig-java-host",
        "cts-host-utils",
        "flag-junit-host",
        "frameworks-base-hostutils",
        "kotlin-test",
        "PackageManagerServiceHostTestsIntentVerifyUtils",
        "block_device_writer_jar",
    ],
@@ -59,6 +62,7 @@ java_test_host {
        ":PackageManagerTestAppUsesStaticLibrary",
        ":PackageManagerTestAppVersion1",
        ":PackageManagerTestAppVersion2",
        ":PackageManagerTestAppVersion2AltKey",
        ":PackageManagerTestAppVersion3",
        ":PackageManagerTestAppVersion3Invalid",
        ":PackageManagerTestAppVersion4",
+178 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.pm.test

import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.host.HostFlagsValueProvider
import com.android.internal.util.test.SystemPreparer
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.io.RandomAccessFile
import kotlin.test.assertNotNull
import org.junit.After
import org.junit.Before
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith

@RunWith(DeviceJUnit4ClassRunner::class)
@RequiresFlagsEnabled(android.security.Flags.FLAG_EXTEND_VB_CHAIN_TO_UPDATED_APK)
class TamperedUpdatedSystemPackageTest : BaseHostJUnit4Test() {

    companion object {
        private const val TEST_PKG_NAME = "com.android.server.pm.test.test_app"
        private const val VERSION_ONE = "PackageManagerTestAppVersion1.apk"
        private const val VERSION_TWO_ALT_KEY = "PackageManagerTestAppVersion2AltKey.apk"
        private const val VERSION_TWO_ALT_KEY_IDSIG =
                "PackageManagerTestAppVersion2AltKey.apk.idsig"
        private const val STRICT_SIGNATURE_CONFIG_PATH =
                "/system/etc/sysconfig/preinstalled-packages-strict-signature.xml"
        private const val TIMESTAMP_REFERENCE_FILE_PATH = "/data/local/tmp/timestamp.ref"

        @get:ClassRule
        val deviceRebootRule = SystemPreparer.TestRuleDelegate(true)
    }

    private val tempFolder = TemporaryFolder()
    private val preparer: SystemPreparer = SystemPreparer(
        tempFolder,
            SystemPreparer.RebootStrategy.FULL,
        deviceRebootRule
    ) { this.device }
    private val productPath =
            HostUtils.makePathForApk("PackageManagerTestApp.apk", Partition.PRODUCT)
    private lateinit var originalConfigFile: File

    @Rule
    @JvmField
    val checkFlagsRule = HostFlagsValueProvider.createCheckFlagsRule({ getDevice() })

    @Rule
    @JvmField
    val rules = RuleChain.outerRule(tempFolder).around(preparer)!!

    @Before
    @After
    fun removeApk() {
        device.uninstallPackage(TEST_PKG_NAME)
    }

    @Before
    fun backupAndModifySystemFiles() {
        // Backup
        device.pullFile(STRICT_SIGNATURE_CONFIG_PATH).also {
            assertNotNull(it)
            originalConfigFile = it
        }

        // Modify to allowlist the target package on device for testing the feature
        val xml = tempFolder.newFile().apply {
            val newConfigText = originalConfigFile
                    .readText()
                    .replace(
                        "</config>",
                            "<require-strict-signature package=\"${TEST_PKG_NAME}\"/></config>"
                    )
            writeText(newConfigText)
        }
        device.remountSystemWritable()
        device.pushFile(xml, STRICT_SIGNATURE_CONFIG_PATH)
    }

    @After
    fun restoreSystemFiles() {
        device.remountSystemWritable()
        device.pushFile(originalConfigFile, STRICT_SIGNATURE_CONFIG_PATH)
        // Files pushed via a SystemPreparer are deleted automatically.
    }

    @Test
    fun detectApkAndXmlTamperingAtBoot() {
        // Set up the scenario where both APK and packages.xml are tampered by the attacker.
        // This is done by booting with the "bad" APK in a system partition, re-installing it to
        // /data. Then, replace the APK in the system partition with a "good" one.
        preparer.pushResourceFile(VERSION_TWO_ALT_KEY, productPath.toString())
                .reboot()

        // Install the "bad" APK to /data. This will also update package manager's XML records.
        val versionTwoFile = HostUtils.copyResourceToHostFile(
            VERSION_TWO_ALT_KEY,
                tempFolder.newFile()
        )
        assertThat(device.installPackage(versionTwoFile, true)).isNull()
        assertThat(device.executeShellCommand("pm path ${TEST_PKG_NAME}"))
                .doesNotContain(productPath.toString())

        // "Restore" the system partition is to a good state with correct APK.
        preparer.deleteFile(productPath.toString())
                .pushResourceFile(VERSION_ONE, productPath.toString())

        // Verify that upon the next boot, the system detect the problem and remove the problematic
        // APK in the /data.
        preparer.reboot()
        assertThat(device.executeShellCommand("pm path ${TEST_PKG_NAME}"))
                .contains(productPath.toString())
    }

    @Test
    fun detectApkTamperingAtBoot() {
        // Set up the scenario where APK is tampered but not the v4 signature. First, inject a
        // good APK as a system app.
        preparer.pushResourceFile(VERSION_TWO_ALT_KEY, productPath.toString())
                .reboot()

        // Re-install the target APK to /data, with the corresponding .idsig from build time.
        val versionTwoFile = HostUtils.copyResourceToHostFile(
            VERSION_TWO_ALT_KEY,
                tempFolder.newFile()
        )
        assertThat(device.installPackage(versionTwoFile, true)).isNull()
        val baseApkPath = device.executeShellCommand("pm path ${TEST_PKG_NAME}")
                .lineSequence()
                .first()
                .replace("package:", "")
        assertThat(baseApkPath).doesNotContain(productPath.toString())
        preparer.pushResourceFile(VERSION_TWO_ALT_KEY_IDSIG, baseApkPath.toString() + ".idsig")

        // Replace the APK in /data with a tampered version. Restore fs-verity and attributes.
        RandomAccessFile(versionTwoFile, "rw").use {
            // Skip the zip local file header to keep it valid. Tamper with the file name field and
            // beyond, just so that it won't simply fail.
            it.seek(30)
            it.writeBytes("tamper")
        }
        device.executeShellCommand("touch ${TIMESTAMP_REFERENCE_FILE_PATH} -r $baseApkPath")
        preparer.pushFile(versionTwoFile, baseApkPath)
        device.executeShellCommand(
            "cd ${baseApkPath.replace("base.apk", "")}" +
                "&& chown system:system base.apk " +
                "&& /data/local/tmp/fsverity_multilib enable base.apk" +
                "&& touch base.apk -r ${TIMESTAMP_REFERENCE_FILE_PATH}"
        )

        // Verify that upon the next boot, the system detect the problem and remove the problematic
        // APK in the /data.
        preparer.reboot()
        assertThat(device.executeShellCommand("pm path ${TEST_PKG_NAME}"))
                .contains(productPath.toString())
    }
}
+10 −0
Original line number Diff line number Diff line
@@ -66,3 +66,13 @@ android_test_helper_app {
        "src/**/*.kt",
    ],
}

android_test_helper_app {
    name: "PackageManagerTestAppVersion2AltKey",
    manifest: "AndroidManifestVersion2.xml",
    srcs: [
        "src/**/*.kt",
    ],
    certificate: ":FrameworksServicesTests_keyset_A_cert",
    v4_signature: true,
}