Some names have been changed or removed to protect the innocent. Disclaimer: this project‘s results do not allow me or anyone else to hack into bank accounts, or even replicate a client’s token without access to a rooted device with an active code generator. On the other hand, a malicious third party application with root privileges would have access to all the information required to generate codes, but would still need the account details (including a password) to fully compromise an account.
Also, some clarifications: This is not a security vulnerability or even criticism by any stretch. The bank‘s app is (arguably) more secure than Google Authenticator (which keeps secrets around in plaintext), and this article should be seen as praise for the bank’s app, which does things the right way by (mostly) adhering to the TOTP standard, and protects its data as well as technically possible.
My current bank, one of Brazil's largest, provides its clients with one of several methods (in addition to their passwords) to authenticate to their accounts, online and on ATMs.
New accounts are usually provided with a credit-card sized piece of paper with 70 single-use codes which are randomly requested, once per access. This requires the client to obtain a new set of codes whenever they run out of them, which is not very practical.
A better alternative is their Android app (also available in several other platforms). It provides a Google Authenticator-like code generator, except it is PIN-protected, and requires a phone call to activate but operates seamlessly after that. Or so I thought.
I found myself calling the bank every so often after changing ROMs, resetting or changing phones. The activation process is simple enough, but the hacker in me did not enjoy the ordeal. I attempted to use Titanium Backup to no avail, for reasons I would soon understand. There was just one thing left to do - reverse engineer the application and build my own. Maybe make my own physical token using an Arduino.
Before diving into the code, I had to go through the normal setup process once, which meant installing their app, calling the bank and activating it. Here are some screenshots of the process.
The first image shows the installation process. Look at all those permissions it requests! I'm sure they are all necessary for some reason, but none of them should be necessary to generate codes, right? The second image shows the actual activation process, inputting four numeric fields that have to be provided over the phone (notice I was in a call then). The third image shows the actual code generator, after a successful activation.
Reverse engineering Android apps requires a few software tools. Here's what I used for this project:
Android SDK
Provides the adb command-line tool, which can pull APKs, data files and settings from the phone.
dex2jar
Converts Android's Dalvik executables into JARs, which are easier to reverse engineer.
JD, JD-GUI
An excellent Java bytecode decompiler.
Eclipse
A Java IDE to validate discoveries during the reverse engineering process.
The first step in reverse engineering an application is obviously getting the application. It is possible to download APK files directly from Google Play, but I decided to get the file directly from my phone, using ADB.
Enable USB debugging in your cell phone, then run the following commands.
Find the package name
$ ./adb shell pm list packages | grep mybank
package:com.mybank
Find the package path
$ ./adb shell pm path com.mybank
package:/data/app/com.mybank-1.apk
Download the package
$ ./adb pull /data/app/com.mybank-1.apk
2950 KB/s (15613144 bytes in 5.168s)
You should now have a com.mybank-1.apk
file in the current directory.
APK files can be extracted using the unzip
utility, because they are ZIP files with a different extension (much like JAR files). Inside the archive, the actual code is in the classes.dex
file, which I renamed to com.mybank-1.dex
just to keep things organized.
Extract the package
$ unzip com.mybank-1.apk
(file list omitted for brevity)
Rename and convert classes.dex to a JAR file
$ mv classes.dex com.mybank-1.dex
$ ./d2j-dex2jar.sh com.mybank-1.dex
dex2jar com.mybank-1.dex -> com.mybank-1-dex2jar.jar
You should now have a com.mybank-1-dex2jar.jar
file in the current directory, which can be opened by JD.
After dragging the JAR file into JD-GUI, you should be greeted by a window similar to the following.
Here's where the fun begins. While there are some obvious packages containing parts of the token module, such as br.com.mybank.integrador.token
, br.com.othercompany.token
and com.mybank.varejo.token
, it doesn't take too long to realize that the core functionality is implemented in a few of the default package classes, which are obfuscated. Bummer.
Here's a snippet from br.com.othercompany.token.GerenciadorConfig
:
public void trocaPINcomLogin(int paramInt, boolean paramBoolean, Perfil paramPerfil)
{
if (paramPerfil == null)
throw new IllegalArgumentException(a.a("1p5/eEf/sl3kbeUcP509qg=="));
if (!this.jdField_a_of_type_U.jdField_a_of_type_JavaUtilHashtable.contains(paramPerfil))
throw new RuntimeException(a.a("86jcmKgr/ZshQu9aGVbuGscy2nHW4UEWqudRoUXhImQ=") + a.a("7u8KqqwqUD3a7FM339fp6pRrxUtQrHDMyqvZ6A2MurQ="));
if ((this.jdField_a_of_type_BrComOtherCompanyTokenParamsGerenciador.isPinObrigatorio()) && (!paramBoolean))
throw new RuntimeException(a.a("aMsL/5kjkXKD4K1SvpTuuJZUS0U0fL19UT2GxjJ/QzQ="));
Configuracao localConfiguracao = paramPerfil.getConfiguracao();
if ((localConfiguracao.a().a()) && (paramPerfil != this.jdField_a_of_type_BrComOtherCompanyTokenPerfil))
throw new RuntimeException(a.a("ASszutKFJW3iqDb7X/+vqAcYxTLXN2SJOIs0ne596Pu3ZoRxjiiscwhV6fT70efX"));
localConfiguracao.a().a(paramInt);
localConfiguracao.a().a(paramBoolean);
this.jdField_a_of_type_U.a(paramPerfil);
if (!paramPerfil.equals(this.jdField_a_of_type_BrComOtherCompanyTokenPerfil))
a(paramPerfil);
}
Every exception thrown by this piece of code is obfuscated, as well as many of the strings used throughout the code. That is a major roadblock, since exception messages and strings in general are a great way of figuring out what the code is doing when reverse engineering something.
Luckily, their developers decided to actually show useful text when a problem occurs and an exception gets thrown, so they wrapped those obfuscated strings with a.a
, presumably a decryption routine that returns the original text. That routine is not too straightforward, but it is possible to get a high level understanding of what it is doing. Here are some findings after analyzing the a
class and its dependencies:
- Class
p
is a base64 decoder.
- Class
b
is an AES implementation. Searching for its internal strings and constants on Google revealed that it is part of Paulo Barreto's JAES, a public domain crypto library.
private static byte[] a
in class a
is an obfuscated key, which can be deobfuscated by this short C program, basically replicating a snippet of the original a.a
method.
#include<stdio.h>
#include<stdlib.h>
int main(int argc, const char * argv[]) {
char keyin[] = {<data from decompiled method>};
char keyout[16];
int i = 0;
for (i = 0; i < 16; i++)
keyout[i] = keyin[i] ^ keyin[31-i];
for (i=0; i < 16; i++)
printf("%01x", (unsigned char)keyout[i]);
printf("\n");
return 0;
} This code yields the AES encryption key.
Unfortunately, a.a
is not just a wrapper for JAES‘s AES class. It also does some crypto of its own. Here’s some pseudopython for a.a
:
def decodeExceptionString(str):
aesKey = <data from previous step>
xorKey = <data from decompiled method>
blockSize = 16
aes = AES(aesKey)
stringBytes = Base64.decode(str)
outputString = ""
for blockStart in xrange(0, len(stringBytes), blockSize):
encryptedBlock = stringBytes[blockStart:blockStart+blockSize]
plaintextBlock = aes.decrypt(encryptedBlock)
outputString += plaintextBlock ^ xorKey
xorKey = encryptedBlock
return outputString
In a nutshell, besides AES with an obfuscated key, this class appears to implement CBC (cipher block chaining) which was not present in the original JAES library.
A simple test to make sure it works:
$ ./decode "ASszutKFJW3iqDb7X/+vqAcYxTLXN2SJOIs0ne596Pu3ZoRxjiiscwhV6fT70efX"
Não é possível alterar PIN sem estar logado.
The message reads (in Portuguese) “it is not possible to change PIN without being logged in”. Success!
Deobfuscating the exception strings was a fun battle, but the war was not over yet. I had yet to figure out how to generate an authentication code myself. After looking around the code for a long while, I found a good entry point to the token generation process in the br.com.othercompany.token.dispositivo.OTP
class. Here's a snippet, with the exception strings deobfuscated.
public String calculate() throws TokenException {
int i = (int)Math.max(Math.min((this.a.getConfiguracao().getAjusteTemporal() + Calendar.getInstance().getTime().getTime() - 1175385600000L) / 36000L, 2147483647L), -2147483648L);
a();
if (i < 0)
throw new TokenException("Janela negativa"), i);
int j = (0x3 & this.a.getConfiguracao().getAlgoritmos().a) >> 0;
switch (j)
{
default:
throw new TokenException("Algoritmo inválido:" + j, i);
case 0:
return a(i);
case 1:
}
return o.a(this.a.getConfiguracao().getChave().a(20), i);
}
This method basically generates a timestamp i
which is the number of 36-second intervals since April 1st, 2007 at midnight (expressed as the Unix timestamp 1175385600000L
). Why 36 seconds? That's how long each token lasts. Why April 1st, 2007 at midnight? No idea.
It also includes a correction factor (getAjusteTemporal()
, which means temporal adjustment in Portuguese). I assume this is calculated at activation time as the difference between the server‘s and the device’s clocks. In this snippet, o.a
is the core token generating function, and its parameters are a byte array (a key) and the current timestamp.
Finding the key
The key is obtained by calling this.a.getConfiguracao().getChave().a(20)
in the previous snippet. this.a
is a Perfil
(profile) object; getConfiguracao()
returns a Configuracao
(settings) object; getChave()
returns a z
object; a(int)
returns a byte array, which is the key itself.
The z
class is obfuscated but fortunately quite simple. It is just a wrapper around a byte array up to 32 bytes in length, and its a(int)
method truncates that array to the provided length. The Perfil
object, in turn, gets created by the PersistenciaDB
(persistence database) class, which contains a bunch of obfuscated strings:
a = a.a("DwYyIlrWxIS9ruNMCKH/PQ==");
b = a.a("SceoTjidi0XqlgRUo9hcDw==");
c = a.a("yrYBlcp8nEfVKUT9WSqTqA==");
d = a.a("jUTzBfsP/AO/Kx/1+VQ3CQ==");
e = a.a("Y56SnU/pIKROPCLHu7oFuw==") + b + a.a("38oyp4eW3xqT3TaMfWZ5RA==") + "_id" + a.a("3Q+FCEVH2PxZ31ms4WHHwNB40EbmtWzHPhwoaB1nM7lGr+9zZzuVpx5iZ4YR+KUw") + c + a.a("bYYIl6LtqthcUCCFFb7JCRSC8zr5hKIFXe5JHFCCkZA=") + d + a.a("ENCtPBu4RtFta2XI1GsQag==") + a.a("ImPhDy43f+Nr4G5ofkZz+g==");
Finally! Investigating the a.a
method pays off. Here are the deobfuscated strings.
a = "token.db";
b = "perfis";
c = "nome";
d = "data";
e = "create table perfis (_id integer primary key autoincrement, nome text not null, data blob not null);";
A SQL statement, interesting. So that‘s how it stores its profiles. And there’s a filename too, token.db
, probably a SQLite database. Further investigation of the carregar
(load) method in PersistenciaDB
class shows that, indeed, it is a SQLite database, accessed through the SQLiteDatabase
class.
One might think it would only be a matter of getting the key from the database, then. Not so fast. The data blob is encrypted as well, as evidenced further in the carregar
method by the use of the aa.a
method (so much for descriptive names - blame the obfuscation). That method accepts as parameters the data blob, an empty buffer, and a parameter that gets passed through the carregar
method - a key - truncated to 16 characters.
Before investigating the crypto behind aa.a
, I decided to find the key to decrypt the blob. It gets passed as a parameter to the carregar
method. After digging around for a bit, I found the class that generates the key: PersistenciaUtils
. Here it is, in its entirety:
public class PersistenciaUtils {
public static byte[] getChave(Context paramContext, byte[] paramArrayOfByte) {
try {
byte[] arrayOfByte = MessageDigest.getInstance("SHA-1").digest(getId(paramContext).getBytes());
return arrayOfByte;
} catch (NoSuchAlgorithmException localNoSuchAlgorithmException) {
}
return new byte[20];
}
public static String getId(Context paramContext) {
String str = Settings.System.getString(paramContext.getContentResolver(), "android_id");
if (str == null)
str = "<their default id>";
return str;
}
}
In other words, the SHA-1 digest of the device's android_id
(a unique identifier), or a default value if that doesn't work. Notice that it hashes the hex string, not the actual bytes. So that's why Titanium Backup did not work when I tried it - I was not backing up this identifier, even though there was an option for that in Titanium Backup. It‘s too late to go back now though, let’s keep reversing this app.
To find my android_id
, I used adb
once again.
$ ./adb shell
shell@hammerhead:/ $ content query --uri content://settings/secure --projection name:value --where "name='android_id'"
Row: 0 name=android_id, value=0123456789abcdef
shell@hammerhead:/ $ exit
aa.a
splits the data blob in several sequential fields: a 96-byte header, a 16-byte nonce, a 16-byte tag, and the rest of the blob as cryptotext. Further inspection of the aa
class reveals some more details about the obfuscated classes:
- Class
e
is an implementation of EAX, an AEAD (Authenticated Encryption with Associated Data) algorithm, from JAES.
- Class
f
is an implementation of CMAC (Cipher-based Message Authentication Code), also from JAES.
- Class
h
is an implementation of the CTR (counter) mode, from JAES as well.
- Class
l
is an unknown implementation of the SHA-1 hashing algorithm. Interestingly, it is not used by the PersistenciaUtil
class, which uses the MessageDigest
class instead.
- Class
m
is an unknown implementation of the HMAC (keyed-Hash Message Authentication Code) algorithm.
- Class
n
is a wrapper around l
and m
, providing HMAC-SHA1.
The method aa.a
derives a second key by computing the CMAC tag of the header and uses it to decrypt the cryptotext. In pseudopython:
def decodeBlob(datablob, android_id):
header = datablob[:96]
nonce = datablob[96:112]
tag = datablob[112:128]
cryptotext = datablob[128:]
key1 = SHA1(android_id)[:16]
aes = AES(key1)
cmac = CMAC(aes)
cmac.update(header)
key2 = cmac.getTag()
eax = EAX(key2, aes)
(validTag, plaintext) = eax.checkAndDecrypt(cryptotext, tag)
if validTag:
return plaintext
If the EAX authentication succeeds, aa.a
returns the decrypted content to PersistenciaDB
, which then interprets the decrypted data.
Looking back at the PersistenciaDB
class, now at the a
method which parses the decrypted data into a Perfil
object, it consists basically of a deserialization of the decrypted data into several booleans, shorts, and byte arrays. It is possible to identify several of the fields, of which three stand out (their offsets were discovered by adding along the deserialization).
pin = int(blob[82:86])
key = blob[38:70]
timeOffset = long(blob[90:98])
And yes, this is finally the key I was looking for. My PIN matched, which was a welcome validation that my implementation was working correctly, and my time offset was small enough to ignore.
Understanding the code generation process
The key obtained at the previous step gets truncated to 20 characters in the OTP
class, which then passes it along with the timestamp to the o.a
method. That method references several of the obfuscated classes identified in the previous steps, which is a relief. Based on that, here's some pseudopython for that method.
def generateToken(key, timestamp):
message = [0] * 8
for i in xrange(7, 0, -1):
message[i] = timestamp & 0xFF
timestamp >>= 8
hmacSha1 = HMAC_SHA1(key)
hmacSha1.update(message)
hash = hmacSha1.getHash()
k = 0xF & hash[-1]
m = ((0x7F & hash[k]) << 24 | (0xFF & hash[(k + 1)]) << 16 | (0xFF & hash[(k + 2)]) << 8 | 0xFF & hash[(k + 3)]) % 1000000;
return "%06d" % m
Basically the timestamp (a long, 8 bytes in length) gets (manually) turned into a big-endian byte array. That array gets hashed using HMAC-SHA1 employing the key as key, generating a hash. The last four bits of the hash determine an index at which an integer is read. Take that integer, modulo 1000000, and that‘s our code. Simple, huh? Yeah, I didn’t think so either. But it works!
A while later, I found this snippet in Google Authenticator's implementation of TOTP:
public String generateResponseCode(byte[] challenge)
throws GeneralSecurityException {
byte[] hash = signer.sign(challenge);
int offset = hash[hash.length - 1] & 0xF;
int truncatedHash = hashToInt(hash, offset) & 0x7FFFFFFF;
int pinValue = truncatedHash % (int) Math.pow(10, codeLength);
return padOutput(pinValue);
}
Looks familiar? It's the exact same algorithm. In fact, only a couple of things prevented me from creating a Google Authenticator QR-Code from this data:
- The arbitrary timestamp epoch of April 1st, 2007 at midnight.
- The period, which is 30 seconds in Google Authenticator and 36 seconds in my bank's token. The key URI format used by Google Authenticator accepts a period parameter which could fix this, but the application currently ignores it.
Or, rather, a Texas Instruments Stellaris LaunchPad I had lying around. They are actually code-compatible when using the Energia IDE, and I even used some Arduino-specific libraries:
The RTC part needs improvement. Since the Stellaris LaunchPad does not have an onboard RTC, the internal clock needs to be set at each startup, which is cumbersome and requires a computer to get it going, and that‘s not very practical. For now, here’s the complete code:
#include <sha1.h>
#include <LCD.h>
#include <RTClib.h>
RTC_Millis RTC;
void setup() {
RTC.begin(DateTime(__DATE__, __TIME__));
LCD.init(PE_3, PE_2, PE_1, PD_3, PD_2, PD_1);
LCD.print("Token");
LCD.print("valverde.me", 2, 1);
delay(1000);
LCD.clear();
}
char token[6];
uint8_t message[8];
long timestamp = 0;
long i = 0;
uint8_t key[] = {<your key here>};
void showToken() {
long now = RTC.now().get() - 228700800 + 7200;
i = now / 36;
int timeLeft = now % 36;
for(int j = 7; j >= 0; j--) {
message[j] = ((byte)(i & 0xFF));
i >>= 8;
}
Sha1.initHmac(key, 20);
Sha1.writebytes(message, 8);
uint8_t * hash = Sha1.resultHmac();
int k = 0xF & hash[19];
int m = ((0x7F & hash[k]) << 24 | (0xFF & hash[(k + 1)]) << 16 | (0xFF & hash[(k + 2)]) << 8 | 0xFF & hash[(k + 3)]) % 1000000;
LCD.print(m, 2, 1);
LCD.print(36 - timeLeft, 2, 15);
}
void loop() {
LCD.clear();
LCD.print("Current token:");
showToken();
delay(1000);
}
An interesting hack occurs on this line:
RTC.begin(DateTime(__DATE__, __TIME__));
Instead of figuring out a way to set the clock at every startup, I used this hack in which the current time is filled in by the compiler just before the code gets uploaded to the board, resulting in a close-enough internal clock. RTClib generates timestamps with an epoch of Jan 1st, 2000 at midnight, so the code generator's epoch had to be converted to 228700800
. A correction factor of 7200
was also required because the compiler fills in the local time instead of UTC, so it is two hours behind for me.
It is important to mention that this project‘s results do not allow me or anyone else to hack into bank accounts, or even replicate a client’s token without access to a rooted device with an active code generator. On the other hand, a malicious third party application with root privileges would have access to all the information required to generate codes, but would still need the account details (including a password) to fully compromise an account.
I would like to thank Daniel Nascimento, Raphael Campos and Miguel Gaiowski for helping me review this article.
Finally, here's some proof that it works (a couple of seconds too fast) :)