This is the first part of the Bit Twiddling series. For your convenience you can find other parts using the links below:
Part 1 — Modifying Android application on a binary level
Part 2 — Reverse engineering Profesor Klaus Intensywny Kurs to fix missing microphone — Profesor Klaus Intensywny Kurs bez mikrofonu
Part 3 — Enabling call recording in Google Phone dialer
Part 4 — Disabling CTRL+ALT+HOME in mstsc.exe (Windows RDP client)
Part 5 — Fixing audio latency in mstsc.exe (RDP)
Part 6 — Stop RDP from detaching GUI when the client disconnects
Recently I had to modify an existing Android application for which I didn’t have a source code, only APK file. This may sound like an impossible task but actually it is pretty easy (very tedious, though). I didn’t want to use any compilers or install Android SDK with emulators as I know how heavy those tools are. I did have an Android phone so I could do all tests using physical device but I didn’t have a debugger.
First, APK file is just an ordinary zip file (as usually in Java world). We can unzip it and see that all binary code is stored in file called classes.dex
which contains binary code for Dalvik runtime.
First thing we want to do is disassemble this file to smali format which is an assembly language for Dalvik platform. We can use Apktool which can do exactly that. What’s more, it can assemble APK file back so now we can modify smali files and have our application ready to go.
Now the question is how to figure out where to put our code. I was lucky as application I was modifying was written in Java/Kotlin so I could decompile it to one of those and read the code a bit easier. To do that I used jadx. Keep in mind that sometimes it is impossible to decompile the code completely which also happened in my case but most of the application was decompiled successfully.
Unfortunately, the code was obfuscated using ProGuard. This made things a little more difficult but generally what you want to do when analyzing code like this is look for strings. They cannot be obfuscated (well, they can be encrypted so it is not entirely true, but it wasn’t the case with my application) and generally appear in raw form so you can search whole codebase and look for them.
Next, you need to modify the code. You may want to look for exactly one place when you inject some static function, call it just once nad have everything done. Since you need to implement function in smali format, java2smali IntelliJ IDEA plugin can be super useful. You just need to write the code in Java, compile it to smali and put in the codebase.
Finally, APK generated by Apktool is not signed. To sign it we can use Zipsigner application which we install in our phone and use there. If you use some later API (29+ I think) then you need to go with signing scheme V2 (or newer). Zipsigner supports V1 only so you can use apksigner from Google or uber apk signer.
Now you just need to transfer modified APK to your phone, sign it and install it. Done.
Some “best practices” you may follow:
- Look for raw strings, but be careful with UI labels as they are pretty often stored in resources.
- Try to find as small integration point as possible.
- Get some way of debugging. Generally things like
System.out.println
don’t work but the concept is very useful. You can modify existing labels to have some form of an output. Without that it is way harder to do anything. - Catch all exceptions and throwables.
- If you don’t have a debugger then imitate stepping through the code with conditions. Just have something like
if(input.length() > 0) { line1; if(input.length() > 1){line 2;}}
etc. By passing input of different length you can emulate stepping through. - You want to make sure that your code you want to inject into target application is correct. Test it beforehand in any console application, make sure that you use exactly the same API and libraries. It is very easy to make a silly mistake like typo in package name which crashes application with potentially no logs.
- Remember that you can’t catch everything. There are native exceptions which your Java handlers won’t capture.
- Be very careful with logging to files. If you don’t have permissions then your code will crash.
Debugging
Without debugger it is much harder to see what is actually going on. Fortunately, it is quite easy to debug applications on binary level. It is called smalidea – it is a plugin which allows you to step through smali files. It is quite well explained on its Github page so I won’t cover it here.