Sunday 5 November 2017

CTF Writeup - Flare-On 2017 - 08: flair.apk


  • Name - flair.apk
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

Next up is an Android challenge. To install the APK file I've created a Marshmallow Android x86 emulated phone using 'Visual Studio Emulator for Android' and installed it using adb:


To debug the APK file I decided to use JEB Android Debugger. This debugger is amazing and really easy to use as you'll soon find out in this blog post. It also allows you to debug applications which are NOT marked as debuggable such as flair.apk.

Load the APK in JEB and view the decoded manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.flare_on.flair" platformBuildVersionCode="25" platformBuildVersionName="7.1.1" xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="25" />
    <meta-data android:name="android.support.VERSION" android:value="25.3.1" />
    <application android:allowBackup="true" android:icon="@mipmap/flair_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme">
        <activity android:name="com.flare_on.flair.Chotchkies" android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:exported="false" android:label="@string/michael_title" android:name="com.flare_on.flair.Michael" android:screenOrientation="portrait" />
        <activity android:exported="false" android:label="@string/brian_title" android:name="com.flare_on.flair.Brian" android:screenOrientation="portrait" />
        <activity android:exported="false" android:label="@string/milton_title" android:name="com.flare_on.flair.Milton" android:screenOrientation="portrait" />
        <activity android:exported="false" android:label="@string/printer_title" android:name="com.flare_on.flair.Printer" android:screenOrientation="portrait" />
        <meta-data android:name="vdf" android:value="cov" />
    </application>
</manifest>

The manifest shows the main class that is called when the application is launched: 'com.flare_on.flair.Chotchkies'. This class reveals that we have 4 hurdles to overcome before we are presented with the key:
private void getFlair(int arg3) {
    Intent v0 = null;
    switch(arg3) {
        case 0: {
            v0 = new Intent(((Context)this), Michael.class);
            break;
        }
        case 1: {
            v0 = new Intent(((Context)this), Brian.class);
            break;
        }
        case 2: {
            v0 = new Intent(((Context)this), Milton.class);
            break;
        }
        case 3: {
            v0 = new Intent(((Context)this), Printer.class);
            break;
        }
    }
    this.startActivityForResult(v0, arg3);
}

So let's get started.

Michael




The Michael activity contains a checkPassword() function which validates the input:

private boolean checkPassword(String arg7) {
    boolean v0;
    int v5 = 9;
    int v4 = 7;
    int v3 = 5;
    if((arg7.isEmpty()) || arg7.length() != 12) {
        v0 = false;
    }
    else {
        v0 = true;
        if(!arg7.startsWith("M")) {
            v0 = false;
        }

        if(arg7.indexOf(89) != 1) {
            v0 = false;
        }

        if(!arg7.substring(2, v3).equals("PRS")) {
            v0 = false;
        }

        if(arg7.codePointAt(v3) != 72 || arg7.codePointAt(6) != 69) {
            v0 = false;
        }

        if(arg7.charAt(v4) != arg7.charAt(8) || arg7.substring(v4, v5).hashCode() != 3040) {
            v0 = false;
        }

        if(arg7.indexOf("FT") != v5) {
            v0 = false;
        }

        if(arg7.lastIndexOf(87) == arg7.length() - 1) {
            return v0;
        }

        v0 = false;
    }

    return v0;
}

The only string that will make it through all the if statements is 'MYPRSHE__FTW'

Brian




The important functions in the Brian class are the following:


[... snip ...]

public void onClick(View arg5) {
    switch(arg5.getId()) {
        case 2131427424: {
            String v2 = this.q.getText().toString();
            if(this.teraljdknh(v2, this.asdjfnhaxshcvhuw(this.findViewById(2131427422), this.findViewById(2131427423)))) {
                Util.flairSuccess(((Activity)this), v2);
            }
            else {
                Util.flairSadness(((Activity)this), this.tEr);
                ++this.tEr;
            }

            break;
        }
    }
}

[... snip ...]

private boolean teraljdknh(String arg2, String arg3) {
    return arg2.equals(arg3);
}

If this.teraljdknh() returns True, then we go to Util.flairSuccess() otherwise we branch to Util.flairSadness(). Since teraljdknh() is just a string equals function, we can put a breakpoint when it is called and peek at the parameters:


As soon as we hit the breakpoint, JEB shows us the local parameters and their values. The ones we're interested in are v2, which contains our input, and v3, which contains the expected input, as these are passed to teraljdknh(). The default type for local variables is 'int' so make sure to change it to 'string' to view the result.

The expected input for this puzzle is therefore 'hashtag_covfefe_Fajitas!'

Milton




The first things to notice are 1) The 5-star rating system and 2) the button to advance to the next level is disabled. Let's look at the Milton class. The following in an excerpt of the relevant sections:


