Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Don’t let attackers exploit your app via Intent...

Don’t let attackers exploit your app via Intents // mDevCamp 2025

My presentation about Intent-based exploits of Android apps and their mitigations as presented at mDevCamp 2025 on June 3rd, 2025.

Intro
Intents are the starting points for every Android application. The platform is very much built on Activities, potentially from different apps interacting with each other to complete some tasks. This open nature can be an avenue for exploitation.

You have to consider Intents what they are: inputs. And inputs must be sanitized. With this mentality, you can protect against many attacks, but some can only be avoided with the right architecture and platform support. Google finally made strides in this area with Android 15’s safer Intents. At the same time, you need to understand the attack surface to defend your apps.

I will describe and demonstrate such issues:
- Privilege escalation via Intent redirection
- Denial-of-service via malformed Intents
- Leaking data via Intent parameter injection
- App impersonation via Task hijacking (StrandHogg)

At the end of the talk, you will have an understanding of mitigating and remediating many Intent-based Android vulnerabilities.

Links
Android Security Evolution:
https://github.com/balazsgerlei/AndroidSecurityEvolution

Safeguarding user security on Android​:
https://youtu.be/RccJYep2v5I

USENIX Security '15 - Towards Discovering and Understanding Task Hijacking in Android​:
https://youtu.be/IYGwXFIYdS8

HackTricks - Android Task Hijacking​:
https://book.hacktricks.wiki/en/mobile-pentesting/android-app-pentesting/android-task-hijacking.html

Application Security Cheat Sheet:
https://0xn3va.gitbook.io/cheat-sheets/android-application/intent-vulnerabilities

Avatar for Balázs Gerlei

Balázs Gerlei

June 03, 2025
Tweet

More Decks by Balázs Gerlei

Other Decks in Programming

