Android app pentesting is an invaluable tactic for identifying the weaknesses and possible penetration points in your apps. In the age of constantly evolving sophisticated cyber-attacks and data breaches penetration testing is mandatory.
We’ve spoken about networking and data storage issues in our previous blog posts, so this final installment in the series can wrap up the story by covering the “miscellaneous” category, the remaining common cases in a pentest.
You’ll find the previous posts here:
Let’s jump right into the cases in no specific order.
Root detection
The Android operating system on our phones is a commercial version of the OS provided by the manufacturer. This means that the end-user doesn’t have full control over their device due to system-level restrictions and safeguards.
To bypass these, it’s possible to perform rooting, which grants the user root access. With a rooted device, the access control imposed by the operating system is compromised and we can’t guarantee the application sandbox features will securely protect our app’s private data.
In the worst-case scenario, the data can become exposed to malicious software that manages to elevate its privileges to root access.
Preparation phase – What you’ll need to know
To protect apps on rooted devices, we first need to detect whether a device is rooted. We do this by performing root detection.
It is important to know that no solution will give you a 100% accurate result. If the result indicates that the device is in fact rooted, we should follow the best practice recommended by OWASP in their Mobile Security Testing Guide book (page 84). We should notify the user that the app runs on a rooted device and that certain high-risk actions will carry additional risk due to its status, or just completely block our app’s usage.
For detecting rooted devices we recommend using an existing implementation rather than doing it from scratch. Google provides SafetyNet which has a set of API-s that help protect your app against security threats altogether.
SafetyNet has no specific API that can tell us if a device is rooted, it’s not a part of its design. However, we can use it to check the flags that we receive from the SafetyNet backend. The flags that we are looking for are ctsProfileMatch
and basicIntegrity
. If these two flags turn out false, it implies that the system integrity has been compromised, and rooting is a potential cause.
There is no need to get into implementation details because the official documentation site offers good descriptions. You can also find some code examples in this repo.
However, you might have noticed that SafetyNet has a limit of 10,000 requests per day across your user base. This is a problem for most apps that are actually interested in that kind of a service but fortunately, there are ways to handle this limitation. The official way to handle it is described in the official documentation.
If this option doesn’t work for you, you can implement a workaround that could cover most of your cases. It includes using the RECEIVE_BOOT_COMPLETED
permission and running the SafetyNet request only when your application receives the mentioned system reboot event. This will keep your API quota limit under control, but it’s not a future-proof solution. The official way is always the preferred one.
A good alternative to SafetyNet is RootBeer written by Scott Alexander-Bown who also wrote the Android Security Cookbook. You can also use it in combination with safetyNet to control your API quota limit.
Logs
Logging is very common in every application. We usually use it to pinpoint an undesired behavior, but we can also use it to track fatal and non-fatal crashes on our crashlytics tools.
Logs can be valuable during the development process. In most cases, we treat them as harmless pieces of characters. Unfortunately, that approach can sometimes put us in an undesirable situation where we can leak sensitive data through our logs.
As Android developers, we use Logcat for inspecting our application’s logs. We only need a couple of command lines and we get a live dump of all the logs, not just from an individual app, but the entire system. A potential attacker could easily do the same and if they found some useful info, they could use it to exploit our application or the entire system.
Preparation phase – What you’ll need to know
To prevent data leakage through logs, it’s best to remove logs from production builds. Luckily, there are some handy tools to handle this task for us.
For example, we can use Timber for logging information and use the Tree
feature to separate logging implementations for different application variants. We can use the default DebugTree
from Timber to log information through Logcat in debug builds. For release builds we can create our own implementation that we will use to send information to our analytics or crashlytics tools. To implement your own tree, you can extend the Timber.Tree
class and add your logic inside the log method:
class ReleaseTree() : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
// handle received logs
}
}
After you have your custom tree implementation you can set it inside the application class depending on the desired buildconfig, something like this:
Timber.plant(if (BuildConfig.DEBUG) Timber.DebugTree else ReleaseTree())
In case you don’t use Timber you might stumble upon a ProGuard option to remove all log messages using this proguard rule:
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
public static int wtf(...);
}
Keep in mind that the rule above will not help you with dynamically constructed strings used to log the data using the Log methods later on. This is due to the fact that dynamically constructed strings (i.e using StringBuilder) can still be seen in the bytecode. For more details about this particular issue, you can check the MSTG source.
Also, be careful about the above proguard rule because you can find a lot of similar rules on the Internet that can break your application. Check this article for more information. Always double-check your source when it comes to security!
Tapjack
Tapjacking is an old security issue that was well known on devices running Android versions 4.0.3. However, it can still be dangerous today if a user is unaware of it.
The exploit is based on the Android permission SYSTEM_ALERT_WINDOW
which allows an app to draw an overlay over other apps. Attackers can use this option to create an overlay that would essentially hijack user taps and use it to obtain sensitive user information, hence the name tapjack.
The Android runtime permission introduced in Android API level 23 is a good measure of protection against tapjacking attacks because the users have to manually grant the permission to draw over other apps. Before API level 23 any developer could just add the permission in the manifest file and they would be able to use the feature immediately when the app is installed.
On some devices that run Android API level 23, there is a system-level security issue with the SYSTEM_ALERT_WINDOW
. An app could use the exploit to draw an overlay over the system permission dialog. That would make it possible for the attacker to change the text of the permission dialog to make the user think the permission they are about to grant is not dangerous, while in reality, it could be the opposite.
In the worst-case scenario, this would allow the attacker to obtain user information from the device. Here you can find more information about that specific case.
Another very popular attack class involving the SYSTEM_ALERT_WINDOW
permission is the cloak-and-dagger which also enables some advanced tapjacking attacks.
Preparation phase – What you’ll need to know
Let’s imagine a situation where your app has a login screen and the user has to enter their login credentials. Let’s say that the user also has an app on their device that they trust, but is unaware that the app has malicious intent. That kind of an app can use the SYSTEM_ALERT_WINDOW
to draw a transparent overlay over any other app and is specifically designed to track keyboard inputs.
With some additional effort, the attacker could obtain our user’s login credentials for our app. To take it another step further, the attacker could literally obtain anything that the user enters on the keyboard.
To protect your app from these kinds of attacks, Android offers a built-in solution to detect overlays over specific UI elements that we think are exploitable. You can do this by using the following XML tag:
android:filterTouchesWhenObscured=”[true|false]”
Keep in mind that by setting this flag to true, the view will not receive touches whenever a toast, dialog, or other window appears above the view’s window. This will get the job done, but it is not recommended to leave it that way because the users will have a terrible time using your app. Imagine you want to press a button and nothing happens, that’s just bad user experience.
Luckily, we have a way of detecting when exactly this happens. We can override the onFilterTouchEventForSecurity
method in a compound view used to place it above other views. That way it can intercept obscured touch events and react to them.
override fun onFilterTouchEventForSecurity(event: MotionEvent): Boolean {
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED == MotionEvent.FLAG_WINDOW_IS_OBSCURED) {
// React to obscured events
return false
}
return super.onFilterTouchEventForSecurity(event)
}
Fortunately, tapjacking, even though potentially very dangerous, is easy to handle. You should make sure your users are aware of the problem because the lack of awareness itself might be the biggest problem with this exploit.
There are signs that Google might deprecate the permission entirely. Android Go versions are already not allowed to use that feature. Also, features like Facebook Chat bubbles, which rely on the SYSTEM_ALERT_WINDOW
permission , are receiving alternate API solutions to cover their use cases. It could be taken as an indication that the long-term plan is permission removal. We will have to wait and see.
Clipboard manager
Clipboard is one of Android’s system components. It is exposed to developers through a class called ClipboardManager which grants copying, monitoring, and paste operations for specified data.
One of the clipboard component’s main characteristics is globally accessible to any app running on the device and no additional permission is needed. This leaves users vulnerable in case there is a malicious app on their device that could sniff out its clipboard and obtain passwords, credit card details, and other sensitive user information. This article shows how Android’s clipboard could be exploited in a password manager app.
Preparation phase – What you’ll need to know
Sadly, there is no good way to mitigate attacks on data contained in the clipboard from within our app, so it should be handled on a system level. There are some mitigation tactics but they strongly depend on the use case you want to achieve. One of the most common and safest approaches is to disable the copying of sensitive data altogether. This better-safe-than-sorry scenario can leave your users with a bad UX, but it protects those less technically savvy.
Screenshots
When you think about it, taking screenshots is very similar to copying plain text. The vulnerability area is very similar to the one from the previous chapter. The only difference, except the data type, is the location of the saved data. Screenshots are usually saved in the device’s internal storage in a folder named Screenshots, but this may slightly vary depending on the manufacturer.
Preparation phase – What you’ll need to know
The mitigation process is also very similar to the one for Clipboard manager. Luckily, we can use the Android built-in API to determine which screens can be screenshotted and which are prohibited. We do this by using the window flag FLAG_SECURED. The snippet below shows how to set the flag in a fragment.
requireActivity().window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
This will also protect your app from screen-recording apps.
Inter-process communication (IPC)
When we install our apps on the Android OS they run in their own secure sandbox. However, like any other system, Android also gives us IPC capabilities to communicate with the rest of the system or with other apps.
We can use more traditional ways like using shared files to communicate, but this is not recommended since we have more evolved solutions provided by the Android OS. One of those solutions is Services. They are components used for long-running operations in the background, but they can also be used for IPC with bound services where other components can bind to a service and interact with it.
Further, there is ContentProviders or a more advanced mechanism like AIDL.
Probably the most common way we use for communication between different Android components is Intents. An Intent is an abstract description of an operation that we can use to communicate with Activities, BroadcastReceivers, and Services. This is a broad subject so we will try to focus on the main techniques for protecting your application components from unwanted interactions.
Preparation phase – What you’ll need to know
There are several tools at your disposal for making your app safe for IPC. If your app does not handle IPC, make sure that your main components don’t have the exported flag set to true in the manifest. It is false by default, but sometimes we set it to true in case we want to start a specific activity from Android Studio for faster development. Just ensure you don’t forget to check that flag before release or create a custom lint rule if you already don’t have it.
Suppose your components need to be exported for IPC. In that case, you should make it restrictive because sniffing broadcasted Intents is as simple as writing a terminal command using tools like Drozer.
What you want to do is set the corresponding permissions for your components so that you indicate to targets that want to communicate with your application that they need to follow the rules you set. Together with that tag and the protection level you can control the restriction level for your components.
For example, suppose your company has multiple applications in production and you want to make sure that only your applications can communicate with each other in order to avoid impersonated intents from potential attackers. In that case, we can define a custom permission inside our manifest like this:
<manifest package="com.example.myapp" >
<permission
android:name="com.example.myapp.permission.READ_USER_DATA"
android:label="Read user data"
android:description="Can access user data like email, username, password, ..."
android:protectionLevel="signature" />
</manifest>
You’ll notice that protectionLevel is set to signature mode, which gives us the desired effect described above. In other words, this is automatically granted to a requesting app if that application is signed by the same certificate. For more information about other protection levels, check the protectionLevel documentation. With our custom permission defined, we can use it in our components. For example, if we want to protect our service with this permission, we would write the following code in our manifest:
<service
android:name=".data.session.SessionService"
android:permission="com.example.myapp.permission.READ_USER_DATA"/>
We can add this to the application tag in our manifest to protect our entire application with this permission.
Alongside the exported flag and custom permissions we also have intent filters in our arsenal to protect our applications from malicious and undesired IPC.
Intent filters can be used to specify the types of intents that an activity, service, or broadcast receiver can respond to. That way we can anticipate the input in our components. Whatever you decide to use, it is always good practice to check the data you receive programmatically to see if it is something that you expect.
Conclusion
Pentesting is an ever-evolving subject, so this is by no means an exhaustive guide. Some important topics were left out due to their complexity, such as WebView vulnerabilities, reverse engineering, and dependency control.
Protecting your applications from attacks is challenging and sometimes even impossible because more sophisticated attacks are constantly evolving and these won’t be easily withstood using standard tweaks and tricks. Protection against them requires tedious manual analysis, coding, and probably lots of frustration.
One thing is certain though, to prepare your applications in the best possible way it is important to stay in the loop. Keep in touch with the community and experiment with attacks on your own. If you know how malicious code works, it is easier to get the know-how to mitigate it.
Testing tools
As a final note, here is a list of useful tools that can help you analyze your applications and hopefully make them a bit more secure:
If you want to look into more ways to improve the security of your digital product, explore our cybersecurity services.