February 3rd, 2017 · Public Disclosure

The State of Mobile Banking Security

One afternoon, I randomly found myself browsing through the filesystem of my phone. I got to /data/data, where Android apps store their data. Being curious in nature, I went through a few applications I thought would be interesting, including the installed mobile banking applications, and oh boy, was I disappointed. The shared preferences XML document for one of top 3 banks in Romania – by number of assets held in 2016 – had the following information:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <string name="TOKEN_SERIAL">...</string>
    <string name="EXTRA_SECRET">test</string>
    <!-- ... -->

The EXTRA_SECRET key stored there is the password for the account, and I should mention, this password is the only thing required to login and initiate transactions. Two-factor authentication is overused on their web banking, but "optimized out for simplicity" on their mobile application. Oooookaaay, then. Wondering why would a bank store the password in plain-text, I could only come up with one possible explanation: the recent addition of the fingerprint authentication.

You see, in order to implement fingerprint authentication on top of your existing login form, one primitive option would be to just simply just store the login credentials in plain-text, possibly naming it as "extra secret", so it definitely does not grab any attention to it, and just pre-fill the password box if the fingerprint API returns true. This, of course, would be a very primitive implementation, which not even a tutorial would do. A bank couldn't possibly have done this, could they?

Checking my older Titanium backup files, I can determine that in 2014, the app mostly looked and functioned the same, but did not have fingerprint authentication. Nor did my password show up in the shared_prefs file. I quickly reinstalled the application, reactivated it, this time without fingerprint authentication, hoping my password won't be saved. Unfortunately, this check would have been too advanced to add: the password is still saved, whether I like it or not.

As a next step, I grabbed the APK off my device, and disassembled the file. Examining it, I found the following function in one of the login helper classes as additional proof that the fingerprint authentication is the culprit:

