Rewrite app-with-activity to match guidelines Bug: 441727732 Test: manually verified from command line and Android Studio Change-Id: I1eeb4852da5d6f19b8e24c319bebd24392f79c51
diff --git a/templates/app-with-activity/submission/appTest/src/main/AndroidManifest.xml b/templates/app-with-activity/submission/appTest/src/main/AndroidManifest.xml index 3f2ec78..bc21fae 100644 --- a/templates/app-with-activity/submission/appTest/src/main/AndroidManifest.xml +++ b/templates/app-with-activity/submission/appTest/src/main/AndroidManifest.xml
@@ -25,11 +25,6 @@ android:supportsRtl="true"> <activity android:name=".PocActivity" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> - </activity> + android:exported="true" /> </application> </manifest>
diff --git a/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/DeviceTest.java b/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/DeviceTest.java index 059b0ea..37dd0be 100644 --- a/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/DeviceTest.java +++ b/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/DeviceTest.java
@@ -17,11 +17,11 @@ package com.android.security; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import android.app.Instrumentation; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -29,7 +29,9 @@ import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,63 +40,114 @@ import java.util.concurrent.atomic.AtomicReference; /** - * An example device test that starts an activity and uses broadcasts to wait for the artifact - * proving vulnerability + * ## Test Architecture: Device-Side Logic + * + * This class runs as an instrumented test on the Android device. It contains a single `@Test` + * method that can be run in different "phases" based on arguments from the host. */ @RunWith(AndroidJUnit4.class) public class DeviceTest { private static final String TAG = DeviceTest.class.getSimpleName(); - Context mContext; + private static final int TIMEOUT_MS = 5000; - /** Test broadcast action */ - public static final String ACTION_BROADCAST = "action_security_test_broadcast"; + /** Test broadcast action sent from the PocActivity with the result. */ + public static final String ACTION_BROADCAST = "com.android.security.action.RESULT_BROADCAST"; - /** Broadcast intent extra name for artifacts */ + /** Broadcast intent extra name for artifacts. */ public static final String INTENT_ARTIFACT = "artifact"; - /** Device test */ - @Test - public void testDeviceSideMethod() throws Exception { - mContext = getApplicationContext(); + private Context mContext; + private Instrumentation mInstrumentation; - AtomicReference<String> actual = new AtomicReference<>(); + @Before + public void setUp() { + mContext = getApplicationContext(); + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + } + + /** + * This is the single entry point for the device-side test. It follows the "Prove the Boundary" + * pattern to create a reliable test. + */ + @Test + public void testVulnerabilityFailsOnVulnerableDevice() throws Exception { + // STEP 1: Assert the Boundary is Intact + // Launch the PoC activity without the trigger file present. + // The sensitive action should be blocked by the security boundary. + Log.d(TAG, "STEP 1: Asserting that the security boundary is intact..."); + String initialArtifact = launchActivityAndGetResult(); + assertEquals( + "The security boundary should have blocked the action.", + "ACCESS_DENIED", + initialArtifact); + Log.d(TAG, "Success: The security boundary correctly blocked the action."); + + // STEP 2: Trigger the Vulnerability + // Create the trigger file that the PoC activity will detect. + Log.d(TAG, "STEP 2: Triggering the vulnerability..."); + mInstrumentation + .getUiAutomation() + .executeShellCommand("touch " + PocActivity.TRIGGER_FILE_PATH) + .close(); + Log.d(TAG, "Success: Vulnerability trigger created at " + PocActivity.TRIGGER_FILE_PATH); + + // STEP 3: Assert the Boundary is Breached + // Relaunch the PoC activity. This time, it should detect the trigger file and the + // sensitive action should succeed. + Log.d(TAG, "STEP 3: Asserting that the security boundary is breached..."); + String finalArtifact = launchActivityAndGetResult(); + + // The final, critical assertion. If the artifact contains the secret data, + // we fail the test and report the artifact as proof. This test SHOULD fail on a + // vulnerable device. + if ("SECRET_DATA_ACCESSED".equals(finalArtifact)) { + fail("Vulnerability proven: Leaked sensitive data: '" + finalArtifact + "'"); + } + + // If the exploit failed, the artifact will be "ACCESS_DENIED". In this case, the + // test will pass, correctly indicating that the device is not vulnerable. + assertEquals( + "The exploit failed; the security boundary was not bypassed.", + "ACCESS_DENIED", + finalArtifact); + Log.d( + TAG, + "The device appears to be secure; the exploit did not bypass the security" + + " boundary."); + } + + /** + * Launches the PocActivity, waits for it to send a result broadcast, and returns the artifact. + */ + private String launchActivityAndGetResult() throws InterruptedException { + final AtomicReference<String> artifactRef = new AtomicReference<>(); final Semaphore broadcastReceived = new Semaphore(0); + BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - try { - if (!intent.getAction().equals(ACTION_BROADCAST)) { - Log.i( - TAG, - String.format( - "got a broadcast that we didn't expect: %s", - intent.getAction())); - } - actual.set(intent.getStringExtra(INTENT_ARTIFACT)); + if (ACTION_BROADCAST.equals(intent.getAction())) { + artifactRef.set(intent.getStringExtra(INTENT_ARTIFACT)); broadcastReceived.release(); - } catch (Exception e) { - Log.e(TAG, "got an exception when handling broadcast", e); } } }; - IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_BROADCAST); - mContext.registerReceiver(broadcastReceiver, filter, Context.RECEIVER_EXPORTED); + mContext.registerReceiver( + broadcastReceiver, new IntentFilter(ACTION_BROADCAST), Context.RECEIVER_EXPORTED); - // start the target app - try { - Log.d(TAG, "starting local activity"); - Intent newActivityIntent = new Intent(mContext, PocActivity.class); - newActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // this could be startActivityForResult, but is generic for illustrative purposes - mContext.startActivity(newActivityIntent); - } finally { - getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); + // Start the target app. + Intent newActivityIntent = new Intent(mContext, PocActivity.class); + newActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(newActivityIntent); + + // Wait for the result from the activity. + if (!broadcastReceived.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + fail("Timed out waiting for result from PocActivity."); } - assertTrue( - "Timed out when getting result from other activity", - broadcastReceived.tryAcquire(/* TIMEOUT_MS */ 5000, TimeUnit.MILLISECONDS)); - assertEquals("The target artifact should have been 'secure'", "secure", actual.get()); + + mContext.unregisterReceiver(broadcastReceiver); + + return artifactRef.get(); } }
diff --git a/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/PocActivity.java b/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/PocActivity.java index 113bd2b..e69a1c1 100644 --- a/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/PocActivity.java +++ b/templates/app-with-activity/submission/appTest/src/main/java/com/android/security/PocActivity.java
@@ -21,22 +21,65 @@ import android.os.Bundle; import android.util.Log; +import java.io.File; + +/** + * ## Test Architecture: The Proof-of-Concept Application + * + * This Activity serves as the proof-of-concept (PoC) application. It is **not** the vulnerable + * component itself. Instead, it is a tool designed to trigger and demonstrate a vulnerability in the + * underlying Android platform or SoC components. + * + * ### The Mock Vulnerability Trigger + * The "vulnerability" this app triggers is simple: if a specific file exists at {@link + * #TRIGGER_FILE_PATH}, a sensitive action that should normally be blocked will succeed. + * + * This simulates a real-world scenario where an attacker-controlled app could perform an action + * (like creating a file or sending an intent) that puts a platform component into an insecure + * state, allowing the app to bypass a security boundary. + * + * This Activity is UI-less and designed for automated testing. It performs its check and reports its + * result via a broadcast as soon as it is launched, then finishes. + */ public class PocActivity extends Activity { private static final String TAG = PocActivity.class.getSimpleName(); + // A file path that the host can write to and the app can read. + static final String TRIGGER_FILE_PATH = "/data/local/tmp/exploit_trigger.txt"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - Log.d(TAG, "poc activity started"); - // Collect the artifact representing vulnerability here. - // Change this to whatever type best fits the vulnerable artifact; consider using a bundle - // if there are multiple artifacts necessary to prove the security vulnerability. - String artifact = "vulnerable"; + boolean isExploitTriggered = new File(TRIGGER_FILE_PATH).exists(); + Log.d(TAG, "PocActivity started. Exploit triggered: " + isExploitTriggered); - Intent vulnerabilityDescriptionIntent = new Intent(DeviceTest.ACTION_BROADCAST); - vulnerabilityDescriptionIntent.putExtra(DeviceTest.INTENT_ARTIFACT, artifact); - this.sendBroadcast(vulnerabilityDescriptionIntent); + // This is the sensitive action that should be protected by a security boundary. + // It is triggered automatically when the Activity is created. + attemptSensitiveAction(isExploitTriggered); + } + + /** + * Attempts to perform the sensitive action. The outcome depends on whether the mock exploit has + * been triggered. + */ + private void attemptSensitiveAction(boolean isExploitTriggered) { + String artifact; + if (isExploitTriggered) { + // STEP 3: The exploit has been triggered. The sensitive action now succeeds. + // We collect an artifact that proves the vulnerability. + artifact = "SECRET_DATA_ACCESSED"; + Log.d(TAG, "Sensitive action succeeded. Captured artifact: " + artifact); + } else { + // STEP 2: The exploit has not been triggered. The sensitive action is blocked. + artifact = "ACCESS_DENIED"; + Log.d(TAG, "Sensitive action blocked by security boundary."); + } + + // Send the result back to the device-side test. + Intent resultIntent = new Intent(DeviceTest.ACTION_BROADCAST); + resultIntent.putExtra(DeviceTest.INTENT_ARTIFACT, artifact); + sendBroadcast(resultIntent); + finish(); // Close the activity after the action is performed. } }
diff --git a/templates/app-with-activity/submission/hostTest/src/main/java/com/android/security/autorepro_placeholder/HostsideTest.java b/templates/app-with-activity/submission/hostTest/src/main/java/com/android/security/autorepro_placeholder/HostsideTest.java index 2cc1406..496ef32 100644 --- a/templates/app-with-activity/submission/hostTest/src/main/java/com/android/security/autorepro_placeholder/HostsideTest.java +++ b/templates/app-with-activity/submission/hostTest/src/main/java/com/android/security/autorepro_placeholder/HostsideTest.java
@@ -21,10 +21,21 @@ import com.android.sts.common.tradefed.testtype.NonRootSecurityTestCase; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; import org.junit.Test; import org.junit.runner.RunWith; +/** + * ## Test Architecture: Host-Side Launcher + * + * This host-side test is minimal. Its only purpose is to: + * 1. Install the PoC app (`appTest`). + * 2. Run a single, designated test method in the device-side test class. + * 3. Clean up the app after the test is complete. + * + * All of the complex PoC logic is contained in the device-side test. + */ @RunWith(DeviceJUnit4ClassRunner.class) public class HostsideTest extends NonRootSecurityTestCase { @@ -34,15 +45,15 @@ static final String TEST_PKG = "com.android.security.appTest_AutoReproPlaceholder"; // The class name will be different from the application ID but will match the source code. static final String TEST_CLASS = "com.android.security.DeviceTest"; + // The single device-side test method that contains the entire PoC. + static final String TEST_METHOD = "testVulnerabilityFailsOnVulnerableDevice"; - /** An app test, which uses this host Java test to launch an Android instrumented test */ + /** + * Installs the app and runs the single, self-contained device-side test. + */ @Test - public void testWithApp() throws Exception { - ITestDevice device = getDevice(); - assertTrue("could not disable root", device.disableAdbRoot()); - uninstallPackage(device, TEST_PKG); - + public void testPoc() throws Exception { installPackage(TEST_APP); - runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod"); + runDeviceTests(TEST_PKG, TEST_CLASS, TEST_METHOD); } }