By Joe Sewell | May 2018 | Get the Code
In last November’s issue, I illustrated how you can use Runtime Checks, a code injection feature included with Visual Studio 2017, to protect your .NET Framework apps from unauthorized use of a debugger, as well as from tampering (msdn.com/magazine/mt845626). Since then, a new type of Check has become available. The Root Check detects when a Xamarin.Android app is running on a “rooted” device—one that allows ordinary apps to act with administrator permissions (root access).
In this follow-up article, I explain why rooted devices pose a risk that all Android developers must understand; detail how Xamarin.Android developers can use Root Checks to detect and respond to that risk; and demonstrate best practices with an example scenario.
The Xamarin platform allows you to efficiently create mobile apps for Android, iOS and Windows devices. Developers familiar with .NET languages like C# can take that knowledge and apply it to the mobile space. Technologies like Xamarin.Forms abstract away many of the differences between platforms, reducing the complexity, cost and risk of developing cross-platform apps. By keeping your Xamarin tools up-to-date, you can continue to support new versions and features of each platform.
However, some platform-specific aspects of mobile development do deserve a developer’s attention. One such aspect is security. Each platform has unique security risks and a unique security model to address those risks. For example, the permission systems differ among platforms and sometimes even among versions of the same platform.
For Android apps, rooted devices are a particularly important security concern. Such devices have been modified to allow apps to break out of the normal security sandbox that the OS imposes. This can expose the device to many dangers, such as malware and password-stealing keyloggers. Often, users root their devices to solve some problem—like wanting a version of an app that’s not normally available for their device—without realizing the severity of these threats. In other cases, a user may not even be aware that the device is rooted and thus vulnerable.
Last September, the Payment Card Industry Security Standards Council (PCI SSC) issued version 2.0 of the Mobile Payment Acceptance Security Guidelines for Developers. To combat the security risks associated with rooted devices, the guidelines recommend mobile app developers implement root detection and a response mechanism to quarantine the app (bit.ly/2H5ymge). Here’s the relevant text from section 4.3 (emphasis added):
[T]he device should be monitored for activities that defeat operating system security controls—e.g., jailbreaking or rooting—and, when detected, the device should be quarantined by a solution that removes it from the network, removes the payment-acceptance application from the device, or disables the payment application. Offline jailbreak and root detection and auto-quarantine are key since some attackers may attempt to put the device in an offline state to further circumvent detection.
In addition to risks associated with a legitimate user operating the app in a rooted environment, such an environment can also indicate a malicious user attempting to reverse engineer the app. Attackers frequently use rooted devices to study and create tampered versions of apps, which they then fill with malware. The Open Web Application Security Project (OWASP) lists code tampering as one of the Top 10 Mobile Risks (bit.ly/2GNbd4o) and specifically calls out root detection and response as a way to combat this risk. Not doing so, according to OWASP, can lead to reputational damage and lost profits.Root Checks
Detecting rooted devices can be challenging. A device can be rooted using many different techniques, and the set of available techniques changes over time and across Android versions. As a result, root detection code must constantly evolve and adapt. This is compounded by the fact that some malicious rooting techniques attempt to conceal their use, so good root detection code must also address these countermeasures. Maintaining up-to-date root detection code is tricky and may not be where you want to spend your limited resources.
Luckily, you don’t have to write your own code to detect rooting. PreEmptive Protection - Dotfuscator Community Edition (CE), which is included with Visual Studio 2017 for Windows, can inject Root Checks into your Xamarin.Android apps. Root Checks detect rooted environments, even when the device is offline. In addition to a standard “exit the app” action, you can configure the Checks to respond to rooting by calling customized app code.
Just like Xamarin itself, Root Checks reduce complexity, cost and risk compared to rolling your own implementation. Keep Dotfuscator up-to-date and let it handle the root detection—get back to work on the interesting parts of your app quicker.Sample Scenario
To demonstrate Root Checks, I’ve provided a sample app called Protected-TodoAzureAuth. It’s based on an existing Xamarin.Forms sample, TodoAzureAuth (bit.ly/2InvU48), originally written by David Britch.
The remainder of this article explains the app, the protection strategy I applied to it, and how I applied that strategy with Root Checks. You can use this case study, as well as additional scenarios included in the sample’s GitHub repository (bit.ly/2GQutOv), to learn approaches to Root Checks you can then apply to your own Xamarin.Android apps.
Original Sample: TodoAzureAuth connects to a Microsoft Azure Mobile App instance, enabling users to view and modify a shared to-do list. To demonstrate how to perform authentication in a Xamarin app, the sample requires the user to log in with a Google account before accessing the to-do list.
The app begins on the Login Page, which has no fields, just a Login button. When the user selects this button, the app delegates the login process to Google’s OAuth system, which may require the user to enter credentials, including a password. As a result, the app itself doesn’t handle the credentials. Once the user has logged in, the app displays the Todo List Page, allowing the user to access the shared to-do list. The user can log out and return to the Login Page by selecting the Logout button.
Protection Strategy: For this article, I treated the TodoAzureAuth Android project, TodoAzure.Droid, as if it were handling sensitive data, like a PCI-compliant app would. I implemented an appropriate protection strategy by using Dotfuscator CE to inject a Root Check into the app, producing a protected version of the app, Protected-TodoAzureAuth.
In the protected app, when the user selects the Login button, the Root Check activates. If the app is running on a rooted device, it exits abruptly, and all further attempts to run the app will also exit after a short error message, even if the device is no longer rooted. Figure 1 shows an overview of the app protected by this strategy.
Figure 1 Overview of the Protected-TodoAzureAuth Sample App
This strategy aligns with the recommendations made by the PCI guidelines quoted earlier:
- The app monitors the device for rooting.
- The app quarantines the device by disabling itself if rooting is detected.
- This security control operates even when the device is offline.
When the app disables itself, the error message alerts the user that the device is unsafe. While not used in the sample, an app using this scenario could also “phone home” to an analytics platform such as Visual Studio App Center (bit.ly/2pYMuk5).
In addition to following the PCI guidelines, this strategy also lines up with the OWASP recommendation to shut down the app in a rooted environment to prevent reverse engineering. I configured the Root Check to activate at other parts in the code, not just the Login process, so that if an attacker produces a tampered version of the app with the Login process’s root detection removed, other parts of the app can still react to rooting. Dotfuscator also obfuscated the code, adding another layer of protection to the app and the Root Check.
Not all apps have the same security requirements, and thus not all apps should react to rooting in the same way. I chose a strict approach for the sample, but a more lenient strategy could allow the app to run on rooted devices in certain circumstances. For an example, see “An Alternate Protection Strategy.”
Protected Sample: You can view the Protected-TodoAzureAuth sample using the GitHub link supplied earlier. On the default master branch, I’ve already configured Dotfuscator CE to protect TodoAzure.Droid with a Root Check so that the app meets the strategy explained earlier. You can follow the Git history, starting with the before-checks branch, to see how I applied the steps in this article to the sample.
Please see the sample’s README for details on how to set up, build and run the sample. The README also includes details on other branches present in the repository that demonstrate different protection strategies than the one used for this article, such as the strategy detailed in “An Alternate Protection Strategy.”Integrating Dotfuscator into Xamarin Builds
Because Dotfuscator operates on .NET assemblies (.dll and .exe files), not mobile platform formats like Android Packages (.apk files), I had to integrate Dotfuscator into the Xamarin build process. Setting up the integration requires installing and registering Dotfuscator CE, downloading a specialized MSBuild targets file and modifying the project file to include those targets. I’ve written on how to perform these integration steps for the Xamarin Blog, so please see those instructions at bit.ly/2w9em6c.
Important Note : Root Checks require Dotfuscator CE version 5.35 or later for Visual Studio 2017. You can always get the latest version at bit.ly/2fuUeow.
I followed the Xamarin Blog instructions to protect the TodoAzure.Droid project file’s Release | AnyCPU build configuration. This article only concerns this Android project because Root Checks are an Android-specific feature, but you can also follow the Xamarin Blog instructions to protect iOS and Universal Windows Platform (UWP) projects with code obfuscation.Configuring the Dotfuscator Protection
Once I integrated Dotfuscator into the TodoAzure.Droid project’s build process, I configured the protection through the Dotfuscator CE UI. The protection settings for a project are saved in a specialized Dotfuscator config file, which the build integration adds to your project the first time it builds.
Creating the Dotfuscator Config File: Using Visual Studio 2017, I built the TodoAzure.Droid project in the Release build configuration for the AnyCPU platform, which is the configuration I had set up to use Dotfuscator. This produced a new Dotfuscator config file, DotfuscatorConfig.xml, in the project’s directory. I added this new file to source control so I could later customize and reapply the protection based on that customization.
The build also created a DotfuscatorReports directory in my project directory, which is where Dotfuscator writes various report files when it runs as part of the integration. Because this directory"s contents update every build, I had my source control ignore this directory.
Opening Dotfuscator: To customize the Dotfuscator config file, I opened the Dotfuscator CE UI from Visual Studio 2017 by choosing Tools | PreEmptive Protection - Dotfuscator. In the Dotfuscator UI that appeared, I chose File | Open Project, navigated to the TodoAzure.Droid project’s directory and selected DotfuscatorConfig.xml, the Dotfuscator config file. The Dotfuscator UI updated to display the two assemblies this Dotfuscator config file protects: TodoAzure.Droid.dll itself and the portable class library (PCL) assembly TodoAzure.dll.
Keep in mind that the Build Project option in the Dotfuscator UI doesn’t perform Xamarin packaging steps. To ensure the Android Package contains the protected assemblies, build the project from Visual Studio or MSBuild instead.
Enabling Code Injection: Checks are part of the Dotfuscator code injection features. To enable code injection, I right-clicked the Injection node in the Dotfuscator navigation bar and checked the Enable option. The Injection node’s text color changed from gray to black, indicating that injection was enabled.
Viewing Configured Checks: The Dotfuscator Checks page displays a list of all configured Checks for a loaded config file, in this case DotfuscatorConfig.xml in the TodoAzure.Droid project. To view this page, I selected the Injection node and switched to the Checks tab.
When I first visited this list, it was empty. Once I configured a new Root Check, as I explain in the next section, the list updated to include a row for that Check, as seen in Figure 2. I could view the configuration for the Check by double-clicking that row.
Figure 2 The Dotfuscator Checks Page, Showing a Root Check
Note that you can configure more than one Root Check for a single Dotfuscator config file, though I didn’t do so for this article. For an example of an app protected by multiple Checks, see the AdventureWorksSalesClient .NET Framework app I wrote about last November.Adding a Root Check
From the Checks page, I added a Root Check by clicking the Add Root Check button. When I did, Dotfuscator displayed a new window for configuring the new Check. Figure 3 shows the finished configuration; this section explains the meaning of each setting and why I chose those settings.
Figure 3 Configuration of the Root Check—Additional Locations Hidden by the Collapsed TodoAzure.dll Node
Locations: Each Check is associated with one or more methods in the app, called Locations. When the app calls such a method, the Root Check activates, detecting at that moment if the device appears to be rooted. After executing all configured reporting and response functionality, assuming those measures didn’t exit the app, the Check returns control to the top of the method.
For this scenario’s Check, I selected multiple Locations. The Location first used in the app is TodoAzure.Droid.MainActivity.AuthenticateAsync, which coordinates a login request. Using this location means the Root Check will perform its detection and response whenever the login process begins.
Per the protection strategy, an app running on a rooted device exits when it first reaches the AuthenticateAsync method. So why did I add other methods that occur later in the app’s lifecycle as additional Locations? This is to help the app defend against reverse engineering. If an attacker creates a tampered version of the app that bypasses or removes the Root Check code at AuthenticateAsync, these other Locations will still be able to react to a rooted environment.
Some of these additional Locations are defined in TodoAzure.dll. This can be surprising, as that assembly contains logic common to all Xamarin platforms, not just Android. How can Dotfuscator inject a Root Check—which detects rooted Android devices—into a platform-agnostic assembly? Recall that this Dotfuscator config file is specific to the TodoAzure.Droid project, which references the TodoAzure project. When Dotfuscator modifies TodoAzure.dll, it will modify only the assembly that Visual Studio or MSBuild copies for use in the current project, TodoAzure.Droid. The original TodoAzure project"s assembly remains unchanged.
Application Notification: Checks can report the results of their detection to app code. This allows you to have customized reporting and response behaviors, while having the Checks injected by Dotfuscator handle the detection work. The app code that receives the detection result is called an Application Notification Sink.
To meet the protection strategy in this scenario, I needed to have the app disable itself, so future runs of the app exit with an error message. I chose to add this disabling logic in a method, TodoAzure.App.DisableIfCompromised, and use it as the Check’s Sink by setting the following Check Properties:
- ApplicationNotificationSinkElement: The kind of code element; in this case, a method.
- ApplicationNotificationSinkName: The simple name of the code element; in this case, DisableIfCompromised.
- ApplicationNotificationSinkOwner: The type that contains the code element; in this case, TodoAzure.App.
Any of the Check"s Locations can call this Sink method as it’s public and static. To be compatible with a Check, the method is synchronous (non-async), takes a single bool argument and has a void return type.
When activated, the Check calls the method, passing in the argument true if the device is rooted and false otherwise. When this argument is true—that is, when the Check detects rooting—the method saves a value to local storage, indicating the app is now disabled. An accompanying property, IsDisabled, exposes the saved value:
After the app is disabled, future runs need to show an error message and exit. To do this, I overrode TodoAzure.LoginPage.OnAppearing, which is called right before the Login Page is shown when the app starts. If the app is disabled, this method hides the Login Page, displays an error dialog and then exits.
Because I also want to defend against reverse engineering, I took additional measures to ensure the app would be more resilient to such an attack. I used a vague name for the saved value, AppStatus, and set the value to a random number, which obscures the meaning of the value. I also configured Dotfuscator to obfuscate the app, renaming identifiers like DisableIfCompromised, so an attacker viewing decompiled code will not easily identify this method as being of interest. For details on how I configured renaming obfuscation, see the sample’s README.
Action: While the Sink (the DisableIfCompromised method) sets a property to ensure future runs of the app exit, it doesn’t itself exit the app when rooting is first detected. Instead, I configured the Check to do this automatically by setting the Action Check Property to Exit.
When the Check detects a rooted device, it notifies the Sink and then immediately exits the app. By having the Check, rather than the Sink, perform this initial exit, I spread multiple copies of the exit logic through the app. Just as with multiple Locations, multiple copies of the exit logic allow the app to better defend itself when an attacker has removed some of the Root Checks.Building and Testing the App
After configuring the Root Check, I exited the Check’s window by selecting OK, then I saved my changes to the Dotfuscator config file by choosing File | Save Project. I built TodoAzure.Droid in Visual Studio to test the protected app, in order to verify that I correctly configured the Root Check to enforce the intended protection strategy.
I tested the app on a non-rooted device, on a rooted device and on an emulator. On the non-rooted device, the app functioned normally, allowing me to log in to view the to-do list. However, on the rooted device and on the emulator, after selecting the Login button, the app abruptly closed. After re-launching the app, the app displayed the error dialog shown in Figure 4; after I closed the dialog, the app exited once more. To view the Login Page again, I had to uninstall and then reinstall the app.
Figure 4 The Protected TodoAzure.Droid Running in an Emulator
I hope this article has helped illuminate a way to effectively detect and respond to rooted Android devices using free tooling included with Visual Studio. While I used a well-known sample app as reference, you can apply the ideas introduced in this article to all kinds of Xamarin.Android apps and to various other protection strategies.
If you’re interested in learning more about Checks, I recommend reading my previous MSDN Magazine article. In it, I discussed additional kinds of Checks that you can apply to .NET Framework apps and how using Checks can prevent data breaches.
You may also be interested in the advanced Check and obfuscation features of Dotfuscator Professional Edition (bit.ly/2xgEZcs) or the companion tool for Java and traditional Android apps, PreEmptive Protection - DashO (bit.ly/2ffHTrN). You can keep up-to-date with all developments in Checks and PreEmptive Protection by following PreEmptive Solutions on Twitter (twitter.com/preemptive) and by visiting our blog (preemptive.com/blog).
This article presents one strategy for detecting and responding to rooted devices, but other strategies are also possible. The strategy that you choose should be appropriate for your app, the context in which your app is used and the security risks you want to address. You can then apply Root Checks to implement the chosen strategy. For instance, if your app must not exit under any circumstances, you could configure a Root Check to disable certain features when rooting is detected, rather than exiting the app.
A single app might even need to react to rooting in different ways depending on information known at runtime. Consider an app available in multiple geographical regions. The app’s response to rooting might need to vary by region to comply with local laws and regulations, especially if that response includes sending incident reports back to the developer (“phoning home”).
When developing a protection strategy, you also have to consider the impact that your strategy will have on users of your app who have intentionally rooted their devices in good faith. These “power users” might be a significant portion of your customer base, and disallowing rooted devices could drive them away from your app. You have to weigh the security risks associated with rooted devices against the business risks associated with alienating legitimate users of such devices.
For this article, I assumed that TodoAzure.Droid handled sensitive data, and thus, to prevent data theft and reverse engineering, rooted devices should be totally prohibited. If I had instead treated the data as non-sensitive, I could have implemented a protection strategy that allows rooted devices in certain circumstances, making the app more accessible to power users. In this alternate strategy, instead of the app disabling itself, the app warns the user when a rooted device is detected. This warning ensures the user is aware of the insecure status of the device and associated risks like credential theft. The user can choose to cancel the login attempt or to accept the risks and continue logging in.
On the warn-users branch of the Protected-TodoAzureAuth GitHub repository, I’ve configured Dotfuscator CE to protect TodoAzure.Droid with a Root Check that implements this alternate protection strategy. The README on that branch explains the finer points of the configuration.
Note that this alternate strategy makes a trade-off between accessibility to power users and the threat of reverse engineering. Under this strategy, bad actors can still install the app on a rooted device for the purposes of reverse engineering; the warning dialog wouldn’t stop them. I still used Dotfuscator to obfuscate the app, providing a degree of protection from reverse engineering. A real app could implement additional controls, like requiring special authentication to use the app on a rooted device.
Figure A shows the warning displayed by the app when run on a rooted device.
Figure A TodoAzure.Droid, Protected by this Alternate Strategy, Running in an Emulator, After the User Selects the Login Button
Joe Sewell is a software engineer and technical writer on the Dotfuscator team at PreEmptive Solutions. He has previously written for MSDN Magazine and the official Xamarin Blog.
Thanks to the following Microsoft technical expert for reviewing this article: David Britch
David Britch works in the Xamarin documentation group at Microsoft. He has written for a range of software development publications including books, guidance documentation, reference implementations, whitepapers, videos, and instructor-led training courses