ECW 2020 - Antirdroid (reverse)

img

ECW 2020 - Antirdroid (reverse)

The ECW 2020 challenge took place the two last weeks and had few reverse challenges, one PE and this android challenge. The challenge was divided in 3 steps for 3 x 150 points.

The qualification of this CTF is not the funny part but the final is quite cool with network discovery and so on. I decided to give a look in challenges which could interest me. In the end I finished 11th. So not so bad, I guess.

Initial foothold

So we were given an APK antirdroid.apk, you can easily unzip it as it’s only an archive of a lot of files. Once unzipped I usually browse for non common files and this way I have identified the following ones :

$ ls -Rl assets res/raw/
assets:
total 644
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-a.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-b.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-c.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-d.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-e.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-f.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-g.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-h.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-i.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-j.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-k.tflite
-rw-r--r-- 1 switch switch  41984 18 oct.  22:23 mnist-l.tflite
-rw-r--r-- 1 switch switch 115744 18 oct.  22:23 mnist.tflite

res/raw/:
total 56
-rw-r--r-- 1 switch switch 22608 18 oct.  22:23 step_1.dex
-rw-r--r-- 1 switch switch 11648 18 oct.  22:23 step_2.dex
-rw-r--r-- 1 switch switch 16624 18 oct.  22:23 step_3.dex

These files are linked to the challenge, and the .dex is the extension of the Dex executable format, the bytecode executed by the Dalvik VM in the Android OS

However the file seems to be encrypted has no smali code is readable. Smali is to dex bytecode what asm is for intel or other arch bytecode. So the files seems to not accessible at the moment.

The others files were unknown for me at this point of the challenge, so we will give a look later.

We can load the APK in Android studio to have some information about imported package but we will see later with JEB. However we will use its emulator to launch the APK.

$ ~/.android_sdk/emulator/emulator -list-avds
Pixel_3a_API_30_x86

$ ~/.android_sdk/emulator/emulator -avd Pixel_3a_API_30_x86

$ adb root
adbd is already running as root

λ ackira ~/nextcloud/CTF/ecw2020/reverse/droid  » adb install antirdroid.apk
Performing Streamed Install
Success

$ adb shell
generic_x86_arm:/ # 
generic_x86_arm:/ ls /data/app/
com.example.ecw-NZMqf2O3-zKUJJYnkPxSzw==

generic_x86_arm:/ ls /data/app/~~G0lio3DSoSgBh6fC_oathg==/com.example.ecw-NZMqf2O3-zKUJJYnkPxSzw==/                     
base.apk  lib/      oat/

The application is installed, we can launch it in our emulator. We are welcomed by this page, there is nothing more. We are not the welcome ..

image-20201026175646080

We can now retrieve the application folder to see if it has created some files but there are only our 3 steps.

Moreover the .tflite files won’t be on file system has its only resides in the APK zip at the moment.

generic_x86_arm:/ # find / -name "step_*.dex" 2> /dev/null
/data/data/com.example.ecw/files/step_1.dex
/data/data/com.example.ecw/files/step_2.dex
/data/data/com.example.ecw/files/step_3.dex
/data/user/0/com.example.ecw/files/step_1.dex
/data/user/0/com.example.ecw/files/step_2.dex
/data/user/0/com.example.ecw/files/step_3.dex

generic_x86_arm:/ # ls -R /data/user/0/com.example.ecw/
/data/user/0/com.example.ecw/:
cache  code_cache  files  lib

/data/user/0/com.example.ecw/cache:

/data/user/0/com.example.ecw/code_cache:

/data/user/0/com.example.ecw/files:
step_1.dex  step_2.dex  step_3.dex

Step 0

The first step is to locate the MainActivity, when creating an android application developers define the first function to be executed here. I simply loaded the APK inside JEB and decompiled com.example.ecw.MainActivity class. There are 3 interesting functions :

  • OnCreate() : The first function to be called.
  • OnResume() : Called a bit later.
  • onActivityResult() : Called when we switch back to this activity after the previous activity exited.
img

The function OnCreate is not really interesting; it only opens the .dex files for later. However, the OnResume function which is called right after is more interesting :

 @Override  // c.i.a.d
    public void onResume() {
        super.onResume();
        if(!this.r && a.a(this, "android.permission.READ_CONTACTS") == 0) {
            IntRef v0 = new IntRef();
            v0.element = 0; # loop counter
            crypto_step_1 crypto_step_inst = new crypto_step_1(this, v0);
             // return a cursor on the contacts sqlite DB, data2 is the column of the contact name or surname
            Cursor contact_csr = this.getContentResolver().query(ContactsContract.Data.CONTENT_URI, null, null, null, "data2");
            if(contact_csr != null) {
                try {
                    do {
                        // for each contact we call the function invoke from the package d.c.a.b;
                        boolean v3 = ((Boolean)crypto_step_inst.invoke(contact_csr)).booleanValue(); 
                        if(!contact_csr.moveToNext()) {
                            break;
                        }
                    }
                    while(!v3);
                }
                catch(Throwable v1_1) {
                    try {
                        throw v1_1;
                    }
                    catch(Throwable v2) {
                        CloseableKt.closeFinally(contact_csr, v1_1);
                        throw v2;
                    }
                }

                CloseableKt.closeFinally(contact_csr, null);
            }

            this.r = true;
        }
    }

To summarize this function

It first checks if we have the permission to access contacts information (do not forget to allow it in the settings even with an emulator). Then it create a variable v0 which will be a loop counter. The next thing it does it’s to retrieve a cursor on the contacts database and order it by the field data2.

We can create a contact and see which data is in this field (spoiler : it’s its surname)

$ adb pull /data/data/com.android.providers.contacts/databases/contacts2.db

/data/data/com.android.providers.contacts/databases...ulled, 0 skipped. 9.5 MB/s (376832 bytes in 0.038s)
image-20201026185555844

And finally it calls the invoke method of the package d.c.a.b I have renamed crypto_step_1. Let’s find it.

