Wednesday, February 18, 2015 At 8:10AM
You’re on yet another thrilling Android assessment, tearing so many holes in this newfound application that you fear the development team may opt to redesign the entire thing instead of fixing what you’ve so gleefully destroyed, when all of a sudden you come across the following encryption key in your hooking output:
Encryption Key:
EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
With your head now tilted at a rather astute 22 degree angle off-center, excitement and jubilation has turned into curiosity and wonder. Why does this encryption key have so many repeating ‘EFBFBD’ string patterns? Does your Substrate debugging code contain printing errors? Did the developers do something wrong while generating the key? Are you even sure that what you are printing out is the encryption key used to protect application data?
Analyzing the Android Application Code
As you might have guessed, the above scenario occurred on a real assessment some time back. Although we didn’t have the source code for the Android application, we were able to decompile the APK. Through a combination of runtime analysis using custom Substrate hooks and sifting through the decompiled obfuscated code, we confirmed that our debugging code was indeed correct and the output that we saw was indeed the encryption key. The next logical step was to inspect the application’s key generation process. It was here that we found our culprit.
For simplicity, instead of viewing decompiled obfuscated code, we have written a sample application that mirrors the behavior above. Take a moment and see if you can pinpoint what might be the root cause of the ‘EFBFBD’ string pattern in the encryption key.
public static String genKey(){ PBEKeySpec localPBEKeySpec = new PBEKeySpec( getPassword().toCharArray(), getSalt(), 20000, 256); try { byte[] theKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") .generateSecret(localPBEKeySpec).getEncoded(); return new String(theKey); } catch (InvalidKeySpecException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; }
Did you spot the issue? If not, don’t worry about it, we’ll walk through it now.
At a high-level, this method derives an encryption key based on the user’s application password. This encryption key is used to protect all information assets stored by the application on the Android device. First, the application creates a ‘PBEKeySpec’ object based on the user’s password, a salt value, an iteration count (20,000), and the desired length of the derived key (256 bits). An encryption key is then generated using the PBKDF2 key derivation function and stored into a byte array. So far, so good…at least until the next statement. What looks to be an innocuous casting of the byte array encryption key to a String, turns out to be the operation that is ultimately responsible for the repeating ‘EFBFBD’ pattern in the encryption key.
Let’s insert some debug statements in the sample application to see what the encryption key was prior to the String conversion and following the String conversion. Since the encryption key will contain non-printable characters, we will convert the key to hex using our own ‘bytesToHex’ method before printing it out.
public static String genKey(){ PBEKeySpec localPBEKeySpec = new PBEKeySpec( getPassword().toCharArray(), getSalt(), 20000, 256); try { byte[] theKey = SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”) .generateSecret(localPBEKeySpec).getEncoded(); Log.i(_TAG, “Key (Before Casting): ” + bytesToHex( theKey ) ); return new String(theKey); } ..snip.. } public static void test() { String theKey = genKey(); Log.i(_TAG, “Key (After Casting): ” + bytesToHex(theKey.getBytes())); }
After running our sample application, below is the resulting output. The encryption key prior to casting represents the key stored in the byte array. This is the correct, expected value. Notice, that the encryption key after the String casting takes place contains the repeating ‘EFBFBD’ pattern mentioned earlier.
Key (Before Casting):
B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
Key (After Casting):
EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
Byte Array to String Casting – Android
At this point we have established that casting our byte array to a String yields a very odd looking pattern of ‘EFBFBD’ strings in the resulting encryption key. So, that raises the question, what does EFBFBD mean?
According to the Android documentation: “the platform’s default charset is UTF-8. (This is in contrast to some older implementations where the default charset depended on the user’s locale.)”. In UTF-8, the hex string ‘EFBFBD’ represents a replacement character. In short, a replacement character is used to replace an incoming character whose value is unknown or un-representable. That means every ‘EFBFBD’ sequence maps to a byte (or two hex positions) from the original key (before casting) that could not be represented after the String casting took place. To illustrate this concept, let’s do a find and replace on the encryption key (after casting); replacing every ‘EFBFBD’ sequence with ‘**’ and line it up under the original encryption key.
B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3 !!******603466**7B**6C24E2B2AA576A******0C6B********76**********
We can now easily see the bytes that could not be represented following the String casting. Note, the ‘!!’ was manually inserted to line up the two outputs. It appears that the first byte of the original encryption key was not included by Android in the String casted encryption key.
Byte Array to String Casting – Different OS
After observing the abnormal String casting behavior on Android, we were curious to see what the outcome would be if we ran a similarly constructed Java application on a different operating system. Enter Windows.
The sample application is given below. Notice, we’ve added an additional debugging statement this time around to identify the default charset being used. According to the Java docs, “the default charset is determined during virtual-machine startup and typically depends upon the locale and charset of the underlying operating system.”
public static String genKey(){ PBEKeySpec localPBEKeySpec = new PBEKeySpec( getPassword().toCharArray(), getSalt(), 20000, 256); try { byte[] theKey = SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”) .generateSecret(localPBEKeySpec).getEncoded(); System.out.println(“Key (Before Casting): ” + bytesToHex( theKey ) ); return new String(theKey); } ..snip.. } return null; } public static void main(String[] args) { System.out.println(“Default Charset: ” + Charset.defaultCharset().name()); String theKey = genKey(); System.out.println(“Key (After Casting): ” + bytesToHex(theKey.getBytes())); }
After running the above Java application from Eclipse on a Windows 8.1 (x64) operating system, we received the following output:
Default Charset:
windows-1252
Key (Before Casting):
B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
Key (After Casting):
B7B0F83F603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
While the encryption keys before and after the String casting takes place look to be identical, they do deviate slightly. Notice that the fourth byte of the original encryption key is ‘8D’, while the fourth byte of the String casted encryption key is ‘3F’. What happened here?
In this particular case, the default charset of the underlying operating system is ‘Windows-1252 (or CP-1252)’. According to the MSDN technical documentation, positions 81, 8D, 8F, 90, and 9D are undefined and reserved by Windows. As a result, after the String casting takes place, the ‘8D’ character in the original encryption key is converted to ‘3F’, which represents the ‘?’ character.
Byte Array to String Casting – Summary View
Below is a summary of the same derived encryption key for different platforms after they have been casted from a byte array to a String. Note, as expected, the byte array representation of the encryption key (before casting) is the same for all platforms.
Key (Byte Array – Before Casting):
B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
Android 4.3 Key (After Casting):
EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
Windows 8.1 (After Casting):
B7B0F83F603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
Ubuntu 12.04 (After Casting):
EFBFBDEFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
Mac OS X 10.10 (After Casting):
EFBFBDEFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD
Wrap-Up
Although we took the long way to get to this point, we did so on purpose…after all, seeing is believing. From what we’ve seen, in the context of crypto operations, casting byte arrays to Strings is fairly unpredictable and in almost all cases, undesirable. From the perspective of an encryption key, a chain of ‘EFBFBD’ (or ‘3F’ for that matter) can lead to a significant loss of entropy. From the perspective of encrypted data, a chain of replacement characters likely means that you altered the contents of the data when you casted the byte array to a String. This was probably not your intention.
In short, avoid casting byte arrays to Strings if at all possible. Most crypto methods/functions accept byte arrays as arguments, dismissing the need to perform String casting. If you must handle the String equivalent of a byte array when dealing with crypto operations, consider converting the contents of the byte array to a Base64 encoded String or to its hex equivalent.
Hopefully, this blog post demonstrates the importance of performing validation on crypto routines at runtime. These subtle security flaws are difficult to identify when solely reviewing decompiled/disassembled code and can be easily overlooked. By performing runtime analysis through method hooking or debugging, flaws in crypto routines can be much easier to identify, and in the end, whether you’re a developer or a pen tester, could mean the difference between spotting that repeating ‘EFBFBD’ string pattern or missing it entirely.
References
http://developer.android.com/reference/java/nio/charset/Charset.html
http://www.fileformat.info/info/unicode/char/0fffd/index.htm
http://docs.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html#defaultCharset()
http://en.wikipedia.org/wiki/Windows-1252
http://msdn.microsoft.com/en-us/goglobal/cc305145.aspx
Author: Stephen Komal
©Aon plc 2023