Reverse Engineering Musical.y/Live.ly Android apps (Part 1)
Part 1 of an interesting journey.
There are many reasons people reverse engineer software, some want to find out more about how an app functions, others want to create modified clients of an app *cough* Snapchat update *cough* while some more nefarious individuals may wish to ‘crack’ a program in order to get free access to a paid product. For me, I had a very particular reason to do so. One of my friends recently got into ‘Live.ly’, a live streaming app where users can go live and interact with others/fans and is part of the Musical.y family, and he came across a problem pretty quickly which was that due to where he lives many of the people he enjoys watching streams of are in different time-zones and he would always miss them. Furthermore, only a very select collection of streams are ever saved so that they can be ‘replayed’. So that’s when I thought what everyone thinks in this situation: “Why not just reverse engineer the damn thing, get access to their private API and create an auto downloader app?” This is where our journey begins…
Setting Up
There are a few things we need first. The APK itself which, after a quick Google search, is easy to get a hold of. Secondly we will need a few tools:
- JD-GUI (for viewing decompiled Java code)
- JavaDecompilersOnline (for viewing code that JD-GUI fails to show due to errors, I would have used this totally instead but has a very annoying AdBlock bug which makes the experience very painful)
- Dex2Jar (For converting dex files in the APK archive into .jar files for viewing)
On the phone we will need:
- Live.ly app installed
- Packet Capture app installed
First Steps
The first thing I like to do when attempting to reverse engineer a private API is to capture the packets going to and from the application in question. To do this we will use the Packet Capture app mentioned above to do this on our device, no need to set up anything on your PC to do this. Just install the app, and then click the 3 dots in the top right and go to the settings page. Once there, tap the first option under the “Certificate” header and tap “Generate”. Once done, the second option under the “Certificate” header should read “Not installed. Tap to install to trusted credentials”, so just do as it says. Once done we are now ready to capture packets. On the top bar tap the play button with the number 1 next to it, this allows us to capture traffic from just 1 app. Search for “Live.ly” and then select it from the list of apps shown, this will start the packet capture. Now it is time to open the Live.ly app itself and perform some basic operations so that we have enough packets to analyze. After this, head back to the packet capture app and stop the capture. Now tap on the item that has a date and time along with the number of captures made, it should look like below after you have done so:
As you can see from above, just by looking at the URL’s, we can determine which packets we are more interested in. The Facebook URL’s we can basically just ignore as they are just part of the initialization of the Facebook SDK, the next 3 packets above those are just requests to app logging services. The first interesting packet we come across is further up the list which is actually made to Live.ly’s servers, if you’re following along then you can just search for a similar packet in your capture. After tapping the packet from the list we can see all the details about the request and all responses in plaintext as shown below:
First off we can see the URL or ‘Endpoint’ they are using by looking at the first line of the request header, the ‘Host’ header and the fact that we know it was an SSL request. Using this information we can clearly see that the app sent a GET request to the URL “https://live.direct.ly/rest/discover/navigate/nonlogin?timestamp=1520746145851&” and the response was an enormous JSON object with what looks like information about more endpoints. Interesting, this immediately says to me that they’re probably not storing a lot of plaintext endpoints in the APK itself and they’re instead getting them with this request and probably caching the response to use later. What else is interesting? Let’s take a closer look at the headers, there are many headers that we could easily replicate/emulate right now like CPU architecture, app, os, country and so on… BUT, what about the headers that aren’t so obvious, for example what the heck is the ‘X-Request-Info5’ header all about, actually what about all the ‘X-Request-’ headers?
After looking at various other requests like the register or login requests, the headers seen in the photo above are more or less the same, except all the ‘X-Request-’ headers have differing values from one request to the next. Well that at least tells us that these headers are request specific. Hmm, that ‘X-Request-Info5’ header value does look a lot like it’s something that’s been Base64 encoded. Lets take the value and decrypt it to see what we can find:
Wow, I guess it’s just a JSON representation of this specific request, that’s why it has a differing value for each request. Now lets take a look at the ‘X-Request-ID’ header, this one is pretty simple. As many of you can see, this is just random UUID. OK, what about the ‘slider-show-session’ and ‘X-Request-Sign5’ header values? I have no idea. For sanity, lets try sending the exact same request without these headers, aaaaaand there’s an error... Time to get down and dirty with the APK and see how we can generate these values for each request ourselves.
Decompiling
Lets start by extracting the APK file, on Linux I can extract this with the built in archive manager very easily but if you’re on Windows, however, you may need to rename the APK to a ‘.zip’ file before trying to extract/open it. After you have the contents of the archive extracted we can have a dig about. The first thing of note is the code itself. It has been compiled into ‘.dex’ files, we will use ‘Dex2Jar’ to decompile them into sort of readable ‘.jar’ files for inspection. To do this we execute the command (on Linux):
./d2j-dex2jar.sh /home/{username}/Downloads/Live.ly/classes.dex
Now we also do the same for ‘classes2.dex’ and ‘classes3.dex’. After this, we open JD-GUI and open the 3 ‘.jar’ files we just generated in the last step. Now we are going to use the handy search tool in JD-GUI to search through all of the code contained in theses files for specific strings to get us started. Open the search tool (Ctrl-Shift-S) and search for “X-Request-Sign5” and make sure your window looks something like the image below with all relevant checkboxes checked.
As you can see we have 1 matching entry in the code from our search, yay! Double click the ‘egt.class’ in the result box and the code will be displayed. The code we get is displayed below.
Although obfuscated, we can still glean a whole lot of useful information from this code. Firstly, my assertion about the ‘X-Request-ID’ header value being a UUID is confirmed with it’s value being set to str1 which in turn is initialized with UUID.randomUUID().toString(). As we can see the X-Request-Sign5 value is localObject2, this object is assigned to the value of dil.a().b((Context)localObject2, str2). The old localObject2 value was initialized to eem.a() which is just a Context object. The other variable, str2, is the Base64 encoded JSON representation of the request, which as we know is the value of the X-Request-Info5 header. OK, so what does dil.a().b(…) do then? Click on the ‘b’ part of the method call and JD-GUI should automatically go there. Aaaaand, damn it! This is where JD-GUI hits an INTERNAL ERROR. But as we can see from the tab at the top, this method is in the dil class.
This means we need to upload the classes2-dex.jar to the JavaDecompilers website. After doing so we need to find the dil.java class. This is what is in the class:
The method names are kind of weird but basically just read the method names by the last character of the name (e.g. read m11443a as just method a). The first method to be called is the a method without any parameters, this just returns a dil instance, nothing interesting there. OK, now we need to find the method b that takes a Context and a String respectively as its parameters. After a little looking we can see that its the final method in this class (m11445b). The below line is the first interesting thing to note.
if (this.f8347e == null) { m11436a(context); }
If the ISafeTokenComponent field is null then the a method that takes a Context as its parameter is called, this looks like an initializer method to me. From line 66 we can see it is opening an asset file called securityGuardInfo and after creating an Object representation of the file it is assigning two variables on lines 92 and 93. If these variables are not empty then something very interesting happens. A method called saveToken is called with the return value of method m11439b as its second parameter. This method takes the value of f8343a, concatenates an ampersand onto the end and then concatenates the return value of dim.a(Context).
To find the value of the field f8343a that was assigned we can look in the assets folder of the APK and look at the file securityGuardInfo and look at the first value, below is the contents of that file.
As we can see the first value is called the secondaryKey. OK, great but what does dim.a(Context) give us? Well, lets take a look in JD-GUI. Here is the dim class.
As we can see, all it returns is an MD5 encoded, undashed UUID with 2 extra bytes appended on the end. It also prepends the String “a0” to the return value. Sound familiar? Yes, this is the method that generates our slider-show-session value as well as our deviceId.
Brilliant, now we know the value passed to the saveToken method! It’s simply:
"016bb7aee66891f9b6ce10f03874eb3889f24546aa" + "&" + deviceId
And I’m betting that the signWithToken method uses this saved token as the HMAC-SHA1 secret key. Lets test this out! We’ll use the request we looked at earlier and using our formula we’ll see if we’re right.
Looking back at our m11445b method in the dil.java class we can see that it prepends “01a6” to the result of the hash. So the final value we have computed for the X-Request-Sign5 is:
01a6cc9523905a60f067a1876e9d93c1dde49fd36c96
And by looking at the request we saw earlier, we’ve done it! Now we just need to write code to emulate all the steps correctly and also intercept all the requests that we want to utilise and we’re off to the races! I have actually written some code that makes successful requests to the Live.ly servers but I have not yet finished it and all of that will be coming in part 2!
I hope you enjoyed my first story and if you enjoyed feel free to comment and give me a clap :)