@Override  // kotlin.jvm.functions.Function1
public Object invoke(Object contact_csr) {
    FileOutputStream random_file_obj;
    FileInputStream v2_2;
    File new_random_file;
    Cipher mainactivity_cipher_inst;
    MainActivity mainactivity_obj;
    a v8_1;
    Object v0_2;
    Cursor contact_csr = (Cursor)contact_csr;
    IntRef v2 = this.c;
    int v3 = v2.element;
    v2.element = v3 + 1; // counter reference
    if(v3 == 4) { // the 4th contact
        if(this.main_activity_object != null) {
            try {
                // fetching name / surname
                v0_2 = Result.constructor-impl(contact_csr.getString(contact_csr.getColumnIndex("data2")));
            }
            catch(Throwable v0_1) {
                v0_2 = Result.constructor-impl(ResultKt.createFailure(v0_1));
            }

            if(Result.isFailure-impl(v0_2)) {
                v0_2 = null;
            }

            String user_password = (String)v0_2;
            if(user_password != null) {
                MessageDigest md5_jean = MessageDigest.getInstance("MD5");
                md5_jean.update(user_password.getBytes(Charsets.UTF_8));
                if(!Intrinsics.areEqual(new BigInteger(1, md5_jean.digest()).toString(16), "b71985397688d6f1820685dde534981b")) { // md5 hash == "jean"
                    return Boolean.valueOf(true);
                }
[...]

So this function do a loaaaad of stuff. But we can split it in 2 parts.

The part above is executed for for every contact present in the phone, however it checks if the global counter is 4 which mean we already called it 3 times. If it’s not the 4th we go back to the previous function and check if there is another contact after.

But in the 4th iteration we retrieve the surname of the contact with the getColumnIndex function and then we compute its md5. If the hash is not b71985397688d6f1820685dde534981b we also go back to the previous function.

It was easy to retrieve the value of the hash with crackstation : jean

So the first contact must be named jean, what a beautiful french name.

So once this checked is passed we had an iterator for a lot of methods spread in many classes. All of these functions checked if the device was rooted or an emulator. As I did it a static way I didn’t care about.

[...]
 try {
                    Iterator v6 = d.c.a.a.a.iterator(); // call a lot of anti-debug function
                    while(true) {
                    label_48:
                        if(!v6.hasNext()) {
                            goto label_57;
                        }

                        Object v8 = v6.next();
                        v8_1 = (a)v8;
                        break;
                    }
                }
                catch(Exception v0_4) {
                    v0_4.printStackTrace();
                    Toast.makeText(this.main_activity_object, "Nice try", 0).show();
                    return Boolean.valueOf(true);
                }

                try {
                    v8_1.a();
                    goto label_48;
                }
                catch(Exception unused_ex) {
                }

                try {
                    d.c.a.a.b = null;
                    goto label_48;
                label_57:
                    Thread.sleep(800L);
                    if(d.c.a.a.b == null) {
                        Intrinsics.throwNpe();
                    }

                    byte[] salt = new byte[]{56, -35, 0x77, 0x91, 71, 0x71, -83, 70, 0x89, 0x7A, -92, 22, 0x7C, 23, -83, 110};
                    IvParameterSpec IV = new IvParameterSpec(new byte[]{-101, 105, -107, 0x8A, -65, 0x75, -35, 92, 0xD1, 0x90, -102, -76, 40, -21, 69, 93});
                    SecretKeySpec secret_key = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(user_password.toCharArray(), salt, 0x10000, 0x100)).getEncoded(), "AES");
                    // AES decryption with "jean" as key
                    Cipher cipher_inst = Cipher.getInstance("AES/CBC/PKCS7Padding");
                    cipher_inst.init(2, secret_key, IV);
                    this.main_activity_object.cipher_inst = cipher_inst;
                    mainactivity_obj = this.main_activity_object;
                    mainactivity_cipher_inst = this.main_activity_object.cipher_inst;
                    String random_uuid = UUID.randomUUID().toString();  // create tempory file
                    new_random_file = new File(mainactivity_obj.getFilesDir(), random_uuid);
                    v2_2 = mainactivity_obj.openFileInput("step_1.dex"); 
                    // reading and decrypting step_1.dex data
                    random_file_obj = mainactivity_obj.openFileOutput(random_uuid, 0);
                }
                catch(Exception v0_4) {
                    v0_4.printStackTrace();
                    Toast.makeText(this.main_activity_object, "Nice try", 0).show();
                    return Boolean.valueOf(true);
                }

                try {
                    byte[] v9 = new byte[0x1000];
                    do {
                        int v10 = v2_2.read(v9);
                        if(v10 > 0) {
                            byte[] v11 = v10 == 0x1000 ? mainactivity_cipher_inst.update(v9) : mainactivity_cipher_inst.doFinal(v9, 0, v10);
                            random_file_obj.write(v11);
                        }
                    }
                    while(v10 >= 0);

                    goto label_189;
                }
                catch(Throwable v0_7) {
                    try {
                        throw v0_7;
                    }
                    catch(Throwable v0_8) {
                    }

                    try {
                        CloseableKt.closeFinally(v2_2, v0_7);
                        throw v0_8;
                    label_189:
                        CloseableKt.closeFinally(v2_2, null);
                        goto label_199;
                    }
                    catch(Throwable v0_9) {
                    }
                }

                try {
                    throw v0_9;
                }
                catch(Throwable v0_10) {
                }

                try {
                    CloseableKt.closeFinally(random_file_obj, v0_9);
                    throw v0_10;
                label_199:
                    CloseableKt.closeFinally(random_file_obj, null);
                    if(new_random_file.exists()) {
                        // then it calls function present in the .dex file
                        PathClassLoader class_loader = new PathClassLoader(new_random_file.getAbsolutePath(), mainactivity_obj.getClassLoader());
                        // delete str
                        String delete_str = new String(this.main_activity_object.cipher_inst.doFinal(Base64.decode("j04vGcW35ZUg23JsqQ+/YA==", 0)), Charsets.UTF_8);
                        new_random_file.getClass().getMethod(delete_str).invoke(new_random_file);  // delete file
                        Object inst_a_a_a_c = class_loader.loadClass(new String(this.main_activity_object.cipher_inst.doFinal(Base64.decode("WOtre8ObMy2nnFbqn2Kb6w==", 0)), Charsets.UTF_8)).newInstance();  // a.a.a.c
                        // a str
                        String a_str = new String(this.main_activity_object.cipher_inst.doFinal(Base64.decode("J9vFCBjTjE6YoMI1wVDwjg==", 0)), Charsets.UTF_8);
                        Class[] v2_4 = new Class[]{Activity.class};
                        int v8_3;

                        // find the right function to call (package a.a.a.c and public final Thread a(Activity arg10) )

                        for(v8_3 = 0; v8_3 < inst_a_a_a_c.getClass().getDeclaredMethods().length; ++v8_3) {
                            Method selected_method = inst_a_a_a_c.getClass().getDeclaredMethods()[v8_3];
                            int v10_1 = !Intrinsics.areEqual(selected_method.getName(), a_str) || !Arrays.equals(selected_method.getParameterTypes(), ((Object[])v2_4)) ? 0 : 1;
                            if(v10_1 != 0) {
                                selected_method.invoke(inst_a_a_a_c, this.main_activity_object);

                                // store the password which will be used to decrypt the other base64 strings       
                                this.main_activity_object.getSharedPreferences("flag", 0).edit().putString("a", new String(this.main_activity_object.cipher_inst.doFinal(Base64.decode("bjmQcWsAN3k8NxmaYYWvy6L+SDvu3ZlDFMSFvepIycxwZLgw5qGRB5ggJLHpDvW3", 0)), Charsets.UTF_8)).apply();
                                ((TextView)this.main_activity_object.q.getValue()).setVisibility(8); // display a input text widget to fill a password
                                return Boolean.valueOf(true);
                            }
                        }

                        throw new NoSuchElementException("Array contains no element matching the predicate.");
                    }

                    throw new FileNotFoundException();
                }
                catch(Exception v0_4) {
                    v0_4.printStackTrace();
                    Toast.makeText(this.main_activity_object, "Nice try", 0).show();
                }
            }

            return Boolean.valueOf(true);
        }

        throw null;
    }

    return Boolean.valueOf(false);
}

The above code will decrypt step_1.dex and then try to identify the a.a.a.c class inside it and its Thread a(Activity arg10) function.

The call to this function will be performed by

selected_method.invoke(inst_a_a_a_c, this.main_activity_object);

Once this code is executed it will makes visible a text input gadget in order to fill a password and then return.

