Reverse-Engineering Credential Encryption, Encoding in an Android App

I’ve been a long-time user of a popular home automation software package. The product exposes its functionality in three ways:

  1. a web application running on an embedded web server,
  2. a mobile app for Android and iOS, and
  3. server-side plugins and an accompanying SDK.

Encouraged by the lack of competition, the product is quite expensive and has one of the ugliest, clunkiest UIs I’ve ever had to deal with. The web application gets the job done, but the mobile app is a trainwreck: it has a splash screen, a “loading configuration” dialog that takes over a second to go away, and a “loading devices” dialog that takes several seconds—or worse—depending on how many devices one has. And the device list is not cached, so one has to sit through this process every time the app loads. It’s often faster and less frustrating to stand up and physically turn on the lights.

Eventually, I decided that I’d had enough and it was time to write an aftermarket Android app for this thing. I had three options:

  1. Best: Write a server-side plugin using the supplied SDK for .NET, design my own communications protocol, and write an Android app that would talk to the server via the plugin. Pros: Clean and officially supported. Cons: Having to learn .NET, design a protocol, and code both the client mobile app and the server plugin.
  2. Worst: Write just an Android app that would talk to the server by scraping its web application. Pro: No entry barrier. Cons: Highly prone to breaking with updates; super ghetto.
  3. Decent: Reverse-engineer the protocol used by the official mobile app and implement it in my own. Pro: Relatively clean, client-only implementation. Cons: Slightly prone to breaking with updates to the protocol; having to reverse-engineer the protocol.

I settled for option #3, which seemed like a good compromise. A quick Google search revealed that the vendor wasn’t keen on giving out details on the protocol, which wasn’t surprising. I also found no third-party libraries or reverse engineering attempts by others, so I was on my own.

Reverse-Engineering the Protocol

I fired up Microsoft Message Analyzer on the server, set up a filter by TCP port number and payload size, launched the vendor’s Android app on my phone, and watched it get to work. Good news! The protocol was text-based:

> CONNECT|1|CONNECT|Android:Nexus.4:2:0.0:7
< 1|ok
> COMMAND|2|VERSIONS|
< 2|1.0.60|2.5.0.80
> COMMAND|3|AUTHENC|fedSfMLlhLKiERA0mkXXmw==::Y7EKllfXujjnmrkbIsDpgw==

It looked like I had some encoding to figure out before I went any further. Looking through the compiled APK revealed the existence of an AUTH command (vs. the above AUTHENC) that accepted clear-text credentials. I was tempted to use it and move on to the rest of the protocol, but I figured it would be unwise to spend any more time on it only to ultimately realize I may not be able to break the encoding.

Reverse-Engineering the Credential Encoding

fedSfMLlhLKiERA0mkXXmw==::Y7EKllfXujjnmrkbIsDpgw==

These looked like Base64-encoded credentials, the trailing == being the giveaway, separated by double colons. Changing only one of the credentials produced the same encoding for the other, meaning the username and password were encoded individually.

Base64-decoding the 22-character strings yielded unreadable 16-byte blobs, which implied that the credentials had been hashed or encrypted prior to being encoded. It was time to dust off apktool and decompile the APK to find out what I was dealing with.

Tearing Down the APK

Skimming the smali source code produced references to these Crypto and Base64 utility classes. This confirmed that Base64-decoding the credentials was the right thing to do, and that the resulting blobs were AES-encrypted. I’d write about how the vendor should be hashing rather than encrypting credentials, but there’s no point.

AES encryption takes three inputs:

  1. the data to be encrypted,
  2. a secret key or passphrase, and
  3. an initialization vector.

The passphrase and initialization vector chosen by the vendor would be embedded in the APK, and hopefully easy enough to find among the smali sources.

Extracting the Initialization Vector

The original Java source code for the Crypto class shows that the passphrase is passed as an argument to the constructor, whereas the initialization vector—a 16-byte array of hex values—is kept in the rawSecretKey static class variable. The former would be found in the vendor’s calling code, and the latter in the Crypto class itself.

I decided to start with the Crypto class’ constructor:

const/4 v3, 0x7

This stores the value 0x7 in variable v3. It will be needed later.