[ ... snip ... ]

public void onRatingChanged(RatingBar arg6, float arg7, boolean arg8) {
    if(arg8) {
        if((((double)arg7)) == 4) {
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("JP+98sTB4Zt6q8g=", 56, "State");
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("rh6HkuflHmw5Rw==", 96, "Chile");
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("+BNtTP/6", 118, "eagle");
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("oLLoI7X/jIp2+w==", 33, "wind");
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("w/MCnPD68xfjSCE=", 148, "river");
            Milton.this.r.setEnabled(true);
        }

        arg6.setEnabled(false);
    }
}

[ ... snip ... ]

private boolean breop(String arg5) {
    boolean v2 = false;
    if(!Milton.trsbd(((Context)this))) {
        byte[] v1 = Stapler.neapucx(arg5);
        try {
            v2 = Arrays.equals(v1, this.nbsadf());
        }
        catch(NoSuchAlgorithmException v0) {
            v0.printStackTrace();
        }
        catch(UnsupportedEncodingException v0_1) {
            v0_1.printStackTrace();
        }
    }

    return v2;
}

[ ... snip ... ]

public void onClick(View arg3) {
    switch(arg3.getId()) {
        case 2131427439: {
            String v0 = this.pexu.getText().toString();
            if(this.breop(v0)) {
                Util.flairSuccess(((Activity)this), v0);
            }
            else {
                this.vbdrt();
                Util.flairSadness(((Activity)this), this.rtgb);
                ++this.rtgb;
            }

            break;
        }
    }
}

[ ... snip ... ]


The onRatingChanged() function is called when the rating on the 5-star scale has been selected. If the rating is 4 (line 5), the button is enabled (line 11). Now we can continue as usual.

In the onClick() function, if this.breop() returns True, then we go to Util.flairSuccess() otherwise we branch to Util.flairSadness(). To ensure that breop() returns True, Array.equals() has to return True as well.

Let's put a breakpoint on Array.equals(), input '123456' and hit the button:


The function Array.equals() is comparing v1 and v3. I had to manually change the type to [B to get them to display an array of bytes. The value of v1 in the extra column is [0x12, 0x34, 0x56] which is our input so v3 must hold the required input.

Disregarding the sign of the byte we get: '10AEA594831E0B42B956C578EF9A6D44EE39938D'

Printer




This is the last hurdle to overcome and the most complicated one. Nothing that JEB cannot handle though! The important function here is cgHbC(); if it returns True we get to Util.flairSuccess():

private boolean cgHbC(String arg15) {
    try {
        if(Printer.ksdc(this)) {
            boolean v9 = false;
            return v9;
        }

        Object v2 = this.wJPBw(Stapler.iemm("Gv@H"));
        Class v4 = Class.forName(Stapler.iemm(",e}e8yGS!8Dev)-e@"));
        int v5 = v4.getMethod(Stapler.iemm("vSBH"), null).invoke(v2, null).intValue();
        byte[] v6 = new byte[v5];
        Method v7 = v4.getMethod(Stapler.iemm("LHG"), Object.class);
        short v0;
        for(v0 = 0; v0 < v5; v0 = ((short)(v0 + 1))) {
            v6[v0] = Byte.class.cast(v7.invoke(v2, Short.valueOf(v0))).byteValue();
        }

        return Class.forName(Stapler.iemm(",e}e8yGS!81PPe(v")).getMethod(Stapler.iemm("H?ye!v"), byte[].class, byte[].class).invoke(null, Stapler.neapucx(arg15), Stapler.poserw(v6)).booleanValue();
    }
    catch(Exception v1) {
        v1.printStackTrace();
        return false;
    }
}

The function is highly obfuscated. Notice though that arg15, which is our input, is only used on line 18. If we put a breakpoint at the end of function Stapler.poserw() to try and make sense of this line of code we get:

return Class.forName("java.util.Arrays").getMethod("equals"), byte[].class, byte[].class).invoke(null, Stapler.neapucx(arg15), Stapler.poserw(v6)).booleanValue();

So this is a simple Array comparison between our input, arg15, and Stapler.poserw(v6).

Putting a breakpoint at the end of Stapler.poserw(v6), changing the type of v1 to [B and collecting the hex values from the Extra column as we did before we get '5F1BE3C9B081C40DDFC4A0238156008EE71E24A4'.

Piecing It Together


At this point we have the 4 correct inputs:
  1. MYPRSHE__FTW
  2. hashtag_covfefe_Fajitas!
  3. 10AEA594831E0B42B956C578EF9A6D44EE39938D
  4. 5F1BE3C9B081C40DDFC4A0238156008EE71E24A4
Inputting these we get:

No comments:

Post a Comment