Also it will save the decrypted base64 in the shared preferences in the key a : LuKXSGlN5(%:Vk=alEbl9khIEPBo=mXu;hR7Ez7E

We now have access to the next step !

step 1

We now have decrypted the file step_1.dex and have called the function public final Thread a(Activity arg10) inside the a.a.a.c package. We are going to decompile it thanks to JEB.

The function code is pretty small. OK just keep in mind it’s java code …

package a.a.a;

import [...]

public final class c {
    public static final c.a a = null;
    public static final String a = "password.txt";
    public Cipher a;
    public boolean a;

    public static {
        c.a = new c.a(null);
    }

    public static final Cipher a(c arg1) {
        return arg1.a;
    }

    public static final void a(c arg0, Cipher arg1) {
        arg0.a = arg1;
    }

    public static final void a(c arg0, boolean arg1) {
        arg0.a = arg1;
    }

    public static final boolean a(c arg1) {
        return arg1.a;
    }

    public final Thread a(Activity arg10) {
        SharedPreferences v3 = arg10.getSharedPreferences("save", 0);
        arg10.getSharedPreferences("flag", 0).edit().putString("w", "k").apply();
        Toast.makeText(arg10, "You made it to step 1", 0).show();
        String v2 = v3.getString("pass1", null);
        LinearLayout v0 = (LinearLayout)arg10.findViewById(R.id.base);
        View v4 = View.inflate(arg10, R.layout.check, null);
        Button v6 = (Button)v4.findViewById(R.id.validation);
        EditText v1 = (EditText)v4.findViewById(R.id.password);
        v1.setText(v2);
        v0.addView(v4);
        v6.setOnClickListener(new c.b(v1, arg10, v3, this, arg10)); // set the callback
        // run some code in background
        return ThreadsKt.thread$default(false, false, null, null, 0, new c.c(arg10), 0x1F, null);
    }
}

It edits the shared preferencesto save some key value pairs, we will see later why they are useful. I didn’t mind it at first analysis and they only make sense in the last step.

The most important things are :

  • get password from the shared preference (empty at the moment)
  • Inflate some layout and widgets including a text box
  • set a button on click listening with the call back a.a.a.c$b class
  • return a thread on the a.a.a.c$c class

The first code to be called is a.a.a.c$c as a.a.a.c$b is only a callback and not executed until the user click on the submit button.

Let’s analyze a.a.a.c$c

package a.a.a;

import a.a.a.e.b;
import a.a.a.e.c;
import a.a.a.e.d;
import a.a.a.e.e;
import a.a.a.e.f;
[...]

public final class define_password_step extends Lambda implements Function0 {
    public final Activity prev_activity;

    public define_password_step(Activity arg2) {
        this.prev_activity = arg2;
        super(0);
    }

    public final void a() { // constructor
        FileOutputStream v1 = this.prev_activity.openFileOutput("password.txt", 0);
        try {
            v1.write(new byte[52]);
        }
        catch(Throwable v0) {
            try {
                throw v0;
            }
            catch(Throwable v2) {
                CloseableKt.closeFinally(v1, v0);
                throw v2;
            }
        }

        CloseableKt.closeFinally(v1, null);
        FileOutputStream pwd_file = this.prev_activity.openFileOutput("password.txt", 0);
        try {
            FileChannel password_file_channel = pwd_file.getChannel();
            Iterator step_iterator = CollectionsKt.listOf(new b[]{e.a, c.a, d.a, f.a}).iterator(); // define all check to call
            while(true) {
            label_35:
                if(!step_iterator.hasNext()) {
                    goto label_51;
                }

                Object step = step_iterator.next();
                b v0_3 = (b)step;
                try {
                    v0_3.a(this.prev_activity, password_file_channel); // we call each step
                    break;
                }
                catch(Exception v0_4) {
                }

                v0_4.printStackTrace();
            }
        }
        catch(Throwable v0_1) {
            throw v0_1;
        }

        goto label_35;
        try {
            throw v0_1;
        }
        catch(Throwable v2_2) {
        }

        CloseableKt.closeFinally(pwd_file, v0_1);
        throw v2_2;
    label_51:
        CloseableKt.closeFinally(pwd_file, null);
    }

    public Object invoke() {
        this.a();
        return Unit.INSTANCE;
    }
}

This function is in charge of creating the file password.txt and to set char in it. In order to set the char it will call all of the following classes e.a, c.a, d.a, f.a. Each one had its own code to check if the device is rooted or debugged.

One example of check class

package a.a.a.e;


public final class c implements b {
    public static final c a;
    public static final String[] file_root_list;

    public static {
        c.a = new c();
        c.file_root_list = new String[]{"/system/app/Superuser.apk", "/system/etc/init.d/99SuperSUDaemon", "/dev/com.koushikdutta.superuser.daemon/", "/system/xbin/daemonsu", "/sbin/su", "/system/bin/su", "/system/bin/failsafe/su", "/system/xbin/su", "/system/xbin/busybox", "/system/sd/xbin/su", "/data/local/su", "/data/local/xbin/su", "/data/local/bin/su"};
    }

    public void a(Context arg12, FileChannel arg13) {
        FileInputStream v7;
        String[] file_root_list = c.file_root_list;
        int v1 = 0;
        while(v1 < file_root_list.length) {
            String file_root = file_root_list[v1];
            try {
                // if one the above files exist it will just return 
                if(new File(file_root).exists()) {
                    return;
                }

                v7 = new FileInputStream(file_root);
            }
            catch(Exception v0_1) {
                goto label_34;
            }

            try {
                int v0_3 = v7.available();
                goto label_28;
            }
            catch(Throwable v0_2) {
            }

            try {
                throw v0_2;
            }
            catch(Throwable v8) {
            }

            try {
                CloseableKt.closeFinally(v7, v0_2);
                throw v8;
            label_28:
                CloseableKt.closeFinally(v7, null);
            }
            catch(Exception v0_1) {
                goto label_34;
            }

            if(v0_3 > 0) {
                return;
            }

        label_34:
            ++v1;
        }

          
        // only this code is interesting for us as it decode chuncks of the password
        if(b.a.a()) {
            String v0_4 = arg12.getSharedPreferences("flag", 0).getString("a", "");
            if(v0_4 == null) {
                Intrinsics.throwNpe();
            }

            IvParameterSpec v1_1 = new IvParameterSpec(new byte[]{-101, 105, -107, 0x8A, -65, 0x75, -35, 92, 0xD1, 0x90, -102, -76, 40, -21, 69, 93});
            SecretKeySpec v3_1 = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(v0_4.toCharArray(), new byte[]{56, -35, 0x77, 0x91, 71, 0x71, -83, 70, 0x89, 0x7A, -92, 22, 0x7C, 23, -83, 110}, 0x10000, 0x100)).getEncoded(), "AES");
            Cipher v0_5 = Cipher.getInstance("AES/CBC/PKCS7Padding");
            v0_5.init(2, v3_1, v1_1);
            b.a.a(v0_5);
        }

        List v0_6 = StringsKt.split$default(new String(b.a.a().doFinal(Base64.decode("7wwCGcbnGp/EAusByZQYcYsxSfBxiEHP4GZPjsAHjGLYVryk6yS9xTo6GmF1J6Z6rDvp8XnuBCZ97DmURQx+lvAvrebYDXPEbiVOcSANTk4=", 0)), Charsets.UTF_8), new String[]{"!"}, false, 0, 6, null);
        ArrayList v3_2 = new ArrayList();
        for(Object v1_2: v0_6) {
            if(((String)v1_2).length() <= 0) {
                continue;
            }

            v3_2.add(v1_2);
        }

        for(Object v0_7: v3_2) {
            a.a.a.b v2 = new a.a.a.b(((String)v0_7));
            arg13.write(v2.a(), v2.a());
        }
    }
}

Only the last part of code is interesting as the first is still anti debugging and rooting identification and has no impact on the flag generation.

It will use the password stored in the a key to decode the base64, which once decoded will give :

8:_!15:e!47:R!35:Z!46:'!51:7!25:B!11:r!26:<!48:C!10:o!27:S!33:r!

Following the code logic (I wont detail it here) it’s just part of the password of the step 1 separated by ! and having the following pattern X:Y with X the place of the char in the string and Y the value. The different calls to all these java classes are just to write it inside the password.txt

Here’s my python script to decode it the static way, be sure to have gathered all code from the different checks.

password_data = "8:_!15:e!47:R!35:Z!46:'!51:7!25:B!11:r!26:<!48:C!10:o!27:S!33:r!" # root file
password_data += "22:p!40:V!20:s!2:s!17:1!21:@!32:W!29:a!37:.!39:t!30:T!36:U!9:f!" # package file + zip + rsrc @ instead of :
password_data += "19:i!38:V!49:b!34:b!4:w!23:y!1:a!41:%!16:p!14:t!6:r!13:s!12:_!" # magisk
password_data += "45:*!3:s!42:b!43:j!31:1!7:d!44:M!28:9!0:p!5:o!18:_!24:5!50:O!" # fclass

password = {}

for i in password_data.split("!")[:-1]:
    pos, value = i.split(":")
    password[pos] = value.replace("@", ":")

flag = "".join([password[str(i)] for i in range(52) ])
print(flag)

# password_for_step1_is: "py5B<S9aT1WrbZU.VtV%bjM*'RCbO7"

Ok we got the password, if we had done it the dynamically way we should have entered it inside the input text but due to the static way we have to analyze the code executed when the button is clicked.

password_for_step1_is: "py5B<S9aT1WrbZU.VtV%bjM*'RCbO7"

Let’s analyze c.b(v1, arg10, v3, this, arg10) callback

package a.a.a;

