Basti’s Buggy Blog

Poking at software.

Reverse Engineering OruxMaps' Preference Encryption - Part 2

In part 1 we covered how to identify an encrypted file and how to look for an entry point for reverse engineering. In this part we will look at the Android app itself and try to develop a tool for encrypting and decrypting files.

Table of Contents

Understanding the Smali Source

Let’s take a quick look at the files we found be grepping for "preferences".

  • uTa.smali: 44 lines of code basically returning an informational message
  • xga.smali: Contains various strings of directory paths
  • tza.smali
    • 7000+ lines of code
    • Uses a lot of SharedPreferences objects → stores settings?
    • A lot of profile-related strings like ant_hrmid (ANT+ Heart Rate Monitor ID)

The last file looks the most interesting, maybe it has some more information. Upon further inspection we can find the code which forms the profile name:

# bool v3 = true;
const/4 v3, 0x1

.line 15
:cond_2
# StringBuilder sb = new StringBuilder();
new-instance v9, Ljava/lang/StringBuilder;
invoke-direct {v9}, Ljava/lang/StringBuilder;->()V

invoke-virtual {v9, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

# sb.append("om2_");
const-string v10, "om2_"
invoke-virtual {v9, v10}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

# sb.append(profileName);
invoke-virtual {v9, v8}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

# sb.append(".xml");
const-string v8, ".xml"
invoke-virtual {v9, v8}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

# v8 = filename = sb.toString() = "om2_" + profileName + ".xml";
invoke-virtual {v9}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v8

# v9 = weirdString = "TagKet1234!·445?¿$%";
const-string v9, "TagKet1234!\u00b7445?\u00bf$%"

# Fya.a(someFile, filename, weirdString, true)
invoke-static {v7, v8, v9, v3}, LFya;->a(Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V
:try_end_0
.catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_0

Which roughly translates into Java like this:

StringBuilder sb = new StringBuilder();
String filename = sb.append("om2_").append(profileName).append(".xml").toString();

Fya.a(someFile, filename, "TagKet1234!·445?¿$%", true);

The string "TagKet1234!·445?¿$%" looks very suspicious (and weird, because it contains unicode characters)! Did we find our password already? Possibly, but we don’t know the encryption algorithm and parameters yet, so let’s keep digging into the static Fya.a function.

Now that we have proven our worth by reading smali source, we are allowed to use higher level decompiler. Bytecode Viewer is a Android decompiler/IDE, which generates .java files by default. Simply open any apk and get started with reversing!

import Sma.a;

public static void a(File var0, String var1, String var2, boolean var3)
  throws IOException {
    // get some cipher
    Cipher var7;
    if (var3) {
	var7 = (new a(var2)).b();
    } else {
	var7 = (new a(var2)).a();
    }

    // read or write an encrypted file?
    Object var5;
    Object var6;
    if (var3) {
	var5 = new FileInputStream(var0);
	var6 = new CipherOutputStream(new FileOutputStream(var1), var7);
    } else {
	var5 = new CipherInputStream(new FileInputStream(var0), var7);
	var6 = new FileOutputStream(var1);
    }

    byte[] var8 = new byte[4024];

    // do the actual writing
    while(true) {
	int var4 = ((InputStream)var5).read(var8);
	if (var4 <= 0) {
	    ((OutputStream)var6).close();
	    ((InputStream)var5).close();
	    return;
	}

	((OutputStream)var6).write(var8, 0, var4);
    }
}

Ok this seems to be straightforward: Based on the boolean argument we create some Cipher class and use it either in a CipherOutputStream or CipherInputStream. The most interesting part for us is the creation of the cipher:

import Sma.a;

Cipher var7;
if (var3) {
   var7 = (new a(var2)).b();
} else {
   var7 = (new a(var2)).a();
}

Following the Constructor call of Sma.a, we find a very cryptoy class. Jackpot!

public final byte[] b = new byte[]{-87, -101, -56, 50, 86, 53, -29, 3};

public Sma$a(String var1) {
   try {
      // PBEKeySpec​(char[] password, byte[] salt, int iterationCount)
      PBEKeySpec var2 = new PBEKeySpec(var1.toCharArray(), this.b, 19);
      SecretKey var5 = SecretKeyFactory.getInstance("PBEWithMD5AndDES")
        .generateSecret(var2);
      this.c = Cipher.getInstance(var5.getAlgorithm());
      this.d = Cipher.getInstance(var5.getAlgorithm());
      PBEParameterSpec var4 = new PBEParameterSpec(this.b, 19);
      this.c.init(1, var5, var4);
      this.d.init(2, var5, var4);
   } catch (Exception var3) {
   }
}

By looking up the documentation of PBEKeySpec, we learn that PBE stands for password based encryption. The documentation also has us covered with the names of the parameters: PBEKeySpec​(char[] password, byte[] salt, int iterationCount). That makes confirm our assumption: "TagKet1234!·445?¿$%" is the password! We also learn more about the algorithm and its parameters:

  • PBEWithMD5AndDES: Password based encryption with MD5 and DES (those two algorithms truly are nightmare fuel)
  • The salt is stored in this.b
  • The iteration count is 19

Re-Implementing the Encryption in Java

Now we are basically done with the reverse engineering, we only need to re-implement the encryption in Java to finally be able to read the contents of our preferences profile xml. But the reverse-engineering gods didn’t want to make it too easy for me. Once I finished re-implementing the relevant code, I decided to give it a shot:

orux javac OruxDecryptor.java
orux java OruxDecryptor
Exception in thread "main" java.security.spec.InvalidKeySpecException: Password is not ASCII
	at java.base/com.sun.crypto.provider.PBEKey.<init>(PBEKey.java:70)
	at java.base/com.sun.crypto.provider.PBEKeyFactory.engineGenerateSecret(PBEKeyFactory.java:219)
	at java.base/javax.crypto.SecretKeyFactory.generateSecret(SecretKeyFactory.java:340)
	at Decryptor.setupCipher(Decryptor.java:86)
	at Decryptor.decryptEncrypt(Decryptor.java:53)
	at Decryptor.main(Decryptor.java:48)

Yes, the password is not ASCII, but I already know that. The Android app seemed to work fine! Let’s see what we do in the setupCipher function:

public static void setupCipher(String password) throws Exception {
  PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, 19);
  // ...
}

That’s exactly how it is implemented in the Android app, too. After checking the smali source, I was 90% sure I didn’t make a mistake.

Replacing the · and the ¿ symbol in the password with any other ASCII symbol prevented the exception, but the decryption obviously didn’t work then.

I investigated the issue and tried many different possibilities:

  • Brute-Forcing: Swapping out the two symbols with non-ASCII characters
  • Setting the Java locale to Spanish (as the dev is Spanish): java.util.Locale.setDefault(new java.util.Locale("es", "ES"));
  • Applying an ASCII mask to the symbols
    • 0xb3 & 0x7f //· (\u00b7)
    • 0xab & 0x7f //¿ (\u00bf)
  • Using Spanish codepages (containing ·¿) to translate unicode values into byte values

All attempts to get a working password failed.

Double Checking the OpenJDK Source Code

Maybe a look into the actual crypto implementation of OpenJDK will help me understand what I’m doing wrong here. And indeed, the PBEKey constructor does check if every character of the string is inside the printable ASCII range.

if (!(passwd.length == 1 && passwd[0] == 0)) {
    for (int i=0; i<passwd.length; i++) {
        if ((passwd[i] < '\u0020') || (passwd[i] > '\u007E')) {
            throw new InvalidKeySpecException("Password is not ASCII");
        }
    }
}

Then I remembered that Android has a custom implementation of the JVM coming with a number of custom classes. As Android is open source (at least the non-Google part), I wanted to cross-check the crypto source code. I did not find the PBEKey class, but a similar BCPBEKey class. It seems to be part of the Bouncy Castle cryptographic library.

Java’s Crypto Providers

Looking at the exception trace from earlier, we can see that the upper part of the stack is actually implemented in the package com.sun.crypto.

Exception in thread "main" java.security.spec.InvalidKeySpecException: Password is not ASCII
	at java.base/com.sun.crypto.provider.PBEKey.<init>(PBEKey.java:70)
	at java.base/com.sun.crypto.provider.PBEKeyFactory.engineGenerateSecret(PBEKeyFactory.java:219)
	at java.base/javax.crypto.SecretKeyFactory.generateSecret(SecretKeyFactory.java:340)
	at Decryptor.setupCipher(Decryptor.java:86)
	at Decryptor.decryptEncrypt(Decryptor.java:53)
	at Decryptor.main(Decryptor.java:48)

Doing some “research”, I was able to confirm that Android uses the Bouncy Castle library in some cases. So we somehow have to replace the Sun crypto with Bouncy Castle crypto!

First let’s look at which crypto providers are currently used by Java:

for(Provider s : Security.getProviders()) {
  System.out.println(s.getName());
}
SUN
SunRsaSign
SunEC
SunJSSE
SunJCE
SunJGSS
SunSASL
XMLDSig
SunPCSC
JdkLDAP
JdkSASL
SunPKCS11

Let’s get Bouncy Castle up and running!

Using Bouncy Castle as a Provider

After downloading1 the appropriate Bouncy Castle release (bcprov-jdk14-166.jar) and placing it in my working directory, I was able to add it as a crypto provider with the highest priority (1):

Security.insertProviderAt(new BouncyCastleProvider(), 1);

In order for Java to find the .jar, we have to adjust the classpath parameter to include the jar and our current directory:

orux tree -L 1
.
├── bcprov-jdk14-166.jar
├── om2_Basti.xml
├── OruxDecryptor.class
├── OruxDecryptor.java
├── OruxMaps7.4.23
└── OruxMaps7.4.23.apk
orux javac -cp ".:bcprov-jdk14-166.jar" OruxDecryptor.java
orux java -cp ".:bcprov-jdk14-166.jar" OruxDecryptor decrypt om2_Basti.xml out.xml

And it worked! You can download the finished script here.

Finally I can look at my settings, change them and encrypt them again:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="gpsies_user"></string>
    <string name="app_autobk">7</string>
    <boolean name="dash_humedad" value="false" />
    <boolean name="app_alw_compass" value="false" />
    <boolean name="polygon_labels" value="true" />

Conclusion

  • The app made saving and changing settings harder than it needs to be
  • Java implementations can be very different

  1. https://www.bouncycastle.org/latest_releases.html ↩︎

See Also