private void handleSuccesfulValidateOnTouchIdPassword(BankingService paramBankingService) {
    if (((JSONObject)paramBankingService.getServiceResponse().getData()).get("status").equals("SUCCESS")) {
        paramBankingService = new Bundle();
        paramBankingService.putString("EXTRA_SECRET", PreferenceBridge.getValue(getContext(), "EXTRA_SECRET").toString());
        PageNavigator.getInstance().navigateToView(/* ... */ paramBankingService);

On successful authentication, the function essentially just enters the correct password and continues with the authentication procedure as if nothing extra happened. This is now confirmed case of CWE-312. Smells like a really last-minute feature addition.

Now, in the XML document above I also included the TOKEN_SERIAL key. The reason for its inclusion is that this seems to function as a "user ID" of some sort. Checking the login procedure of the application, only the first half of TOKEN_SERIAL and all of EXTRA_SECRET are sent to the server.

Attack surfaces

Why is this a problem, you ask?

The phone stores all the information required to login and preform financial transactions with it. On the web banking part of the institution, two-factor authentication (via token or SMS) is used very heavily, but on the mobile application, the SMS step is "optimized out", it will only ask you to re-enter your password in order to confirm a transaction. The password, which is readable by malicious actors.

As such, a malicious actor could steal a phone, or just borrow the ones left unattended, in order to quickly find out the password, login, and cause financial damage.

Having physical access to the phone is not the only way to exploit this, just last month at least 10 million devices running AirDroid were vulnerable, including me. One of the vulnerabilities discovered in AirDroid allows an attacker to install APK files onto the user's device. However, a fresh vulnerability is not required as a lot of users fall prey to mobile banking trojans. Alternatively, one can use spear phishing and social engineering to try and lure one particular user into installing a malicious APK onto their device, although, this will not work with more tech-savvy users.

Given that this particular bank is both popular and its application stores all the information required to login to the user's account and make financial transactions, it could be a prime candidate for banking trojans targeting Romanian users. Tinba, Dridex, Dyre, Neverquest and Zeus v2 variants, to name a few. It's very possible this vulnerability was being "exploited" in the wild, as one of the main functions of a banking trojan is to collect credentials.

Shifting the discussion from FUD to something technical, you may say but you cannot read /data/data without root, which is true, but we can get creative and explore our options:

Exploiting without root

One of the things the developers of the application forgot to do, besides using common sense and following best practices, is to disallow backups of the application data. Setting android:allowBackup="false" in AndroidManifest.xml would have killed this section, but it is not set.

As a result, it is possible to create a backup, open it and extract the password from the created archive using this one-liner:

$ adb backup ro.bankName && (printf "\x1f\x8b\x08\x00\x00\x00\x00\x00"; tail -c+25 backup.ab) | tar xOzf - 2>/dev/null | grep -a EXTRA_SECRET
<string name="EXTRA_SECRET">test</string>

This does require physical access to the device in order to enable ADB and confirm the backup procedure.

Piggybacking on other exploits

One of the biggest problems in the Android world is the fragmentation of the versions running in the wild. Google did step up their game regarding security updates, but other vendors did not. This means that the average Android user in Romania might be vulnerable to either CVE-2016-5195 (also known as Dirty COW), or CVE-2016-8655, which is a similar yet different local privilege escalation vulnerability in the Linux kernel.

Both of the mentioned kernel vulnerabilities can be used to root Android quite efficiently. A proof-of-concept Dirty COW rooter is available at github.com/timwr/CVE-2016-5195, however, the latter vulnerability is also confirmed to work on Android in the disclosure message itself. After the malicious APK was installed on the device, one way or another, using the methods discussed above, this malware can now auto-try all known vulnerabilities, and see if any of them work. This technique is not so far-fetched, and malware doing this already exist. Upon successful privilege escalation, the /data/data directory becomes readable.

Interestingly enough, CVE-2016-8655 was publicly disclosed after I disclosed this vulnerability to the bank, but before a fix was shipped, proving once again that you cannot rely on the integrity of the devices, such as checking whether the device is rooted or whether SafetyNet is tripped, if you're not taking any active approaches to protecting secrets in your application.

Possible fixes

Recent versions of Android expose the "secure enclave"/Trusted Execution Environment for programmers to use. Essentially, this allows the creation and use of encryption keys which are stored on a secure platform, and cannot be retrieved using standard Android backup tools, or even by reading the flash directly. Thankfully, fingerprint is also a recent addition to Android phones, therefore, if you can use that, you can also most likely use another addition to the Android API, which couples the encrypted storage's key release functionality with the fingerprint authentication: FingerprintManager.authenticate()

See the AsymmetricFingerprintDialog sample application in the Android SDK for more information on how to use this functionality.

Actual fix

The new version with the patch was published on the Play Store nearly 3 months after the initial disclosure to the bank. My first immediate note is that the application does not require users to provide a new password, however, more importantly, while it does not use the "extra secret" entry anymore, it does not clear its current value. As such, users who activated the app before the patched version was published and have not changed their password since (after all, there was no indication to do so) are still vulnerable.

Disassembling the APK yet again, reveals the approach they took for fixing this: all calls using this variable mostly remained the same, with the only change being, that instead of the password being retrieved from shared preferences, it is now retrieved from a separate class. Delving deeper into said class shows that the password is now stored as an encrypted binary file, which uses AES-256 as the cipher, and the appropriate hardware-backed Android keystore mechanism for retrieving the decryption key.

In summation, this was a very primitive issue that should not have happened in the first place. The bank's communication was also lacking, since they did not care at all, until I got third parties involved and threatened with public shaming. This idea was given to me on IRC, after my initial attempts to contact the bank, but before they actually took me seriously, it was suggested that I should go via the public shaming route. I was not sure what the repercussions would be, nor did I want to provide this information to banking malware authors, so I've decided not to do this. Instead, I've decided to involve competent third parties and suggest public disclosure if the bank does not consider this a serious vulnerability. Even today, after the fix was published, I've decided to withhold the name of the bank, as current users might still be vulnerable, due to the application not clearing the previous password.

Disclosure timeline