public final class c.b implements View.OnClickListener {
    public final c a;
    public final Activity a;
    public final SharedPreferences a;
    public final EditText a;
    public final Activity b;

    public c.b(EditText arg1, Activity arg2, SharedPreferences arg3, c arg4, Activity arg5) {
        this.a = arg1;
        this.a = arg2;
        this.a = arg3;
        this.a = arg4;
        this.b = arg5;
        super();
    }

    @Override  // android.view.View$OnClickListener
    public final void onClick(View arg14) {
        Method[] v7_1;
        String v6_1;  // a
        Object v5_2;  // a.a.a.c / 
        FileOutputStream v8;
        FileInputStream v7;
        File v6;
        Cipher v5_1;
        Activity v2_2;
        String v4 = this.a.getText().toString();
        if(c.a(this.a)) {
            Toast.makeText(this.a, "clicks are not gonna resolve the challenge", 0).show();
            return;
        }

        try {
            IvParameterSpec v0_1 = new IvParameterSpec(new byte[]{-101, 105, -107, 0x8A, -65, 0x75, -35, 92, 0xD1, 0x90, -102, -76, 40, -21, 69, 93});
            SecretKeySpec v5 = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(v4.toCharArray(), new byte[]{56, -35, 0x77, 0x91, 71, 0x71, -83, 70, 0x89, 0x7A, -92, 22, 0x7C, 23, -83, 110}, 0x10000, 0x100)).getEncoded(), "AES");
            Cipher v2 = Cipher.getInstance("AES/CBC/PKCS7Padding");
            v2.init(2, v5, v0_1);
            c.a(this.a, v2);
            
            // ECW_oe8%jXffkWul&#!V@tqB(:V%WP?JUKm@I(2KqIfv
            String v2_1 = new String(c.a(this.a).doFinal(Base64.decode("ZnoETjqJ0h3VUtdPQnzkWsqrDFtvsK4BQ+1NJGx38YHXq9QxUEmztU9CsN4vCTbI", 0)), Charsets.UTF_8);  
            this.a.getSharedPreferences("flag", 0).edit().putString("j", v2_1).apply();
            Log.i("FLAG 1", v2_1);
            v2_2 = this.a;
            v5_1 = c.a(this.a);
            String v0_2 = UUID.randomUUID().toString();
            v6 = new File(v2_2.getFilesDir(), v0_2);
            v7 = v2_2.openFileInput("step_2.dex");
            v8 = v2_2.openFileOutput(v0_2, 0);
        }
        catch(Exception v0) {
            goto label_169;
        }

        try {
            byte[] v9 = new byte[0x1000];
            do {
            label_71:
                int v10 = v7.read(v9);
                if(v10 > 0) {
                    byte[] v0_4 = v10 == 0x1000 ? v5_1.update(v9) : v5_1.doFinal(v9, 0, v10);
                    v8.write(v0_4);
                }

                goto label_85;
            }
            while(true);
        }
        catch(Throwable v0_3) {
        }

        try {
            throw v0_3;
        }
        catch(Throwable v2_3) {
        }

        try {
            CloseableKt.closeFinally(v7, v0_3);
            throw v2_3;
        label_85:
            if(v10 >= 0) {
                goto label_71;
            }

            CloseableKt.closeFinally(v7, null);
            goto label_95;
        }
        catch(Throwable v0_5) {
        }

        try {
            throw v0_5;
        }
        catch(Throwable v2_4) {
        }

        try {
            CloseableKt.closeFinally(v8, v0_5);
            throw v2_4;
        label_95:
            CloseableKt.closeFinally(v8, null);
            if(!v6.exists()) {
                throw new FileNotFoundException();
            }

            v5_2 = new PathClassLoader(v6.getAbsolutePath(), v2_2.getClassLoader()).loadClass(new String(c.a(this.a).doFinal(Base64.decode("tvEf77LVcQcHX2FtkIoSBQ==", 0)), Charsets.UTF_8)).newInstance();  // a.a.a.c / 
            v6_1 = new String(c.a(this.a).doFinal(Base64.decode("TbQSB6aY7Ye++tVv84UPIA==", 0)), Charsets.UTF_8);  // a
            v7_1 = v5_2.getClass().getDeclaredMethods();
            int v8_1 = v7_1.length;
        }
        catch(Exception v0) {
            goto label_169;
        }

        int v0_6 = 0;
        while(true) {
        label_126:
            if(v0_6 >= v8_1) {
                throw new NoSuchElementException("Array contains no element matching the predicate.");
            }

            Method v9_1 = v7_1[v0_6];
            try {
                int v2_5 = (Intrinsics.areEqual(v9_1.getName(), v6_1)) && (Arrays.equals(v9_1.getParameterTypes(), new Class[]{Activity.class})) ? 1 : 0;
                if(v2_5 != 0) {
                    v9_1.invoke(v5_2, this.b);
                    c.a(this.a, true);
                    this.a.edit().putString("pass1", v4).apply();
                    return;
                }

                ++v0_6;
                goto label_126;
                throw new NoSuchElementException("Array contains no element matching the predicate.");
                throw new FileNotFoundException();
            }
            catch(Exception v0) {
                break;
            }
        }

    label_169:
        v0.printStackTrace();
        Toast.makeText(this.a, "Nice try", 0).show();
    }
}

This part is a bit like the end of the step 0, as it use the provided password to decrypt some base64. Here we can get our first flag ECW_oe8%jXffkWul&#!V@tqB(:V%WP?JUKm@I(2KqIfv !

Then it will also decode the file step_2.txt with the same key and like the step 0 will identify the class and the method to execute inside without forgetting to store the flag inside the shared preferences.

It will also be the a function inside a.a.a.c which will be executed for the step 2.

step 2

We now have decrypted the file step_2.dex and have called the function public final Thread a(Activity arg10) inside the a.a.a.c package. We gonna decompile it thanks to JEB.

Image tagged in matrix cat - Imgflip

As before we land in the code which is loading some layout and input text


public final void a(Activity prev_activity) {
    SharedPreferences v3 = prev_activity.getSharedPreferences("save", 0);
    Toast.makeText(prev_activity, "You made it to step 2", 0).show();
    check_frida.a.c(prev_activity); // frida check
    String v2 = v3.getString("pass2", null);
    LinearLayout v0 = (LinearLayout)prev_activity.findViewById(R.id.base);
    View v4 = View.inflate(prev_activity, R.layout.check, null);
    Button v6 = (Button)v4.findViewById(R.id.validation);
    EditText v1 = (EditText)v4.findViewById(R.id.password);
    v1.setText(v2);
    v0.addView(v4);
    v6.setOnClickListener(new on_click_listener(v1, prev_activity, v3, this, prev_activity));
}

