Qropolis Writeup

By
Anandhu K A
Published on
20 Sep 2020
6 min read
DOMECTF2020

I’ll give you a walkthrough of the Qropolis challenge that was part of DOME CTF 2020. Let’s break it down step by step.

So for this challenge you’re provided with an apk file and the challenge description goes like this: “Vardy received a note from one of his old friend, Jamie that says, “I’m leaving this app with you. Please make me proud.”

So, for solving this challenge you’ll have to first decompile the apk file. You can use a tool like Apk decompiler online for that.

Once you decompile the apk, you can view the files in it.

Go to androidmanifest.xml

        <?xml version="1.0" encoding="utf-8"?>
        <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="29" android:compileSdkVersionCodename="10" package="com.dom.qropolis" platformBuildVersionCode="29" platformBuildVersionName="10">
        <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="29"/>
        <uses-feature android:name="android.hardware.camera" android:required="true"/>
        <uses-permission android:name="android.permission.CAMERA"/>
        <uses-feature android:name="android.hardware.camera.autofocus"/>
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
        <uses-permission android:name="android.permission.INTERNET"/>
        <uses-permission android:name="android.permission.CAMERA"/>
        <application android:theme="@style/AppTheme" android:label="@string/app_name" android:icon="@drawable/scan" android:allowBackup="true" android:supportsRtl="true" android:roundIcon="@drawable/scan" android:appComponentFactory="androidx.core.app.CoreComponentFactory">
            <activity android:name="com.dom.qropolis.CameraActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"/>
                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
            </activity>
                <activity android:theme="@style/Theme.Translucent.NoTitleBar" android:name="com.google.android.gms.common.api.GoogleApiActivity" android:exported="false"/>
            <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version"/>
        </application>
        </manifest>
    

From the manifest file it is clear that there is only one activity named CameraActivity for this app.

So, now we have an idea that the application is related to a camera related activity.

Open CameraActivity.java

Goto function onCreate(), from there we can see onCreate is calling initViews();

Goto intiViews

        public void onCreate(Bundle bundle) {
            super.onCreate(bundle);
            requestWindowFeature(1);
            getWindow().setFlags(1024, 1024);
            setContentView(C0392R.layout.activity_2);
            initViews();
        }
    
        private void initViews() {
            this.txt1 = (TextView) findViewById(C0392R.C0394id.txt1);
            TextView textView = (TextView) findViewById(C0392R.C0394id.txt);
            this.txt = textView;
            try {
                textView.setTypeface(ResourcesCompat.getFont(getApplicationContext(), C0392R.font.montserrat));
            } catch (Exception e) {
                Log.d("exception", e.getMessage());
            }
            this.f61v = (SurfaceView) findViewById(C0392R.C0394id.f65v);
            Button button = (Button) findViewById(C0392R.C0394id.btn);
            this.f59b = button;
            button.setOnClickListener(new View.OnClickListener() {
                public void onClick(View view) {
                    try {
                        new CryptUtil();
                        if (!CameraActivity.this.txt.getText().equals((Object) null)) {
                            if (CameraActivity.this.txt.getText().length() != 0) {
                                if (CryptUtil.decrypt(CameraActivity.this.txt.getText().toString()).startsWith("domectf")) {
                                    CameraActivity.this.txt.setVisibility(4);
                                    CameraActivity.this.txt1.setVisibility(0);
                                    CameraActivity.this.txt1.setText(CryptUtil.decrypt(CameraActivity.this.txt.getText().toString()));
                                    Toast.makeText(CameraActivity.this.getApplicationContext(), "This may looks like a flag..", 0).show();
                                    return;
                                }
                                return;
                            }
                        }
                        Toast.makeText(CameraActivity.this.getApplicationContext(), "invalid input", 0).show();
                    } catch (Exception e) {
                        Toast.makeText(CameraActivity.this.getApplicationContext(), "invalid input", 0).show();
                        Log.d("exception", e.getMessage());
                    }
                }
            });
            initialiseDetectorsAndSources();
        }

    

From initViews() we can see that all initialization of the variable occurs here.

Then when you check button click listener, you can see that when onclick textview “txt” is read and then it is decoded from CryptUtil function.

After that if the decode string starts with “domectf”, the decoded text returns a toast notification that shows “This may look like a flag.”

And if the decoded string does not satisfy the condition, then “invalid input ” is shown as the toast notification.

Now let us search where txt, the textview is assigned a text.

        private void initialiseDetectorsAndSources() {
            this.code = new BarcodeDetector.Builder(this).setBarcodeFormats(0).build();
            this.f60c = new CameraSource.Builder(this, this.code).setRequestedPreviewSize(1920, 1080).setAutoFocusEnabled(true).build();
            this.f61v.getHolder().addCallback(new SurfaceHolder.Callback() {
                public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i2, int i3) {
                }
        
                public void surfaceCreated(SurfaceHolder surfaceHolder) {
                    try {
                        if (ActivityCompat.checkSelfPermission(CameraActivity.this, "android.permission.CAMERA") == 0) {
                            CameraActivity.this.f60c.start(CameraActivity.this.f61v.getHolder());
                            return;
                        }
                        ActivityCompat.requestPermissions(CameraActivity.this, new String[]{"android.permission.CAMERA"}, CameraActivity.PERMISSION);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
        
                public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
                    CameraActivity.this.f60c.stop();
                }
            });
            this.code.setProcessor(new Detector.Processor<Barcode>() {
                public void release() {
                    Toast.makeText(CameraActivity.this.getApplicationContext(), "To prevent memory leaks scanner has been stopped", 0).show();
                }
        
                public void receiveDetections(Detector.Detections<Barcode> detections) {
                    final SparseArray<Barcode> detectedItems = detections.getDetectedItems();
                    if (detectedItems.size() != 0) {
                        CameraActivity.this.txt.post(new Runnable() {
                            public void run() {
                                if (((Barcode) detectedItems.valueAt(0)).email != null) {
                                    CameraActivity.this.txt.removeCallbacks((Runnable) null);
                                    CameraActivity.this.txt.setText(((Barcode) detectedItems.valueAt(0)).email.address);
                                    return;
                                }
                                CameraActivity.this.txt.setText(((Barcode) detectedItems.valueAt(0)).displayValue);
                            }
                        });
                    }
                }
            });
        }
    

This is the function where the values for txt, textview is assigned. By looking at the function, it is clear that the android app scans for barcodes, and the values are stored in textview.

From the above code, we know the application scans and decodes the code using CryptUtil class. There are no other clues about where the barcode is stored.

Let us assume the barcode is hidden inside the apk file.

Search for all images files in the apk file.

Using terminal search all imagetypes

qropolis1

Then you will get a list of all the images stored in the apk file.

Clearly, there are a huge number of images stored inside the apk. Opening these images and looking for a barcode is a very time consuming process. But closely watching these image names, we can see that there is a .ttf file.

qropolis2

All you have to do now is rename the montserrat.ttf file to .webp. Then open the app again and scan the QR code present in it.

qropolis3

You will get the flag once you scan the QR code.

Automated human-like penetration testing for your web apps & APIs
Teams using Beagle Security are set up in minutes, embrace release-based CI/CD security testing and save up to 65% with timely remediation of vulnerabilities. Sign up for a free account to see what it can do for you.

Written by
Anandhu K A
Anandhu K A
Lead Engineer
Find website security issues in a flash
Improve your website's security posture with proactive vulnerability detection.
Free website security assessment
Experience the power of automated penetration testing & contextual reporting.