Exploiting App PIN Mechanisms on Android

Locating and exploiting custom application protection methods.

The Mobile Security Guys
Level Up Coding

--

Mobile applications are part of our daily lives, from banking, messaging, health, financial to social networking; but what do most of this type of app have in common?

They all hold some sort of private user data.

Whether that data is account numbers, private messages, credit card statements, or the location where you last checked yourself in… that nice cafe round the corner from your house. Our devices store an awful lot of information that would aid someone in potentially stealing an identity.

Photo by Morning Brew on Unsplash

We rely on the app developers to adequately protect the data we put into their apps.

If we, as users, decide we want to use an app, we have to rely on the developers to create a secure app and protect our stored data; that is the implicit trust we place onto them. We have usernames, passwords, biometrics, but what about the custom security features that are added into applications in an attempt to safeguard this private information away from malicious intent; protections such as PINs?

In this post, we will take a look at an app that has a custom PIN entry mechanism as a separate line of defence should anyone else have physical access to the device or know the lock screen password. This would be a secondary authentication mechanism in an attempt to balance usability and security, so the user doesn’t have to login to the app every time they use it.

We’re not talking about how the PIN is stored or the encryption surrounding it. We‘re interested in looking at the actual logic and internal workings of the PIN implementation and the underlying weaknesses.

One PIN to Rule Them All

Code

In this first example, we will show how it is possible to patch the application so that it allows any PIN to work, regardless of the original PIN created by the user.

On looking at the Java code, we see a method called onPINSubmit(), we can see this returns void so nothing is passed back to the calling method. All the PIN function is handled here.

In order to bypass this authentication, we need to ensure we always trigger the appropriate IF conditions.

  1. A PIN has previously been setup, and therefore exists.
  2. The submitted PIN string is not empty.
  3. Comparing the stored PIN with the one just submitted.

If the PIN is valid, the IF is passed and the code continues running on lines 153 onwards.

While looking at the smali we can clearly see these IF conditions and the result of the check being passed into the variable, v0, and jumping to cond_2 depending on the value, lines 477, 485 and 493. These statements match the three bullet points above, so the line 493 does the actual comparison of the two PIN strings which is the last check.

This comparison is done with the statement, if-eqz v0, :cond_2, whereby it jumps to cond_2, if the value of v0 is equal to zero. Equal to zero meaning that if the strings do not match the result would be 0/False. If the PIN strings matched, the result would be 1/True and jump to cond_2 would not be taken.

The result of the previous conditions, isEmpty, uses the opposite, if-nez v0, :cond_2, and therefore would jump if the result was 1/True.

Three conditions that make up the conditional comparison of the stored and entered PIN.

Patching

It is then clear to see that we do not want to jump to cond_2 and the last check on line 493 is the most critical, as this is the last condition which changes the value of v0. If we modified the code above this, v0 would be overwritten with another value once the check finished.

By noticing the return values of the methods are Boolean, we can start to build our PIN bypass.

We can also see that the return values of isEmpty and equals methods are all Boolean values which provides our way of patching this check.

Firstly we could modify the line which accepts the value from the equals check of the strings, rather than moving the result of the actual check we could hardcode a constant of 1 into v0.

move-result v0  -->  const/4 v0, 0x1
Hardcoding a value of 1 to ensure the check returns True

A second patch could be simply reversing the logic of the smali conditional statement so that it jumps to cond_2 on a valid PIN, not an invalid one.
This however may trigger the user to notice something is amiss when they attempt to use the app and insert their correct PIN and it doesn’t work. Meaning option 1 would be preferable in this instance.

if-eqz v0, :cond_2  -->  if-nez v0, :cond_2
Reversing the conditional check to return True when an incorrect PIN is entered. This would however also return False when the real PIN was input

Unlimited PIN Retries

Code

The PIN authentication mechanism also had another protection; this extra piece of security was a count of the incorrect PIN attempts. After 3 incorrect tries, the app would logout and force the user to re-authenticate fully with a valid username and password.

In the same class as the PIN check above, a return void method named checkPINRetryCount() was found. This is a very simple code block that checked if the wrong attempt variable, incremented in onPINSubmit() method, equals 3.

If we look at the smali code we can immediately see the logic flaw involved in this check. The retry counter limit is hardcoded to the constant decimal value 0x3, and then the comparison is done on line 281, if-ne v0, v1, :cond_0.

  • If v0 does not equal v1, then jump to cond_0.
  • :cond_0 will return-void.
Hardcoded incorrect try value and the comparison check

Patching

As with the previous example, there are multiple ways that this check can be bypassed due to the simplicity of the mechanism.

We have the following options:

  • Adding a new code statement to return void as soon as the method is invoked, before the actual comparison check is performed.
Bypassing of the check by returning void before the comparison is equated.
  • It is also possible to change the hardcoded limit value on line 279. Significantly increasing it in from 3 to 999 to allow considerably more attempts.

Note in this case we had to replace the const/4 with const/16 because we need more bits to represent an integer with the value of 999.

Modifying the retry value to allow 999 attempts instead of the original 3.
  • Replacing the comparison instruction with its opposite equivalent, if-eq, but also changing the registers used for the comparison in order to always return True.
    The modified code would now compare, v1== v1, or 0x3 == 0x3, and always be True to jump to cond_0, the return-void statement.
Reversing the conditional check comparison statement to always return True.

We’ve looked at a couple of examples of custom implementations of an extra security mechanism in the form of PIN entry. While the idea of these extra protections seems like a great idea from a user and development point of view, by not ensuring security is part of the development lifecycle it is possible to introduce very basic flaws in the flow of the code that can be leveraged by any attacker.

While these are only simple examples, they represent a very real problem to homebrew solutions to security. Any homebrew implementation should maintain a lot more security focus than the rest of the application which may use well embedded open source libraries, or vendor backed APIs.

The attack vector here is a very real threat, as we’ve seen the hardest part would not be bypassing the defences, but socially engineering the users to install the modified app and obtain physical access to gain the information from the device.

--

--

Random posts about mobile security and testing techniques from a bunch of mobile professionals.