This time there are checks which are looking for the Frida server files inside some directories but it wont impact us.

 public final void onClick(View arg15) {
        FileOutputStream v6_1;
        FileInputStream v8;
        File v7;
        Cipher v5;
        Activity v1_2;
        String method;
        String classname;
        String user_input_mdp = this.a.getText().toString();
        if(check_input.do_check_object.do_check_input(user_input_mdp)) {
            try {
                byte[] IV = new byte[]{56, -35, 0x77, 0x91, 71, 0x71, -83, 70, 0x89, 0x7A, -92, 22, 0x7C, 23, -83, 110};
                IvParameterSpec v1 = new IvParameterSpec(new byte[]{-101, 105, -107, 0x8A, -65, 0x75, -35, 92, 0xD1, 0x90, -102, -76, 40, -21, 69, 93});
                SecretKeySpec v3 = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(user_input_mdp.toCharArray(), IV, 0x10000, 0x100)).getEncoded(), "AES");
                Cipher cipher_inst = Cipher.getInstance("AES/CBC/PKCS7Padding");
                cipher_inst.init(2, v3, v1);
                landing_step2.a(this.a, cipher_inst);
                String v1_1 = new String(landing_step2.a(this.a).doFinal(Base64.decode("5sxJURBMWadPV+Qfj2g/WFVWcaLbXoUxyXeiIvpa4pu1SjSj0nqneJeN0tNkKbJx", 0)), Charsets.UTF_8);
                classname = new String(landing_step2.a(this.a).doFinal(Base64.decode("gkZ6pGuoDU6Lz5bc23Y/5ZfI9XPcJd/r1PRrsE1epqc=", 0)), Charsets.UTF_8);
                method = new String(landing_step2.a(this.a).doFinal(Base64.decode("rbfA5lkSHq0eL4dmwH4gHg==", 0)), Charsets.UTF_8);
                Activity v0_3 = this.b;
                Toast.makeText(v0_3, "Congrats, almost there", 0).show();
                this.b.getSharedPreferences("flag", 0).edit().putString("q", v1_1).apply();
                Log.i("FLAG 2", v1_1);
                v1_2 = this.a;
                v5 = landing_step2.a(this.a);
                String v6 = UUID.randomUUID().toString();
                v7 = new File(v1_2.getFilesDir(), v6);
                v8 = v1_2.openFileInput("step_3.dex");
                v6_1 = v1_2.openFileOutput(v6, 0);
            }
            catch(Exception v0) {
                v0.printStackTrace();
                Toast.makeText(this.a, "Nice try", 0).show();
                return;
            }

            try {
                byte[] v12 = new byte[0x1000];
                do {
                label_184:
                    int v13 = v8.read(v12);
                    if(v13 > 0) {
                        byte[] v0_5 = v13 == 0x1000 ? v5.update(v12) : v5.doFinal(v12, 0, v13);
                        v6_1.write(v0_5);
                    }

                    goto label_198;
                }
                while(true);
            }
            catch(Throwable v0_4) {
            }
[...]

This time the on click listener call back directly call code to verify the input, no code is executed in the background. The function executed check_input will do a lot of call with always the same pattern :

 public final boolean do_check_input(String user_input) {
        return weird_comparison.multi_cmp(user_input, 4, 0, 2, null) * weird_comparison.multi_cmp(user_input, 6, 0, 2, null) == 4840 && ((char)(weird_comparison.multi_cmp(user_input, 9, 0, 2, null) + weird_comparison.multi_cmp(user_input, 14, 0, 2, null))) == 0xD9 && weird_comparison.multi_cmp(user_input, 6, 0, 2, null) * weird_comparison.multi_cmp(user_input, 8, 0, 2, null) == 9559 && ((char)(weird_comparison.multi_cmp(user_input, 8, 0, 2, null) + weird_comparison.multi_cmp(user_input, 13, 0, 2, null))) == 0x8D && weird_comparison.multi_cmp(user_input, 9, 0, 2, null) * weird_comparison.multi_cmp(user_input, 7, 0, 2, null) == 0x28FE && weird_comparison.multi_cmp(user_input, 1, 0, 2, null) * weird_comparison.multi_cmp(user_input, 2, 0, 2, null) == 5346 && weird_comparison.multi_cmp(user_input, 4, 0, 2, null) * weird_comparison.multi_cmp(user_input, 0, 0, 2, null) == 0xD20 && ((char)(weird_comparison.multi_cmp(user_input, 10, 0, 2, null) + weird_comparison.multi_cmp(user_input, 2, 0, 2, null))) == 0xA7 && weird_comparison.multi_cmp(user_input, 9, 0, 2, null) * weird_comparison.multi_cmp(user_input, 13, 0, 2, null) == 0x17FA && ((char)(weird_comparison.multi_cmp(user_input, 12, 0, 2, null) + weird_comparison.multi_cmp(user_input, 14, 0, 2, null))) == 0xC1 && weird_comparison.multi_cmp(user_input, 6, 0, 2, null) * weird_comparison.multi_cmp(user_input, 3, 0, 2, null) == 0x35E2 && weird_comparison.multi_cmp(user_input, 3, 0, 2, null) * weird_comparison.multi_cmp(user_input, 10, 0, 2, null) == 9804 && weird_comparison.multi_cmp(user_input, 7, 0, 2, null) * weird_comparison.multi_cmp(user_input, 0, 0, 2, null) == 8904 && ((char)(weird_comparison.multi_cmp(user_input, 7, 0, 2, null) + weird_comparison.multi_cmp(user_input, 14, 0, 2, null))) == 0xE0 && ((char)(weird_comparison.multi_cmp(user_input, 9, 0, 2, null) + weird_comparison.multi_cmp(user_input, 13, 0, 2, null))) == 0xA1 && weird_comparison.multi_cmp(user_input, 9, 0, 2, null) * weird_comparison.multi_cmp(user_input, 14, 0, 2, null) == 0x2DA2 && ((char)(weird_comparison.multi_cmp(user_input, 10, 0, 2, null) + weird_comparison.multi_cmp(user_input, 13, 0, 2, null))) == 0x94 && ((char)(weird_comparison.multi_cmp(user_input, 14, 0, 2, null) + weird_comparison.multi_cmp(user_input, 5, 0, 2, null))) == 0xD8 && ((char)(weird_comparison.multi_cmp(user_input, 4, 0, 2, null) + weird_comparison.multi_cmp(user_input, 6, 0, 2, null))) == 0xA1 && ((char)(weird_comparison.multi_cmp(user_input, 6, 0, 2, null) + weird_comparison.multi_cmp(user_input, 2, 0, 2, null))) == 202 && weird_comparison.multi_cmp(user_input, 9, 0, 2, null) * weird_comparison.multi_cmp(user_input, 8, 0, 2, null) == 7821 && weird_comparison.multi_cmp(user_input, 14, 0, 2, null) * weird_comparison.multi_cmp(user_input, 5, 0, 2, null) == 0x2D2C && weird_comparison.multi_cmp(user_input, 9, 0, 2, null) * weird_comparison.multi_cmp(user_input, 4, 0, 2, null) == 0xF78 && ((char)(weird_comparison.multi_cmp(user_input, 4, 0, 2, null) + weird_comparison.multi_cmp(user_input, 8, 0, 2, null))) == 0x77 && ((char)(weird_comparison.multi_cmp(user_input, 6, 0, 2, null) + weird_comparison.multi_cmp(user_input, 3, 0, 2, null))) == 0xEB && weird_comparison.multi_cmp(user_input, 6, 0, 2, null) * weird_comparison.multi_cmp(user_input, 2, 0, 2, null) == 9801 && ((char)(weird_comparison.multi_cmp(user_input, 0, 0, 2, null) + weird_comparison.multi_cmp(user_input, 10, 0, 2, null))) == 170 && weird_comparison.multi_cmp(user_input, 7, 0, 2, null) * weird_comparison.multi_cmp(user_input, 10, 0, 2, null) == 9116 && ((char)(weird_comparison.multi_cmp(user_input, 7, 0, 2, null) + weird_comparison.multi_cmp(user_input, 10, 0, 2, null))) == 0xC0 && ((char)(weird_comparison.multi_cmp(user_input, 6, 0, 2, null) + weird_comparison.multi_cmp(user_input, 8, 0, 2, null))) == 200 && weird_comparison.multi_cmp(user_input, 11, 0, 2, null) * weird_comparison.[...]ti_cmp(user_input, 1, 0, 2, null) + weird_comparison.multi_cmp(user_input, 2, 0, 2, null))) == 0x93;
    }

note that JEB is truncating the line, you should use JADX to get the whole line

and the comparison look like this in the end :

public final class weird_comparison {
    public static final int return_char_value(@NotNull String user_input, int nb_x, int nb_0) {
        Character index_in_string = StringsKt.getOrNull(user_input, nb_x);
        return index_in_string == null ? nb_0 : index_in_string.charValue();
    }

    public static int multi_cmp(String user_input, int nb_x, int nb_0, int nb_2, Object arg5) {
        if((nb_2 & 2) != 0) {
            nb_0 = -1;
        }

        return weird_comparison.return_char_value(user_input, nb_x, nb_0);
    }
}

It takes 4 parameters :

  • the user input
  • an integer between 0 and 15
  • const 0
  • const 2

The last 2 params are always the same constant 0, and 2. After a further analysis the last 2 parameters are just useless as they didn’t impact the execution flow.

return_char_value will only returns the value at the index provided by the second parameter. It’s look like a big equation for z3.

from z3 import *