const/16 v0, 0x10
new-array v0, v0, [B

This declares the 16-byte (0x10 in hexadecimal) array where the initialization vector will be stored.

The following code fills the array with the values I was out to “steal,” one at a time. The aput-byte opcode takes three parameters: the value to be inserted, the array into which to insert it, and the position in the array (index) at which to insert the value.

const/4 v1, 0x0
const/4 v2, 0x1
aput-byte v2, v0, v1

The above inserts value 0x1 at index 0x0.

const/4 v1, 0x3
const/16 v2, 0x76
aput-byte v2, v0, v1

The above inserts value 0x76 at index 0x3.

const/4 v1, 0x4
const/16 v2, 0x9
aput-byte v2, v0, v1

The above inserts value 0x9 at index 0x4.

const/4 v1, 0x5
const/16 v2, 0x52
aput-byte v2, v0, v1

The above inserts value 0x52 at index 0x5.

const/4 v1, 0x6
const/16 v2, 0x20
aput-byte v2, v0, v1

The above inserts value 0x20 at index 0x6.

const/16 v1, 0x56
aput-byte v1, v0, v3

The above inserts value 0x56 at index 0x7. If you recall, 0x7 was sneakily stored in v3 at the top of the constructor—before this array was even declared—for obfuscation.

const/16 v1, 0x8
const/16 v2, 0x45
aput-byte v2, v0, v1

The above inserts value 0x45 at index 0x8.

const/16 v1, 0xa
const/16 v2, 0x74
aput-byte v2, v0, v1

The above inserts value 0x74 at index 0xa (10 in decimal).

const/16 v1, 0xb
const/16 v2, 0x42
aput-byte v2, v0, v1

The above inserts value 0x42 at index 0xb (11 in decimal).

const/16 v1, 0xc
aput-byte v3, v0, v1

The above inserts value 0x7 at index 0xc. The same 0x7 that was stored in v3 at the beginning of the constructor, and was used earlier as an index, is now used as a value—also for obfuscation.

const/16 v1, 0xd
const/16 v2, 0x62
aput-byte v2, v0, v1

The above inserts value 0x62 at index 0xd (13 in decimal).

const/16 v1, 0xe
const/16 v2, 0x12
aput-byte v2, v0, v1

The above inserts value 0x12 at index 0xe (14 in decimal).

const/16 v1, 0xf
const/16 v2, 0x64
aput-byte v2, v0, v1

The above inserts value 0x64 at index 0xf (15 in decimal).

sput-object v0, Lutils/crypto;->rawSecretKey:[B

Finally, the above stores the filled array in the rawSecretKey class variable.

I retraced the steps and built a visual representation of the array. Note that indices 0x1, 0x2 and 0x9 were skipped above, leaving zero values in those slots. (Arrays are zeroed upon initialization.)

  0   1   2    3   4    5    6    7    8   9   10   11  12   13   14   15
-------------------------------------------------------------------------
0x1 0x0 0x0 0x76 0x9 0x52 0x20 0x56 0x45 0x0 0x74 0x42 0x7 0x62 0x12 0x64

And there it is: the initialization vector.

Extracting the Passphrase

The passphrase is passed as an argument to the constructor of the Crypto class. I grepped through the smali sources to find where the class was instantiated, and found the following in one the vendor’s classes:

const-string v1, "a"
const-string v2, "bc123"

The above declares two string constants with values “a” and “bc123”.

new-instance v6, Ljava/lang/StringBuilder;
invoke-static {v1}, Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v7
invoke-direct {v6, v7}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V

The above creates a StringBuilder and initializes it with the result of feeding the value of v1, “a”, to String.valueOf(). This redundant call, made solely for obfuscation purposes, returns the same “a”. The end result is a StringBuilder containing the string “a”.

invoke-virtual {v6, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v6

The above appends the value of v2, “bc123”, to the contents of the StringBuilder. This yields “abc123″—the passphrase.

invoke-virtual {v6}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v6
invoke-static {v6}, Lutils/crypto;->crypto(Ljava/lang/String;)V

Finally, the above passes the passphrase to the constructor of the Crypto class.

Once the Crypto object has been instantiated and initialized with the passphrase, the vendor uses it to encrypt and encode the user’s credentials so they can be transmitted securely to the server:

.local v6, user_enc:Ljava/lang/String;
.local v2, pass_enc:Ljava/lang/String;

The above declares two string variables, v6 (user_enc) and v2 (pass_enc), which will be used to store the encoded credentials.

const-string v9, "utf-8"

The above stores in variable v9 the string “utf-8”, which is the character encoding to which the user’s credentials will be converted prior to encrypting and encoding.

invoke-virtual {v7, v9}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B
move-result-object v9

The above converts the clear-text username (previously stored in v7 in code not shown here) to UTF-8 and stores the result in v9, in byte array form.

invoke-static {v9}, Lutils/crypto;->encryptAsBase64([B)Ljava/lang/String;
move-result-object v6

The above passes the clear-text username byte array to the Crypto object’s encryptAsBase64() method, which AES-encrypts the username and then encodes it in Base64. The encrypted, encoded username is finally stored in v6 (user_enc).

invoke-virtual {v3, v9}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B
move-result-object v9

The above converts the clear-text password (previously stored in v3 in code not shown here) to UTF-8 and stores the result in v9, in byte array form.

invoke-static {v9}, Lutils/crypto;->encryptAsBase64([B)Ljava/lang/String;
move-result-object v2

The above passes the clear-text password byte array to the Crypto object’s encryptAsBase64() method, which AES-encrypts the password and then encodes it in Base64. The encrypted, encoded password is finally stored in v2 (pass_enc).

In summary, the vendor’s app:

  1. converts the user’s credentials to UTF-8,
  2. encrypts them with AES using a static initialization vector and passphrase, and
  3. encodes the encrypted blobs in Base64.

Reproducing the Credential Encoding

Once I had the AES initialization vector and passphrase, I wrote a proof of concept that I would later implement in my Android app to encode the credentials in the same manner as the vendor’s app.

Setting the Initialization Vector in the Crypto Class

private static byte[] rawSecretKey = {
    0x01, 0x00, 0x00, 0x76, 0x09, 0x52, 0x20, 0x56,
    0x45, 0x00, 0x74, 0x42, 0x07, 0x62, 0x12, 0x64
};

Proof of Concept

import com.neocodenetworks.smsfwd.Crypto;

public class CredentialEncoder {
    final static String passphrase = "abc123";

    final static String username   = "jdoe";
    final static String password   = "foobar";

    public static void main(String[] args) throws Exception {
        final Crypto crypto = new Crypto(passphrase);

        final String encodedUsername = crypto.encryptAsBase64(username.getBytes("utf-8"));
        final String encodedPassword = crypto.encryptAsBase64(password.getBytes("utf-8"));

        System.out.println(encodedUsername + "::" + encodedPassword);
    }
}

Running the above console application produces the following output:

fedSfMLlhLKiERA0mkXXmw==::Y7EKllfXujjnmrkbIsDpgw==

The encoded credentials, “fedSfMLlhLKiERA0mkXXmw==” being the username and “Y7EKllfXujjnmrkbIsDpgw==” being the password,  match those produced by the vendor’s official mobile app.

Leave a Reply

Your email address will not be published. Required fields are marked *