Let’s dive into analyzing the OWASP Uncrackable Level 1
app!
This can be solved in two ways,
I’ll show you both ways. So, let’s buckle up for both ways to solve this challenge.
Using Frida
Upon opening the app, it closes due to root detection, as shown below:
To understand why, we can decompile the APK using jadx. In the AndroidManifest.xml
, the Launcher activity is defined as owasp.mstg.uncrackable1
.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
package="owasp.mstg.uncrackable1">
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="28"/>
<application
android:theme="@style/AppTheme"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true">
<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>
In MainActivity
, we find the code responsible for detecting the root and closing the app using System.exit(0);
.
private void a(String str) {
AlertDialog create = new AlertDialog.Builder(this).create();
create.setTitle(str);
create.setMessage("This is unacceptable. The app is now going to exit.");
create.setButton(-3, "OK", new DialogInterface.OnClickListener() { // from class: sg.vantagepoint.uncrackable1.MainActivity.1
@Override // android.content.DialogInterface.OnClickListener
public void onClick(DialogInterface dialogInterface, int i) {
System.exit(0);
}
});
create.setCancelable(false);
create.show();
}
@Override // android.app.Activity
protected void onCreate(Bundle bundle) {
if (c.a() || c.b() || c.c()) {
a("Root detected!");
}
if (b.a(getApplicationContext())) {
a("App is debuggable!");
}
super.onCreate(bundle);
setContentView(R.layout.activity_main);
}
Let’s write a frida
script to hook and bypass this check.
Java.perform(function() {
var hook = Java.use("java.lang.System");
hook.exit.implementation = function() {
console.log("Root Check Bypassed!!! 😎");
};
});
Once bypassed, the app presents a text field and a verify button. Clickingverify
shows a message: That's not it. Try again.
By searching for this string in the code, we find the verification logic:
Let’s take this as a reference to move ahead and find this string in the code. After searching this out, you can see a code like below which seems like a comparison between input value and some hard-coded value.
We need to inspect the a
method to understand the comparison.
public class a {
public static boolean a(String str) {
byte[] bArr;
byte[] bArr2 = new byte[0];
try {
bArr = sg.vantagepoint.a.a.a(b("8d127684cbc37c17616d806cf50473cc"), Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0));
} catch (Exception e) {
Log.d("CodeCheck", "AES error:" + e.getMessage());
bArr = bArr2;
}
return str.equals(new String(bArr));
}
public static byte[] b(String str) {
int length = str.length();
byte[] bArr = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
bArr[i / 2] = (byte) ((Character.digit(str.charAt(i), 16) << 4) + Character.digit(str.charAt(i + 1), 16));
}
return bArr;
}
}
The bArr
value is what our input is being compared to. The sg.vantagepoint.a.a.a
method is an AES decryption method.
public class a {
public static byte[] a(byte[] bArr, byte[] bArr2) {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES/ECB/PKCS7Padding");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(2, secretKeySpec);
return cipher.doFinal(bArr2);
}
}
We can hook this function to get the string value
var a = Java.use('sg.vantagepoint.a.a');
a.a.implementation = function (p0, p1) {
console.log('p0 (byte array): ' + bytesToString(p0));
console.log('p1 (byte array): ' + bytesToString(p1));
var result = this.a(p0, p1);
console.log("Result ->", bytesToString(result))
return result;
};
function bytesToString(bytes) {
var result = '';
for (var i = 0; i < bytes.length; ++i) {
result += String.fromCharCode(bytes[i]);
}
return result;
}
Execute the script to see the byte array as a string.
(base) C:\Users\booyaa\uncrackable\level> frida -U -l ./hook_level1.js -f owasp.mstg.uncrackable1
____
/ _ | Frida 16.2.3 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to LE2001 (id=4a298ca9)
Spawned `owasp.mstg.uncrackable1`. Resuming main thread!
[LE2101::owasp.mstg.uncrackable1 ]-> Hooked exit()
Root Check Bypassed!!! 😎
p0 (byte array): ヘvトᅨᅢ|amタlsᅩ
p1 (byte array): ¥Bbᅨ[レᅢᅠᄉ₩ᄂᄑvレI│t.ユᆱ|v
Result -> I want to believe
Process terminated
[LE2101::owasp.mstg.uncrackable1 ]->
Thank you for using Frida!
(base) C:\Users\booyaa\uncrackable\level>
And there we have it; the final string: I want to believe
.
Using just Python and no rooted device or Frida.
# level1.py
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def a(key, cipher_text):
cipher = AES.new(key, AES.MODE_ECB)
return unpad(cipher.decrypt(cipher_text), AES.block_size)
def b(hex_string):
return bytes.fromhex(hex_string)
try:
key = b("8d127684cbc37c17616d806cf50473cc")
cipher_text = base64.b64decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=")
returned_bytes = a(key, cipher_text)
print("Solved: ", returned_bytes.decode())
except Exception as e:
print("AES error: ", str(e))
Thanks for following along! Cheers 🍺