regex = r"f\.a\(arg8, ([0-9]+), 0, 2, null\) (\*|\+) f\.a\(arg8, ([0-9]+), 0, 2, null\)\)?\)? == (0?x?[a-fA-F0-9]+)"

# for line in directives.split("\n"):
#     found = findall(regex, line)[0]
#     print("s.add((flag[{0}] {1} flag[{2}]) == {3})".format(*found))

flag = IntVector("f", 16)
s = Solver()

s.add((flag[4] * flag[6]) == 4840)
s.add((flag[9] + flag[14]) == 0xD9)
s.add((flag[6] * flag[8]) == 9559)
s.add((flag[8] + flag[13]) == 0x8D)
s.add((flag[9] * flag[7]) == 0x28FE)
s.add((flag[1] * flag[2]) == 5346)
s.add((flag[4] * flag[0]) == 0xD20)
s.add((flag[10] + flag[2]) == 0xA7)
s.add((flag[9] * flag[13]) == 0x17FA)
s.add((flag[12] + flag[14]) == 0xC1)
s.add((flag[6] * flag[3]) == 0x35E2)
s.add((flag[3] * flag[10]) == 9804)
s.add((flag[7] * flag[0]) == 8904)
s.add((flag[7] + flag[14]) == 0xE0)
s.add((flag[9] + flag[13]) == 0xA1)
s.add((flag[9] * flag[14]) == 0x2DA2)
s.add((flag[10] + flag[13]) == 0x94)
s.add((flag[14] + flag[5]) == 0xD8)
s.add((flag[4] + flag[6]) == 0xA1)
s.add((flag[6] + flag[2]) == 202)
s.add((flag[9] * flag[8]) == 7821)
s.add((flag[14] * flag[5]) == 0x2D2C)
s.add((flag[9] * flag[4]) == 0xF78)
s.add((flag[4] + flag[8]) == 0x77)
s.add((flag[6] + flag[3]) == 0xEB)
s.add((flag[6] * flag[2]) == 9801)
s.add((flag[0] + flag[10]) == 170)
s.add((flag[7] * flag[10]) == 9116)
s.add((flag[7] + flag[10]) == 0xC0)
s.add((flag[6] + flag[8]) == 200)
s.add((flag[11] * flag[1]) == 6468)
s.add((flag[9] + flag[8]) == 0xB2)
s.add((flag[2] + flag[14]) == 0xC7)
s.add((flag[7] + flag[0]) == 190)
s.add((flag[8] * flag[5]) == 7742)
s.add((flag[15] * flag[13]) == 7316)
s.add((flag[10] * flag[13]) == 5332)
s.add((flag[8] * flag[13]) == 4898)
s.add((flag[6] + flag[14]) == 0xEF)
s.add((flag[8] + flag[5]) == 0xB1)
s.add((flag[1] * flag[4]) == 0xA50)
s.add((flag[0] + flag[3]) == 0xC6)
s.add((flag[11] + flag[1]) == 0xA4)
s.add((flag[10] * flag[2]) == 6966)
s.add((flag[0] * flag[3]) == 9576)
s.add((flag[12] * flag[14]) == 8850)
s.add((flag[6] * flag[14]) == 0x37C6)
s.add((flag[0] * flag[10]) == 7224)
s.add((flag[2] * flag[14]) == 9558)
s.add((flag[9] + flag[7]) == 205)
s.add((flag[8] + flag[0]) == 0xA3)
s.add((flag[15] + flag[13]) == 180)
s.add((flag[1] + flag[4]) == 106)
s.add((flag[8] * flag[0]) == 6636)
s.add((flag[4] * flag[8]) == 3160)
s.add((flag[4] + flag[0]) == 0x7C)
s.add((flag[7] * flag[14]) == 12508)
s.add((flag[3] + flag[10]) == 200)
s.add((flag[9] + flag[4]) == 139)
s.add((flag[1] + flag[2]) == 147)

if s.check() != unsat:
    mod = s.model() 
    flag = "".join([ chr(mod[flag[i]].as_long()) for i in range(16) ]) 
    print(flag)     

And we finally get an input which is satisfying the model : TBQr(byjOcVbK>vv.

With this flag we can decrypt the base64 encoded flag.

    public static void decrypt_flag_step_2() {
        try {

            byte[] IV = new byte[]{56, -35, 0x77, (byte) 0x91, 71, 0x71, -83, 70, (byte) 0x89, 0x7A, -92, 22, 0x7C, 23, -83, 110};
            IvParameterSpec v1 = new IvParameterSpec(new byte[]{-101, 105, -107, (byte) 0x8A, -65, 0x75, -35, 92, (byte)0xD1, (byte)0x90, -102, -76, 40, -21, 69, 93});
            SecretKeySpec v3 = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec("TBQr(byjOcVbK>vv".toCharArray(), IV, 0x10000, 0x100)).getEncoded(), "AES");
            Cipher cipher_inst = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher_inst.init(2, v3, v1);
            String v1_1 = new String(cipher_inst.doFinal((Base64.getDecoder().decode("5sxJURBMWadPV+Qfj2g/WFVWcaLbXoUxyXeiIvpa4pu1SjSj0nqneJeN0tNkKbJx"))));
            //v3_1 = new String(landing_step2.a(this.a).doFinal(Base64.decode("gkZ6pGuoDU6Lz5bc23Y/5ZfI9XPcJd/r1PRrsE1epqc=", 0)), Charsets.UTF_8);
            //v4 = new String(landing_step2.a(this.a).doFinal(Base64.decode("rbfA5lkSHq0eL4dmwH4gHg==", 0)), Charsets.UTF_8);
            System.out.println(v1_1);

        } catch (Exception e) {
        System.out.println(e);
    }

    }

