In the previous article we set up our analysis environment and now are ready to reverse engineer our first application.
While searching for a suitable target for this article, I came across these challenges from OWASP. The ones in the link above become progressively more difficult so they will make good practice.
They are good to practice on but for this article I wanted to keep it simple and avoid native code and obfuscations. Only L1 serves this purpose.
For this article we will need the following:
- Android UnCrackable L1 - the application we will reverse engineer
- JADX - a decompiler for Android applications
- Apktool - a tool we can use to extract and modify resources and code from an app
- Frida - we can use Frida to modify a binary at runtime
APK
An APK file is a format used for Android applications. It is essentially a ZIP archive that contains a specific structure of files and directories, as described in the APK article on Wikipedia.
This archive contains code and resources required by the application.
Unzipping the file is not enough to have something usable, as the resources are encoded and we only have DEX bytecode. This is where Apktool comes in.
Apktool
Apktool converts the .dex file(s) to smali and decodes the resources.
Most Android applications are primarily written in Java or Kotlin, but some can contain native code or be built using frameworks such as React Native.
Android applications run on the Android RunTime (ART) instead of the Java Virtual Machine (JVM). In earlier versions, they run on the Dalvik virtual machine.
DEX is the bytecode format used by ART and Dalvik, and we can convert it to smali
, which would be analogous to Assembly language for native code.
Apktool will convert the .dex
file or files to smali
, and decode the resources.
Note that the resources are not encrypted, simply encoded. The documentation for resources can be found on Google’s Android documentation.
|
|
To install the application, drag and drop the APK file to the Android emulator.
The goal is to find the key or somehow bypass the check.
We will start with simply bypassing the check.
Let’s open AndroidManifest.xml.
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="owasp.mstg.uncrackable1">
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme">
<activity android:label="@string/app_name" android:name="sg.vantagepoint.uncrackable1.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
We can see that application begins at sg.vantagepoint.uncrackable1.MainActivity
.
We will now open UnCrackable-Level1\smali\sg\vantagepoint\uncrackable1\MainActivity.smali
.
Looking at the methods, we see one called verify
.
.method public verify(Landroid/view/View;)V
.locals 3
const p1, 0x7f020001
invoke-virtual {p0, p1}, Lsg/vantagepoint/uncrackable1/MainActivity;->findViewById(I)Landroid/view/View;
move-result-object p1
check-cast p1, Landroid/widget/EditText;
invoke-virtual {p1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object p1
invoke-virtual {p1}, Ljava/lang/Object;->toString()Ljava/lang/String;
move-result-object p1
new-instance v0, Landroid/app/AlertDialog$Builder;
invoke-direct {v0, p0}, Landroid/app/AlertDialog$Builder;-><init>(Landroid/content/Context;)V
invoke-virtual {v0}, Landroid/app/AlertDialog$Builder;->create()Landroid/app/AlertDialog;
move-result-object v0
invoke-static {p1}, Lsg/vantagepoint/uncrackable1/a;->a(Ljava/lang/String;)Z
move-result p1
if-eqz p1, :cond_0
const-string p1, "Success!"
invoke-virtual {v0, p1}, Landroid/app/AlertDialog;->setTitle(Ljava/lang/CharSequence;)V
const-string p1, "This is the correct secret."
:goto_0
invoke-virtual {v0, p1}, Landroid/app/AlertDialog;->setMessage(Ljava/lang/CharSequence;)V
goto :goto_1
:cond_0
const-string p1, "Nope..."
invoke-virtual {v0, p1}, Landroid/app/AlertDialog;->setTitle(Ljava/lang/CharSequence;)V
const-string p1, "That\'s not it. Try again."
goto :goto_0
:goto_1
const/4 p1, -0x3
const-string v1, "OK"
new-instance v2, Lsg/vantagepoint/uncrackable1/MainActivity$2;
invoke-direct {v2, p0}, Lsg/vantagepoint/uncrackable1/MainActivity$2;-><init>(Lsg/vantagepoint/uncrackable1/MainActivity;)V
invoke-virtual {v0, p1, v1, v2}, Landroid/app/AlertDialog;->setButton(ILjava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)V
invoke-virtual {v0}, Landroid/app/AlertDialog;->show()V
return-void
.end method
The listing above may not make a lot of sense, but we see a few strings that will hint at what we want to change, even if you don’t yet know smali.
if-eqz p1, :cond_0
const-string p1, "Success!"
...
:cond_0
const-string p1, "Nope..."
Looking at the Android documentation, if-eqz
works like this:
Branch to the given destination if the given register's value compares with 0 as specified.
We can simply remove that line, rebuild and sign the APK.
|
|
Signing is required, and the easiest way to sign the APK is with uber-apk-signer.
|
|
You will need to uninstall the old version first, as the signature won’t match the previous one. When you run the app, any input will work.
JADX
The former solution works, but reading smali
will become a chore soon enough. Let’s open the APK on JADX and see if it’s any easier.
|
|
|
|
That is way easier. We are comparing our input against the result of decrypting a string using AES. Let’s rename some variables to make this easier to understand.
If you are confused about b
, remember that 2 hex characters in the string represent a single byte.
|
|
Now we can write our own decryption code:
|
|
We now have the key, and don’t need to modify the program to pass the check.
Frida
There is another way to solve this challenge, by modifying the binary at runtime.
The instructions for setting up Frida are available on their documentation. If you followed along with the last video you will want to download frida-server-*-android-x86_64.xz
. We need a server running on the Android device so that we can interact with it and run scripts. There are other ways to use Frida on Android, but for this one we need a rooted device.
What we will do is write a script to modify the VerifyKey
method to always return true.
On JADX, right click the VerifyKey
function and select “Copy as frida snippet”.
|
|
Now install frida-tools
.
|
|
Now we modify one of the example scripts from the documentation:
|
|
|
|
The script itself is JS, but we can write a simple wrapper to run it using Python.
After running the script, any key will work since we have modified VerifyKey
to return true always.
Conclusion
We looked at several ways of solving the same challenge, and now you are familiar with the most common tools used for Android reverse engineering.
Attempting to solve the next challenges on the OWASP website will be a good way to practice 😃