Transcript

  1. Intent Types @balazsgerlei, balazsgerlei.com // Starting an Activity val activityIntent

    = Intent(this, OtherActivity::class.java).apply { putExtra("key", value) } startActivity(activityIntent) // Starting a Service val fileUrl = "file:///mnt/sdcard/foo.txt" val serviceIntent = Intent(this, MyService::class.java).apply { data = fileUrl.toUri() } startService(serviceIntent) // Sending a Broadcast val broadcastIntent = Intent("com.victim.messenger.IN_APP_MESSAGE").apply { putExtra("key", value) }
  2. Intent Types @balazsgerlei, balazsgerlei.com // Starting an Activity val activityIntent

    = Intent(this, OtherActivity::class.java).apply { putExtra("key", value) } startActivity(activityIntent) // Starting a Service val fileUrl = "file:///mnt/sdcard/foo.txt" val serviceIntent = Intent(this, MyService::class.java).apply { data = fileUrl.toUri() } startService(serviceIntent) // Sending a Broadcast val broadcastIntent = Intent("com.victim.messenger.IN_APP_MESSAGE").apply { putExtra("key", value) }
  3. Intent Types @balazsgerlei, balazsgerlei.com // Starting an Activity val activityIntent

    = Intent(this, OtherActivity::class.java).apply { putExtra("key", value) } startActivity(activityIntent) // Starting a Service val fileUrl = "file:///mnt/sdcard/foo.txt" val serviceIntent = Intent(this, MyService::class.java).apply { data = fileUrl.toUri() } startService(serviceIntent) // Sending a Broadcast val broadcastIntent = Intent("com.victim.messenger.IN_APP_MESSAGE").apply { putExtra("key", value) }
  4. Intent Types @balazsgerlei, balazsgerlei.com // Explicit Intent val explicitIntent =

    Intent(this, OtherActivity::class.java) startActivity(explicitIntent)
  5. Intent Types @balazsgerlei, balazsgerlei.com // Explicit Intent val explicitIntent =

    Intent(this, OtherActivity::class.java) startActivity(explicitIntent) .apply { putExtra("key", value) }
  6. Intent Types @balazsgerlei, balazsgerlei.com // Explicit Intent val explicitIntent =

    Intent(this, OtherActivity::class.java) startActivity(explicitIntent) .apply { putExtra("key", value) } // Implicit Intent val url = "https://example.com" val implicitIntent = Intent(Intent.ACTION_VIEW).apply { data = url.toUri() } startActivity(implicitIntent)
  7. Intent to Filter Pairing <intent-filter> <action android:name="android.intent.action.SEND"/> <category android:name="android.intent.category.DEFAULT"/> <data

    android:mimeType="text/plain"/> </intent-filter> @balazsgerlei, balazsgerlei.com val intent = Intent(Intent.ACTION_SEND).apply { addCategory(Intent.CATEGORY_DEFAULT) type = "text/plain" }
  8. PendingIntent • A PendingIntent is an Intent that is set-up

    to be launched at a later point • E.g., the Intent wrapped into a Notification • Allows apps to take actions on behalf of another app • Using that app's identity and permissions @balazsgerlei, balazsgerlei.com
  9. Intent DoS • A malicious app can send a malformed

    Intent to crash another app • The easiest is without an Action (set to null) • The most effective is with a custom Serializable extra • Unknown by the recipient • Simple form of Denial of Service @balazsgerlei, balazsgerlei.com
  10. Intent Fuzzer App • An app to try send malformed

    Intents to other apps • github.com/balazsgerlei/IntentFuzzer @balazsgerlei, balazsgerlei.com
  11. Sanitizing Intents – Intent DoS Mitigation • Check for unexpected

    actions, extras, etc. when parsing • Handle Exceptions • Ignore malformed Intents • Clear malformed Bundles • Otherwise crash because of Serializable extra between Android 9 (API 28) and 11 (API 31) • Use IntentSanitizer from androidx.core IntentSanitizer.Builder() .allowAnyComponent() .allowAction("android.intent.action.MAIN") .allowCategory("android.intent.category.LAUNCHER") .allowFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .build() .sanitizeByThrowing(intent) @balazsgerlei, balazsgerlei.com
  12. Intent Redirection • An attacker fully or partially controls the

    content of an Intent that is used to launch a component in the context of a victim app • Elevation of Privilege • Most commonly an embedded Intent in the extras field @balazsgerlei, balazsgerlei.com
  13. Intent Redirection – Mitigations • Sanitize and Filter Intents •

    Carefully implement parsing logic • Don’t launch Intents that are embedded into Intents from outside • Don’t use a proxy Activity • Use (immutable) PendingIntents @balazsgerlei, balazsgerlei.com
  14. Intent Redirection via WebView • Faulty parsing logic in the

    shouldOverrideUrlLoading method of WebViewClient • Creating Intent via parsing the URL String • Could expose private components (e.g., Activities) via component and selector fields @balazsgerlei, balazsgerlei.com
  15. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { startActivity(it) } return true } .also { Intent.parseUri(url, 0)
  16. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { startActivity(it) } return true } .also { Intent.parseUri(url, 0)
  17. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { startActivity(it) } return true } .also { Intent.parseUri(url, 0)
  18. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { startActivity(it) } return true } .also { Intent.parseUri(url, 0)
  19. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { startActivity(it) } return true } .also { Intent(Intent.ACTION_VIEW, Uri.parse(url))
  20. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { Intent.parseUri(url, 0) .apply { addCategory(Intent.CATEGORY_BROWSABLE) component = null selector = null } .also { startActivity(it) } return true }
  21. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { Intent.parseUri(url, 0) .apply { addCategory(Intent.CATEGORY_BROWSABLE) component = null selector = null } .also { startActivity(it) } return true }
  22. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { Intent.parseUri(url, 0) .apply { addCategory(Intent.CATEGORY_BROWSABLE) component = null selector = null } .also { startActivity(it) } return true }
  23. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { Intent.parseUri(url, 0) .apply { addCategory(Intent.CATEGORY_BROWSABLE) component = null selector = null } .also { } return true } val activityInfo = it.resolveActivityInfo( packageManager, PackageManager.MATCH_DEFAULT_ONLY) if (activityInfo.exported) { } startActivity(it)
  24. Intent Redirection via WebView @balazsgerlei, balazsgerlei.com override fun shouldOverrideUrlLoading(view: WebView?,

    url: String?): Boolean { Intent.parseUri(url, 0) .apply { addCategory(Intent.CATEGORY_BROWSABLE) component = null selector = null } .also { } return true } val activityInfo = it.resolveActivityInfo( packageManager, PackageManager.MATCH_DEFAULT_ONLY) if (activityInfo.exported) { } startActivity(it)
  25. Intent Redirection via WebView - Mitigations • Think through the

    functionality you want to provide • Use the constructor of Intent • Explicitly set component and selector to null • Check exported status of the component before calling @balazsgerlei, balazsgerlei.com
  26. More WebView-related vulnerabilities • “Overcoming Unsecurities in WebViews” – droidcon

    London • droidcon.com/2024/12/14/overcoming-unsecurities-in-webviews/ @balazsgerlei, balazsgerlei.com
  27. Explicit Intents partially matching Intent Filters • When the target

    component is specified (explicit Intent) the Intent gets delivered, even if the Action does not match • Sharing the parsing logic between a public and a private receiver • It can lead to triggering a private action through a public receiver @balazsgerlei, balazsgerlei.com
  28. Explicit Intents partially matching Intent Filters <receiver android:name=".ExternalReceiver" android:exported="true"> <intent-filter>

    <action android:name="com.example.victim.PUBLIC_EXTERNAL_ACTION" /> </intent-filter> </receiver> <receiver android:name=".InternalReceiver" android:exported="false"> <intent-filter> <action android:name="com.example.victim.PRIVATE_INTERNAL_ACTION" /> </intent-filter> </receiver> @balazsgerlei, balazsgerlei.com
  29. Explicit Intents partially matching Intent Filters <receiver android:name=".ExternalReceiver" android:exported="true"> <intent-filter>

    <action android:name="com.example.victim.PUBLIC_EXTERNAL_ACTION" /> </intent-filter> </receiver> <receiver android:name=".InternalReceiver" android:exported="false"> <intent-filter> <action android:name="com.example.victim.PRIVATE_INTERNAL_ACTION" /> </intent-filter> </receiver> @balazsgerlei, balazsgerlei.com
  30. Explicit Intents partially matching Intent Filters class ExternalReceiver : BroadcastReceiver()

    { override fun onReceive(context: Context, intent: Intent) { CentralizedIntentHandler.handleIntent(context, intent) } } class InternalReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { CentralizedIntentHandler.handleIntent(context, intent) } } @balazsgerlei, balazsgerlei.com
  31. Explicit Intents partially matching Intent Filters class ExternalReceiver : BroadcastReceiver()

    { override fun onReceive(context: Context, intent: Intent) { CentralizedIntentHandler.handleIntent(context, intent) } } class InternalReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { CentralizedIntentHandler.handleIntent(context, intent) } } @balazsgerlei, balazsgerlei.com
  32. Explicit Intents partially matching Intent Filters object CentralizedIntentHandler { fun

    handleIntent(context: Context, intent: Intent) { when (intent.action!!) { PUBLIC_EXTERNAL_ACTION -> { Toast.makeText(context, "EXECUTING EXTERNAL ACTION...", Toast.LENGTH_LONG) .show() } PRIVATE_INTERNAL_ACTION -> { Toast.makeText(context, "EXECUTING INTERNAL ACTION...", Toast.LENGTH_LONG) .show() with(context) { Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }.also { startActivity(it) } } } } } } @balazsgerlei, balazsgerlei.com
  33. Explicit Intents partially matching Intent Filters - Mitigations • Will

    be fully mitigated in Android 16 (API 36) • Fully separate logic between different receivers, don’t rely solely on IntentFilters • Can use IntentSanitizer here too @balazsgerlei, balazsgerlei.com
  34. Intent Hijacking • A malicious app can hijack an (implicit)

    Intent • If the sender does not specify a fully-qualified component class name or package when sending it • An attacker can • Access sensitive data • Perform arbitrary actions (e.g., launch components) • Modify a mutable PendingIntent • Since Android 12 (API 31) the mutability needs to be explicitly set @balazsgerlei, balazsgerlei.com
  35. Intent Hijacking - Mitigations • Specify the target component, if

    possible • Narrow down the target • Don’t put sensitive data into (Implicit) Intents • Make PendingIntents immutable • Use the Photo Picker component to pick media • From androidx.activity library • Current version still supports Android 5 (API 21) – older versions Android 4.4 (API 19) @balazsgerlei, balazsgerlei.com
  36. Tasks and the Back Stack • Tasks are a stack

    of Activities • The Back Stack is the history of Activities that the user can navigate back to Activity 1 Task 1 @balazsgerlei, balazsgerlei.com
  37. Tasks and the Back Stack • Tasks are a stack

    of Activities • The Back Stack is the history of Activities that the user can navigate back to Activity 1 Task 1 Activity 2 @balazsgerlei, balazsgerlei.com
  38. Tasks and the Back Stack • Tasks are a stack

    of Activities • The Back Stack is the history of Activities that the user can navigate back to Activity 1 Task 1 Activity 2 Activity 3 @balazsgerlei, balazsgerlei.com
  39. Tasks and the Back Stack • Tasks are a stack

    of Activities • The Back Stack is the history of Activities that the user can navigate back to Activity 1 Task 1 Activity 2 @balazsgerlei, balazsgerlei.com
  40. Tasks and the Back Stack • Tasks are a stack

    of Activities • The Back Stack is the history of Activities that the user can navigate back to Activity 1 Task 1 Activity 2 Task 2 Activity 1 @balazsgerlei, balazsgerlei.com
  41. Tasks and the Back Stack • Tasks are a stack

    of Activities • The Back Stack is the history of Activities that the user can navigate back to Activity 1 Task 1 Activity 2 Task 2 Activity 1 @balazsgerlei, balazsgerlei.com
  42. StrandHogg 1 • Static attack, the malicious app needs to

    target a specific application in it’s Manifest • Can be found via static analysis • Should not go through Play Review • Can be used for • Privilege Escalation by asking for Permissions • App impersonation, phishing credentials • The user may not even know they fell victim @balazsgerlei, balazsgerlei.com
  43. StrandHogg 1 • The user need to launch the malicious

    app first • Then next time they try to launch the victim, it will be brought back to the foreground instead Malicious Activity Hijacker Task @balazsgerlei, balazsgerlei.com
  44. StrandHogg 1 • The user need to launch the malicious

    app first • Then next time they try to launch the victim, it will be brought back to the foreground instead Malicious Activity Hijacker Task Victim Task @balazsgerlei, balazsgerlei.com
  45. StrandHogg 1 • The user need to launch the malicious

    app first • Then next time they try to launch the victim, it will be brought back to the foreground instead Malicious Activity Hijacker Task Victim Task Same Task Affinity @balazsgerlei, balazsgerlei.com
  46. StrandHogg 1 • The user need to launch the malicious

    app first • Then next time they try to launch the victim, it will be brought back to the foreground instead Malicious Activity Hijacker Task Victim Task Same Task Affinity @balazsgerlei, balazsgerlei.com
  47. StrandHogg 1 • The two key configurations in Manifest: •

    android:taskAffinity – Activities with the same affinity conceptually belong to the same task. The affinity of a task is determined by the affinity of its root Activity. • Can be set to anything • If not set, it’s the application ID by default • android:allowTaskReparenting – whether the activity can move from the task that started it to the task it has an affinity for when that task is next brought to the front • android:excludeFromRecents – whether the task initiated by this Activity is excluded from the Recents screen @balazsgerlei, balazsgerlei.com
  48. StrandHogg 1 – Mitigations • Fully fixed in Android 11

    (API 30) • If your app can run on older Android versions, it’s affected • Specify an empty taskAffinity for your Activities • Much easier with a single Activity • Specify a singleInstance launch mode • Can result in broken user experience @balazsgerlei, balazsgerlei.com
  49. StrandHogg 2 • More serious than the first variant •

    Fully dynamic, implemented in code • Multiple victims can be targeted • Can load code, or the list of targets dynamically • The attacker may check which target apps are installed • Used for the same goals (permissions, credentials) • Easy to implement, hard to detect and mitigate • The user may not even know they fell victim @balazsgerlei, balazsgerlei.com
  50. StrandHogg 2 • The attacker app needs to be running

    in the background • It doesn’t need to be launched first Victim Activity Task 1 @balazsgerlei, balazsgerlei.com
  51. StrandHogg 2 • The attacker app needs to be running

    in the background • It doesn’t need to be launched first Victim Activity Task 1 Malicious Activity @balazsgerlei, balazsgerlei.com
  52. StrandHogg 2 • The attacker app needs to be running

    in the background • It doesn’t need to be launched first Victim Activity Task 1 Malicious Activity Task 2 Distraction Activity @balazsgerlei, balazsgerlei.com
  53. StrandHogg 2 • The attacker app needs to be running

    in the background • It doesn’t need to be launched first Victim Activity Task 1 Malicious Activity @balazsgerlei, balazsgerlei.com
  54. StrandHogg 2 • The key is launching three Intents: •

    1st launches the target Activity • It needs to have Intent.FLAG_ACTIVITY_NEW_TASK • 2nd is the malicious one, launching the attacker Activity • 3rd is a distraction, belonging to the attacker app that can provide benign functionality • It also needs to have Intent.FLAG_ACTIVITY_NEW_TASK • Need to be pass these to a single startActivities() call @balazsgerlei, balazsgerlei.com
  55. StrandHogg 2 – Mitigations • Fully fixed in Android 10

    (API 29) • The fix backported to Android 8, 8.1 and 9 (API 26, 27 and 28) via the May 2020 security update • Specify a singleInstance launch mode • Can result in broken user experience • Keep track of the number of Activities and abort if it differs • Hard to implement and prone to both false positives and missing the attack @balazsgerlei, balazsgerlei.com
  56. Background Activity Launch restrictions • Since Android 10 (API 29)

    apps can start activities when one or more of ~13 conditions are met, e.g.: • The app has a visible window, such as an activity in the foreground. • The app has an activity in the back stack of the foreground task. • The app has an activity in the back stack of an existing task on the Recents screen. • The app has an activity that started very recently. @balazsgerlei, balazsgerlei.com
  57. Android 15 (API 35) Android 16 (API 36) • Secured

    BAL • Don’t bring the Task to the foreground when launching Activities from the background • PendingIntent creators are blocked from background activity launches by default • Safer Intents • Not enforced (to ease adoption) • Prevent launching activities from other apps into your own task • Default security against general Intent redirection attacks • Apps will be able to enforce “Safer Intents” restrictions! @balazsgerlei, balazsgerlei.com
  58. Safer Intents 1. Intents must have non-null Actions 2. Explicit

    Intents must match target component’s Intent Filters @balazsgerlei, balazsgerlei.com
  59. Safer Intents • Not enforced in Android 15 (API 35)

    but can be detected via StrictMode (e.g., print violations to LogCat) • Apps targeting Android 16 (API 36) should be able to apply these restrictions via the new intentMatchingFlags in Manifest • none – disable all special matching rules • enforceIntentFilter – Explicit intents should match the target component's intent filter and Intents without an action should not match any intent filter • allowNullAction – Should be used together with enforceIntentFilter, and if so it relaxes the matching rules to allow intents without an action to match @balazsgerlei, balazsgerlei.com
  60. Prevent launching activities from other apps into your own task

    • android:allowCrossUidActivitySwitchFromBelow • Disallowed by default, can allow it, in a per-Activity basis • But both apps need to target the new Android version, the one in the foreground and the one in the background that tries to launch its Activity on top @balazsgerlei, balazsgerlei.com
  61. Prevent launching activities from other apps into your own task

    • android:allowCrossUidActivitySwitchFromBelow • Disallowed by default, can allow it, in a per-Activity basis • But both apps need to target the new Android version, the one in the foreground and the one in the background that tries to launch its Activity on top • Unfortunately, it has been pulled from Android 15 at the last minute (it’s not in Android 16 yet either) • Due to the many bugs reported @balazsgerlei, balazsgerlei.com
  62. Takeaways • Sanitize Intents • Allow list, not block list

    • Do Threat Modelling • Simplify Activity usage (use a single Activity if possible) • Restrict what components can join your Task • Set an empty taskAffinity instead of relying on the default • Set a restrictive launch mode for your Activity (preferably singleInstance) @balazsgerlei, balazsgerlei.com
  63. Děkuju! Thank you! • speakerdeck.com/balazsgerlei • Android Security Evolution •

    github.com/balazsgerlei/AndroidSecurityEvolution • Safeguarding user security on Android • youtu.be/RccJYep2v5I • USENIX Security '15 - Towards Discovering and Understanding Task Hijacking in Android • youtu.be/IYGwXFIYdS8 • HackTricks - Android Task Hijacking • book.hacktricks.wiki/en/mobile-pentesting/android-app-pentesting/android-task- hijacking.html • Application Security Cheat Sheet • 0xn3va.gitbook.io/cheat-sheets/android-application/intent-vulnerabilities @balazsgerlei, balazsgerlei.com