And our second flag : ECW_AIU/yMZg3c7(NqGyqu8Iv3j8Oszx+1<>i'7&o(9g.

I think you know what happen now, we retrieve the classname with a loop and also the function to be executed in order to land in the step_3.dex code

step 3

We are now in the 3rd and last step of this challenge. With the decoded base64 from the last step we know that the class is com.example.step_3.Step3 and the called method is run. Let’s fire JEB once again and for the last time !

    public final void run(Activity arg9) {
        Toast.makeText(arg9, "You made it to step 3", 0).show();
        SharedPreferences v3 = arg9.getSharedPreferences("flag", 0);
        String v0 = "";
        char v1;
        for(v1 = 'a'; v1 <= 0x7A; v1 = (char)(v1 + 1)) {
            String v4 = v3.getString(String.valueOf(v1), null);
            if(v4 != null) {
                v0 = v0 + v4;
            }
        }

        try {
            arg9.startActivityForResult(new Intent(arg9, Class.forName("com.example.ecw.FinishActivity")), 12);
        }
        catch(Exception v0_1) {
            v3.edit().putString(String.valueOf(RangesKt.random(new CharRange('a', 'z'), Random.Default)), String.valueOf(RangesKt.random(new CharRange('a', 'z'), Random.Default))).apply();
            Toast.makeText(arg9, "You made it to step 3 but i have a bad feeling about you", 0).show();
        }
    }

Apart an useless computation this function is only calling a new intent for com.example.ecw.FinishActivity. We can go back to the APK main classes from the beginning to see what is in this method.

There are 3 methods :

    public FinishActivity() {
        this.p = LazyKt__LazyJVMKt.lazy(new FinishActivity$a.a(this));
    }

    @Override  // c.b.k.e
    public void onCreate(Bundle arg5) {
        super.onCreate(arg5);
        this.setContentView(0x7F0A0021);  // layout:end_layout
        Object v5 = this.p.getValue();
        v5.getClass().getMethod("onCreate").invoke(v5);
    }

    @Override  // c.b.k.e
    public void onDestroy() {
        super.onDestroy();
        Object v0 = this.p.getValue();
        v0.getClass().getMethod("onDestroy").invoke(v0);

The onCreate function is invoking a new object inside FinishActivity$a.

Which is also a trampoline toward another function call.

Once raTLFVkpCb4yP1YXsMdvqr2TjJSxtpiYA0yJLQ2UTPs= decoded with the key stored in the shared preference at index a key : LuKXSGlN5(%:Vk=alEbl9khIEPBo=mXu;hR7Ez7E is com.example.step_3.FinishImpl

    public final Object invoke() {
        Class v0 = this.b.getClass();
        String v1 = this.b.getSharedPreferences("flag", 0).getString("a", null);
        if(v1 == null) {
            v1 = "fail";
        }

        byte[] v4 = new byte[]{56, -35, 0x77, 0x91, 71, 0x71, -83, 70, 0x89, 0x7A, -92, 22, 0x7C, 23, -83, 110};
        IvParameterSpec v5 = new IvParameterSpec(new byte[]{-101, 105, -107, 0x8A, -65, 0x75, -35, 92, 0xD1, 0x90, -102, -76, 40, -21, 69, 93});
        SecretKeySpec v2 = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(v1.toCharArray(), v4, 0x10000, 0x100)).getEncoded(), "AES");
        Cipher v1_1 = Cipher.getInstance("AES/CBC/PKCS7Padding");
        v1_1.init(2, v2, v5);
        ClassLoader v2_1 = ClassLoaderSharing.INSTANCE.getLoader();
        if(v2_1 != null) {
            Class v1_2 = v2_1.loadClass(new String(v1_1.doFinal(Base64.decode("raTLFVkpCb4yP1YXsMdvqr2TjJSxtpiYA0yJLQ2UTPs=", 0)), Charsets.UTF_8));
            if(v1_2 != null) {
                v0 = v1_2;
            }
        }
        // call com.example.step_3.FinishImpl
        return v0.getConstructor(Activity.class).newInstance(this.b);
    }

Sooo, we go back again to the code in step_3.dex in the com.example.step_3.FinishImpl function.

The code is pretty long but in the end there are only 2 interesting methods :

onCreate

    public final void onCreate() {
        // create a view where we can draw something
        this.digitClassifier = new DigitClassifier(this.activity);
        DrawView v0 = (DrawView)this.activity.findViewById(R.id.draw_view);
        this.drawView = v0;
        if(v0 != null) {
            v0.setStrokeWidth(70f);
        }

        DrawView v0_1 = this.drawView;
        if(v0_1 != null) {
            v0_1.setColor(-1);
        }

        DrawView v0_2 = this.drawView;
        if(v0_2 != null) {
            v0_2.setBackgroundColor(0xFF000000);
        }

        this.nextButton = (Button)this.activity.findViewById(R.id.next_button);
        this.clearButton = (Button)this.activity.findViewById(R.id.clear_button);
        this.resetButton = (Button)this.activity.findViewById(R.id.reset_button);
       

This method is in charge of loading the layout for this new activity. However this layout is quite special has it isn’t from the official libraries and must be downloaded before https://github.com/divyanshub024/AndroidDraw

This layout allow us to draw something on our phone and to fetch the data as a Bitmap object.

The next operation done is to retrieve all the key and thus its value from the shared preference and to concatenate it

 SharedPreferences sharedpref = this.activity.getSharedPreferences("flag", 0);
        char v0_3;
        // concatenate all the key, value stored inside shared preference
        for(v0_3 = 'a'; v0_3 <= 122; v0_3 = (char)(v0_3 + 1)) {
            String v2 = sharedpref.getString(String.valueOf(v0_3), null);
            if(v2 != null) {
                this.password = this.password + v2;
            }
        }

        Button v0_4 = this.clearButton;
        if(v0_4 != null) {
            v0_4.setOnClickListener(new FinishImpl.onCreate.2(this));
        }

        Button v0_5 = this.nextButton;
        if(v0_5 != null) {
            v0_5.setOnClickListener(new FinishImpl.onCreate.3(this));
        }

        Button v0_6 = this.resetButton;
        if(v0_6 != null) {
            v0_6.setOnClickListener(new FinishImpl.onCreate.4(this));
        }

        this.digitClassifier.initialize();
    }

If you had done this challenge dynamically without patching anything you will have the correct letters set, else you will have missing item or junk ones inside your shared preferences file.

After a review of the smali with a lot of grep I noticed that only the following key were present :

  • a = LuKXSGlN5(%:Vk=alEbl9khIEPBo=mXu;hR7Ez7E"=
  • j = ECW_oe8%jXffkWul&#!V@tqB(:V%WP?JUKm@I(2KqIfv
  • q = ECW_AIU/yMZg3c7(NqGyqu8Iv3j8Oszx+1<>i'7&o(9g

The password is the concatenation in the alphabetically order.

Then it sets 3 buttons, one to clear, one to submit (next) and another to reset everything.

Finally it will call a method to initialize something inside the digitClassifier class.

classifyDrawing

The second function name is pretty explicit, its job is to classify the object drew on the view.

    private final void classifyDrawing() {
        Bitmap v0 = this.drawView == null ? null : this.drawView.getBitmap();
        if(v0 != null && (this.digitClassifier.isInitialized())) {
            int v1 = this.digitClassifier.getNumber(v0);
            System.out.println("recognized: " + v1);
            if(this.digitClassifier.verifyNext(v0, this.index)) {
                if(v1 == -1) {
                    Toast.makeText(this.activity, "An error happened", 0).show();
                }
                else {
                    this.pin = this.pin + v1;
                }
            }

            int v0_1 = this.index + 1;
            this.index = v0_1;
            if(v0_1 == 12) {
      [...]

It gathers the data on the draw view inside a bitmap object and then check if the class is successfully initialized. At this point we can guess that this.digitClassifier.getNumber(v0); will return the drew number. The next function this.digitClassifier.verifyNext has the bitmap object as first parameter and the second one an index which is incremented after each drawing.

This index value is verified, if its the 12th drawing we will call the following code :

              try {
                    String v0_3 = this.password + this.pin;
                    IvParameterSpec v1_1 = new IvParameterSpec(new byte[]{-101, 105, -107, 0x8A, -65, 0x75, -35, 92, 0xD1, 0x90, -102, -76, 40, -21, 69, 93});
                    SecretKeySpec v2 = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(v0_3.toCharArray(), new byte[]{56, -35, 0x77, 0x91, 71, 0x71, -83, 70, 0x89, 122, -92, 22, 0x7C, 23, -83, 110}, 0x10000, 0x100)).getEncoded(), "AES");
                    Cipher v0_4 = Cipher.getInstance("AES/CBC/PKCS7Padding");
                    v0_4.init(2, v2, v1_1);
                    Activity v1_2 = this.activity;
                    Intent v2_1 = new Intent();
                    v2_1.putExtra("end_flag", new String(v0_4.doFinal(Base64.decode("fEd6buSL5HmuH0pTdCJG4ZVCCn/bMC8bun44MKlw6mz2UrtH9Zhz3gMax4X8eGq5", 0)), Charsets.UTF_8));
                    v1_2.setResult(10, v2_1);
                    this.activity.finish();
                }
                catch(Exception v0_2) {
                    System.out.println("Wrong pin: " + this.pin);
                    this.index = 0;
                    this.pin = "";
                    Toast.makeText(this.activity, "Try again", 0).show();
                }
            }

So the password + the pin are the key to decrypt the final flag. We have the first part so we now have to analyze how the pin number are verified.

For this we gonna decompile the DigitClassifier class and especially the digitClassifier.verifyNext method.

The first method called is the initialize function if you remember, this function is calling lot of functions and end by calling initializeInterpreters

    private final void initializeInterpreters() throws IOException {
        AssetManager v1 = this.context.getAssets();
        char v0;
        for(v0 = 'a'; v0 < 109; v0 = (char)(v0 + 1)) {
            Interpreter v2 = new Interpreter(this.loadModelFile(v1, "mnist-" + v0 + ".tflite"), new Interpreter.Options());
            int[] v3 = v2.getInputTensor(0).shape();
            int v4 = v3[1];
            this.inputImageWidth = v4;
            int v3_1 = v3[2];
            this.inputImageHeight = v3_1;
            this.modelInputSize = v3_1 * (v4 * 4);
            this.interpreters.add(((Object)v2));
        }

        this.finalInterpreter = new Interpreter(this.loadModelFile(v1, "mnist.tflite"), new Interpreter.Options());
        this.isInitialized = true;
        Log.d("DigitClassifier", "Initialized TFLite interpreter.");
    }

Tensor Flow pattern matching

After some reading about TensorFlow and especially this code (which seems highly the source of the copy past of this challenge), I understood that the tflite files found while investigating in step 0 were indeed models created before in order to match the drawing of the user.

So this code is loading the 12 .tflite files and another one mnist.tflite. For each of these file it creates an interpreter which will be stored inside a list this.interpreters and the last one in this.finalInterpreter.

identifying the number drew

The function in charge of this, as stated before, is getNumber

    public final int getNumber(@NotNull Bitmap arg9) {
        Object new_greatest;
        if(this.isInitialized) {
            ByteBuffer v1 = this.convertBitmapToByteBuffer(Bitmap.createScaledBitmap(arg9, this.inputImageWidth, this.inputImageHeight, true));
            float[][] v6 = new float[][]{null};
            int v0;
            for(v0 = 0; v0 < 1; ++v0) {
                v6[v0] = new float[10];
            }

            this.finalInterpreter.run(v1, ((Object)v6));
            Iterator v7 = ArraysKt.getIndices(v6[0]).iterator();
            if(v7.hasNext()) {
                Object index_0 = v7.next();
                if(v7.hasNext()) {
                    float index_0_value = v6[0][((Number)index_0).intValue()];
                    Object v3;
                    for(v3 = index_0; true; v3 = new_greatest) {
                        Object cmp_value = v7.next();
                        float cmp_value_ = v6[0][((Number)cmp_value).intValue()];
                        if(Float.compare(index_0_value, cmp_value_) < 0) {
                            index_0_value = cmp_value_;
                            new_greatest = cmp_value;
                        }
                        else {
                            new_greatest = v3;
                        }

                        if(!v7.hasNext()) {
                            break;
                        }
                    }
                }
                else {
                    new_greatest = index_0;
                }
            }
            else {
                new_greatest = null;
            }

            Integer v0_2 = (Integer)new_greatest;
            return v0_2 == null ? -1 : ((int)v0_2);
        }

        throw new IllegalStateException("TF Lite Interpreter is not initialized yet.".toString());
    }

The way to determine the digit seems complicated but is indeed very simple.

To understand this you have to understand how works the function run of the interpreter. This function take only two parameters, the first one a ByteBuffer created thanks to the bitmap object and an array of float to store the result.

This function only use the finaInterpreter which is initialized with the mnist.tflite file the other 12 are not used here. Once the method is called the second parameter will hold 10 float value, these value represent the confidence of the algorithm to have detected the number. For example if we have :

-28.054903
-28.035492
-6.668576
-6.4378166
-19.293821
9.122528
-18.654612
-22.195911
-4.326079
-3.4205115

The function will return 5 as 9.122528 is the biggest value in the list and is at the 5th position.

However this is not the function which is verifying the pin. The function which actually do this work is verifyNext.

    public final boolean verifyNext(Bitmap bitmap, int index) {
        if(this.isInitialized) {
            ByteBuffer bytebuffer = this.convertBitmapToByteBuffer(Bitmap.createScaledBitmap(bitmap, this.inputImageWidth, this.inputImageHeight, true));
            Interpreter interpreter = (Interpreter)this.interpreters.get(index % this.interpreters.size());
            float[][] float_array = new float[][]{null};
            int i;
            for(i = 0; i < 1; ++i) {
                float_array[i] = new float[2];
            }

            interpreter.run(bytebuffer, ((Object)float_array));
            return float_array[0][1] > float_array[0][0];
        }

        throw new IllegalStateException("TF Lite Interpreter is not initialized yet.".toString());
    }

The function return a boolean which state if the number at a given index is good. The index is used to select the interpreter in the list created at initialization time.

The verification is pretty clear, we only have to deduce which number correspond to each of the 12 tflite files !

brute forcing each file

In order to see which tflite file holds which number model I decided to draw something and call the all 12 interpreters to see which of them can identify the number. If it can identify it, it will mean that we found it corresponding number as it could not identify another one.

Let’s create an application with android studio and load the step_3.dex. As you can’t load the dex file like this we need to create a jar with dex2jar

$ d2j-dex2jar step_3_decrypted.dex
dex2jar step_3_decrypted.dex -> ./step_3_decrypted-dex2jar.jar

Then put in in your libs directory and also puts all the tflite files in src/main/assets like this

image-20201027153843103

Then after having installed the Draw view package we can create our main function like this :

package com.example.myapplication;

[...]
import dalvik.system.PathClassLoader;
import org.tensorflow.lite.Interpreter.Options;
import org.tensorflow.lite.Interpreter;

public class MainActivity extends AppCompatActivity {

    Bitmap data;
    DrawView v0;
    Context ctx;
    Interpreter v2;
    DigitClassifier dc;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ctx = this.getApplicationContext();

        String password = "LuKXSGlN5(%:Vk=alEbl9khIEPBo=mXu;hR7Ez7E" + "ECW_oe8%jXffkWul&#!V@tqB(:V%WP?JUKm@I(2KqIfv" + "ECW_AIU/yMZg3c7(NqGyqu8Iv3j8Oszx+1<>i'7&o(9g";


        v0 = (DrawView)this.findViewById(R.id.draw_view);
        v0.setStrokeWidth(80f);
        v0.setColor(-1);
        v0.setBackgroundColor(0xFF000000);
        Button b = (Button)this.findViewById(R.id.button);

        dc = new DigitClassifier(this);
        dc.initialize();

        b.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {

                int nb = dc.getNumber(v0.getBitmap());
                System.out.println(nb); // detected number

                for(int i = 0; i < 12; i++) {
                    System.out.print(i + " ");
                    // call all interpreter to detect the number
                    System.out.println(dc.verifyNext(v0.getBitmap(), i));
                }

                v0.clearCanvas();
            }


        });

    }

}

