The purpose of this post, is to remind testers to always perform static analysis on a mobile application they are testing. I often hear people mention the components that they look at, such as the web/API portion, or locally stored data, but let's not forget about analyzing a disassembled app. Reverse Engineering is daunting for sure, especially if it's not something you've focused on. Luckily, Java disassembles to nearly source, making Android apps pretty easy to analyze.
Java disassemblers have come a long way. The process used to look something like this. Use Apktool to disassemble apk(Android package), use Dex2Jar to convert classes.dex file to jar file, load jar file in a java disassembler to view pseudo code.
Now, java disassembler Jadx-GUI makes this way easier and produces better output. You can feed it the apk or classes.dex file and it will handle the rest.
While this gives you almost source that you can review, you cannot edit the code or perform any binary patching. You will still need to edit the Smali files if you need to alter the bytecode. ApkStudio can provide you with a nice GUI for viewing and editing the smali. It will also let you rebuild and re-sign the app.
What inspired me to talk about this is a test I was on where the developers had obfuscated the application during compilation. So when I viewed the disassembled app, it was pretty hard to follow methods and classes as I had no idea what they were. I had almost thrown in the towel, when I found a file containing hardcoded public and private keys. Obviously this is bad, but to know the true severity, I needed to know how and what exactly the keys were being used for.
In this example I will show you how I traced the methods to figure out what these hardcoded keys were actually used for, and why it was worth the time to figure it out. I will also show you how you can use the disassembled Java to aid you in your reversing efforts. For the sake of keeping this app anonymous, I have blurred out any sensitive data.
Jadx-GUI has a "Deobfuscation" feature in its tools that will replace the methods and classes with an alpha-numeric scheme that will make it easier for you to track methods, classes, and what they do. This specific app has lots of methods that call other public static methods, that return values from other methods. It's a pain to trace, but it keeps the developers from re-using methods everywhere. I will do my best to walk you through my process of what the hardcoded keys are used for, so bear with me.
Let's start with the methods that contain the hardcoded keys. The top method (m13318a) contains what appears to be a Public RSA key. We can also see that a JSON Object is being created, and based on the error message in the catch, it looks like this is doing some encrypting. Safe to say this is preparing to make a POST request. The method at the bottom (m13319b), seems to be the handling the incoming response from the server, as we can see the JSON Object being pulled apart into two String values, and the catch error message mentioning "decryptCheck".
At first I was only interested in the private key, and the decrypting of the JSON values, but after more poking around, I realized, that I needed to look at what was being encrypted and put into the 4 JSON key/value pairs going out to the server first. The Public key is sent as a string to method "C2598b.m13313a", so let's take a look at what that method does.
In the method above, the string "str" is the hardcoded Public key, str2 has to do with the message digest, but we aren't really concerned with that. Variable f9192e is the value returned by "m13308f", which is a method generating a usable Key from the Public key string.
Then variable f9190c is a value returned from "m13317e", which is the generated AES key as a byte array. Let's take a look real quick at how they're generating this AES key. Now I'm really curious what the RSA keys are for.
Without getting too in depth, we can see that the AES key is 32 bytes, 17 bytes are random bytes, and the last 15 are the current date/time in UTC, with "Z" appended to the end.
Moving down to the try catch block, we can see they're creating a new Cipher instance, using the AES key byte array we just looked at as the SecretKeySpec, then grabbing the IV from the instance. Which is most likely just random bytes from the AES key. Then, it seems that the method encrypts and base64 encodes the public key from variable f9189b and stores it in variable f9193f. This all just returns "true" if there are no errors. UGH!!!
Okay, now let's go back to building the JSON Object. We need to find out what's in these values. String a is returned from "c2598b.m13310a", which returns the value from method "m13307b". The value being passed in is the AES key byte array that was previously created. Then the byte array is encrypted with the public key that was created previously, base64 encoded, and sent back. So now we know that the hardcoded Public RSA key is ultimately used to encrypt the AES key, and that encrypted/encoded AES key is then plopped into param "a" of the JSON Object that is about to get sent out in a POST request. WOW!!!
So things just got worse right?! Let's take a look at param "b".
So "c2598b.m13314b" is getting called which also calls that encrypt/encode method "m13307b", but this time it's passing in the IV that we created earlier. So now we have the encrypted/encoded IV as param "b", going out in the JSON as well. HOLD UP!!! If we know that the key and IV are being sent in the POST request, and that the Public RSA key is used to encrypt the key and IV, then the response data must also contain something similar since they're using current date/time as a key. So let's skip the rest of the POST and move down to the decryption stuff.
The String return in this method is what we're interested in. The hardcoded Private RSA key is just the first string param that is being sent to the method "m13312a". The image below shows that the second two params are coming from methods "c2598b.m13310a", and "c2598b.m13314b", and the last two params are the two strings from the JSON Object that was parsed.
Following the first method takes us to "c2598b.m13310a" which calls the encrypt/encode method and returns the encrypted/encoded AES key that's still stored in memory. Then, "c2598b.m13314b" returns the encrypted/encoded IV.
So now we can trace what is actually decrypting user data sent back from the server. Follow "c2598b.m13312a", which takes us here.
Still with me?? We're almost done. So we passed in 5 params and we just gained the Int "1" as our second param of 6 that is getting sent to method "m13311a". Here we instantiate a Key variable "a", then check if the int we passed in is equal to 1, if it is then it calls "m13304a" and passes the first string which is the hardcoded Private RSA key. This method does that same thing the other method did for the Public key. It strips off the beginning and end, and turns it into a usable key. Move down a little further where we set the values for variables f9190c and f9191d by calling method "m13306a" and passing in the AES key, IV, and the Private RSA keys. Here we can see that this just decodes and decrypts the key and IV.
Now for the part we've been waiting for. We create a new Cipher instance using the decrypted AES key, with an IVParameterSpec of the decrypted IV, then we finally base64 decode and decrypt the user data from "str4" or JSON Object value "a" from the reponse body.
What a mess. See how obfuscation can be a deterrent? But, with a lot of staring and going in circles, I was finally able to make sense out of it. Now, remember when I mentioned using the code in Jadx-GUI to aid in your reversing efforts? Here is a Proof-of-Concept I put together to demonstrate impact. I wanted to show the data being decrypted instead of just providing a theory. So I copied out some methods and altered them to work by passing in the key, IV, and encrypted user data from a JSON response. Along with the hardcoded Private RSA key, I was able to decrypt user data. What's also bad, is that because the AES key and IV are sent in each request, you can decrypt the response no matter how old the intercepted transmissions are. In this case, there was a lot of PII in the user data, making this a pretty bad situation. Below are screenshots of my Android Studio PoC and the printed, decrypted results.
Welp, if you're still reading, then hats off to you. Even if you don't fully understand what I just quickly walked through, I hope that you can take something from this post. That message being, always look through the code and try to look for poor key entropy, hardcoded keys, and things similar. It can take a mediocre test and turn it into a great one. This also provides the most value to your client. Party On!!