Solving the Traced Android CTF with Frida
hacking reverse engineering ctf android fridaReversing is fun, but why work harder than we have to 😎? In this post, we learn how to re-sign an Android application bundle to run in our emulator, attempt a static analysis of an API key generation function, and use the Frida dynamic instrumentation toolkit to solve a simple Android CTF.
Introduction
A group called traced recently released a miniature Android CTF, involving a vulnerable Android application designed to teach the basics of Android reverse engineering. This was advertised as a beginner CTF, and like many beginner CTF, it's entirely solvable through static analysis. While looking at it, I ran into some interesting issues which I think are pretty typical for challenges you might face while reverse engineering real-world Android applications.
As I said before: static analysis, beginner CTF. While it's entirely possible to read through the source code of this app, I personally think it's a lot more fun to use dynamic techniques. What's even better is bottling those techniques into a script that you can run from your terminal— and so in this post I'm going to walk through my process of doing exactly that using the popular Frida toolkit.
Requirements
You'll need to add the following items to your shopping cart if you want to follow along with this post:
- The target APK, which you can get here.
- An Android emulator. I'm using the free version of Genymotion. You can also download emulators through Android Studio.
- Android SDK tools, which come bundled with the above.
- apktool, a tool for working with APK bundles.
- jadx, a nice DEX to Java decompiler.
- Frida server binary, and frida command line tools. Frida is an extremely powerful binary instrumentation framework which we'll use to write our solver.
Getting Started
The first thing I tried was getting the app running in my emulator:
adb install app-release.apk
Performing Push Install
app-release.apk: 1 file pushed. 4.5 MB/s (1334574 bytes in 0.282s)
pkg: /data/local/tmp/app-release.apk
Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]
Android requires codesigning for apps that run on the platform. This error message indicates that there's a problem with the codesigning on this app. This is a relatively common scenario when you're working with an APK that didn't come from the app store (such as one provided to an application tester by a developer for testing), and it basically means we'll need to resign the app ourselves. Luckily, this process is free and easy using the apktool
, keytool
, and jarsigner
utilities.
- Use
apktool
to extract and decode the contents of the bundle:
java -jar ./tools/apktool/apktool_2.4.0.jar d app-release.apk
- Use
apktool
to build a new bundle, which will be the one we sign:
java -jar ~/tools/apktool/apktool_2.4.0.jar b app-release
Now you should have a new built apk at ./app-release/dist/app-release.apk
. This is the package that we'll be signing.
- Generate our signing key:
keytool -genkey -v -keystore codemuch.keystore -storepass C0d3much4traced -alias android -keypass C0d3much4traced -keyalg RSA -keysize 2048 -validity 10000
- Sign the bundle…
jarsigner -verbose -keystore codemuch.keystore -storepass C0d3much4traced -keypass C0d3much4traced ./app-release/dist/app-release.apk android
You'll get a warning that it's self signed, but that doesn't matter on your emulator.
- So, with our application signed, let's try installing it again.
adb install ./app-release/dist/app-release.apk
Performing Push Install
./app-release/dist/app-release.apk: 1 file pushed. 3.4 MB/s (1404756 bytes in 0.399s)
pkg: /data/local/tmp/app-release.apk
Success
NOTE: Often times, you may run into an instance where you want to make some sort of modification on a target APK that you're working on. In those cases, you'll need to follow this process above to re-sign your app and deploy the modified copy on your emulator. So it's good to undrstand why!
At this point, I ran the application. There's a username and password prompt, and the application locks you out after a certain number of failed attempts.
Static Analysis
The first thing I usually do is take a look at the MainActivity
class. So I opened up the application using jadx
, and tried to get a sense of what was happening when the application starts.
The real interesting thing here seems to be the APIKeyCalc
within the APIKey
class. It looks like this function will just print out a variety of keys depending on the credentials you enter.
If you follow the logic of this function and the referenced functions, you can work out the key from here. But why go through all of this trouble? What if we could just write a short script to launch the application, execute getAPIKey6
right when we launch the application, and dump the key to our console?
Well, we're going to do exactly that with Frida.
Frida Setup
- Install Frida locally, and check that it works.
virtualenv --python=$(command -v python3) env; source ./env/bin/activate
pip install frida frida-tools
frida --version
- Download the latest x86 Frida server release:
curl -LO https://github.com/frida/frida/releases/download/12.8.20/frida-server-12.8.20-android-x86.xz
xz --decompress frida-server-12.8.20-android-x86.xz
- Move the binary to your emulator:
adb push frida-server-12.8.20-android.x86 /tmp/
- In a new terminal window, open adb shell and start the server.
adb shell
cd /tmp/
./frida-server-12.8.20-android.x86
- Test frida from your console:
frida-ps -U
NOTE: If the server is running, you should see a list of running processes on your Android device.
Writing our Solution
Now that Frida is installed, we have the ability to perform dynamic instrumentation. We
can hook functions, and all kinds of other intersting stuff. What we're going to attempt to do, is launch our application and immediately run the APIKey.getAPIKey6
method. We can do that with this simple Python loader, and a Frida script written in Javascript. Here's the Python loader, which will get a handle to the Frida server running in our emulator, launch the target application package, and execute a Frida script:
The real logic goes into the JavaScript file. Since getAPIKey6
is an instance method, we first need to create an instance of the APIKey
class. This requires
three parameters: two Java Strings and a Context object associated with the current running application. Once we've got an instance of APIKey
, we can execute our target method, and output its return value to the console. All of this is pretty easy using the Frida Java bridge, which essentially provides an API for the Java runtime which we can access through our Frida scripts.
When we run it, this is what we get:
$ python loader.py
[+] APIKey Dumper loaded!
[+] Inside perform...
[+] Found our target class: <class: app.traced.tracedctf.APIKey>
[+] Let's try to run a method...
[+] APIKey instance created: app.traced.tracedctf.APIKey@ad8edce
[+] Here's your API key: jB&KmQ$9p6b53MGJnmnOU
If we enter our key of jB&KmQ$9p6b53MGJnmnOU
into the validation form, we are congratulated for our hard work :-)
If you can read Java and you spend some time looking at the source code for this challenge, you'll probably realize that you can work through the solution just by following the logic. That is to say, purely through static analysis. So why would we want to go through the trouble of writing code rather than reverse engineering the solution from source?
Well, in this case, I did it for fun, and as a learning exercise. But imagine a scenario where the APIKeyCalc
function were significantly more complicated. Maybe it's highly obfuscated, or maybe it implements some sort of custom transformation on a piece of information we're trying to access.
We've (potentially) saved ourselves a TON of time, by simply following this general process:
- Identify a sensitive function in a target application. I would say that for a lot of these types of challenges, a “sensitive function” is one that can reach a cleartext representation of some piece of information we want to access.
- Use dynamic instrumentation to subvert the normal program flow of the application and access that function directly, exposing the information we're after.
NOTE: In this case, we ran our target method and grabbed the return value, but it would've been equally possible to extract this value if it was a function parameter, or a piece of data sitting somewhere in memory.
Conclusion
And that's all! In short, static analysis can sometimes be the way to go, but the ability to dynamically “instrument” an application while it's running is extremely powerful tool for your reverse engineering arsenal. Frida provides the means to reach into an application during runtime and manipulate its classes and methods at will, which depending on your goal, can help you save significant time and effort compared to other approaches.