We can now build the application and start to draw on the draw view.

As you can see in the debug output, the first 3 drew is recognized by the getNumber() function but by 0 other interpreter. I had to draw it a second time to have one interpreter, the one at index 0 to identify it.

I can conclude that the first digit of the pin is 3 and there is no other 3 in the pin. Then I drew a 5 which is identified at the index 5 and 8 so at the moment the pin look like : 3XXXX5XX5XXX

You can now repeat all the number until you get the whole pin : 314685405262.

Finally you can decrypt the final flag

        String password = "LuKXSGlN5(%:Vk=alEbl9khIEPBo=mXu;hR7Ez7E" + "ECW_oe8%jXffkWul&#!V@tqB(:V%WP?JUKm@I(2KqIfv" + "ECW_AIU/yMZg3c7(NqGyqu8Iv3j8Oszx+1<>i'7&o(9g";
        String pin = "314685405262";

        String v0_3 = password + pin;

        try {

            IvParameterSpec v1_1 = new IvParameterSpec(new byte[]{-101, 105, -107, (byte) 0x8A, -65, 0x75, -35, 92, (byte) 0xD1, (byte) 0x90, -102, -76, 40, -21, 69, 93});
            SecretKeySpec v2 = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(v0_3.toCharArray(), new byte[]{56, -35, 0x77, (byte) 0x91, 71, 0x71, -83, 70, (byte) 0x89, 122, -92, 22, 0x7C, 23, -83, 110}, 0x10000, 0x100)).getEncoded(), "AES");
            Cipher v0_4 = Cipher.getInstance("AES/CBC/PKCS7Padding");
            v0_4.init(2, v2, v1_1);

            String final_flag = new String(v0_4.doFinal(Base64.getDecoder().decode("fEd6buSL5HmuH0pTdCJG4ZVCCn/bMC8bun44MKlw6mz2UrtH9Zhz3gMax4X8eGq5")));
            System.out.println(final_flag);

        } catch (Exception e) {
            System.out.println(e);
        }

which is : ECW_50l8!3*ojKrfFHYCiLON+iDd5-0(4!iG04Y6U32L

I struggled a bit getting the 9 and 8 recognized as the training models are far from perfect.