32C3 CTF – Android Reverse-Engineering libdroid

January 9, 2016

I unfortunately didn’t have time to participate to the CCC CTF this year, but I wanted to look at the android reverse challenge and see if I could solve it using the Xposed Framework. So here we go, same toolkit as last time, Jadx, Genymotion and Android Studio (see here)

Firing up an emulator with API 23 and starting the app shows a keypad with cute little smileys. After clicking on “bear”, “bear”, “ghost”, “monkey”,”heart”, “burger”, a message appears, “no rootkit for you”. Same happens after pressing the “get flag” button.

app

So let’s open the app in jadx. There we can see an obfuscated class with native calls

    public static native String generateConfusion();

    public static native String getFlag();

    public static native String getOperatingSystem();

    public static native String getPhoneNumber();

    public static native String installRootkit();

    public static native String installiOS();

    public static native String obtainWorldDomination();

    private static native void phoneHome(byte[] bArr, byte[] bArr2);

    static {
        System.loadLibrary("libdroid");
        a = a(getOperatingSystem(), 1);
        b = a(getPhoneNumber(), 1);
        c = a(installRootkit(), 1);
        d = a(generateConfusion(), 1);
        f = a(obtainWorldDomination(), 1);
        g = a(installiOS(), 1);
        flag = a(getFlag(), 1);
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView((int) R.layout.activity_a);
        try {
            a(a);
        } catch (Exception e) {
        }
        this.e = BuildConfig.FLAVOR;
    }

    void a(String a) throws Exception {
        InputStream b = getAssets().open(a);
        ByteArrayOutputStream b2 = new ByteArrayOutputStream();
        byte[] data = new byte[AccessibilityNodeInfoCompat.ACTION_COPY];
        while (true) {
            int nRead = b.read(data, 0, data.length);
            if (nRead == -1) {
                break;
            }
            b2.write(data, 0, nRead);
        }
        b2.flush();
        BufferedReader b4 = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(a(b2.toByteArray(), b))));
        while (true) {
            String c = b4.readLine();
            if (c != null) {
                if (c.startsWith(g)) {
                    g = c.substring(g.length());
                }
                if (c.startsWith((String) f)) {
                    f = Base64.decode(c.substring(((String) f).length()), 0);
                }
            } else {
                return;
            }
        }
    }

So when the app is starting, some static values are initialized and then modified again in the a(a) method.
The interesting part is the click event handler a(View v).


public void a(View v) {
        if (v.getId() == R.id.button) {
            this.e += d.charAt(1);
        }
        if (v.getId() == R.id.button2) {
            this.e += d.charAt(2);
        }
        if (v.getId() == R.id.button3) {
            this.e += d.charAt(3);
        }
        if (v.getId() == R.id.button4) {
            this.e += d.charAt(4);
        }
        if (v.getId() == R.id.button5) {
            this.e += d.charAt(5);
        }
        if (v.getId() == R.id.button6) {
            this.e += d.charAt(6);
        }
        if (v.getId() == R.id.button7) {
            this.e += d.charAt(7);
        }
        if (v.getId() == R.id.button8) {
            this.e += d.charAt(8);
        }
        if (v.getId() == R.id.button9) {
            this.e += d.charAt(9);
        }
        if (v.getId() == R.id.button10) {
            this.e += d.charAt(0);
        }
        if (this.e.length() == 6 || v.getId() == R.id.button11) {
            CharSequence flag = flag;
            try {
                InputStream b = getAssets().open(g);
                ByteArrayOutputStream b2 = new ByteArrayOutputStream();
                byte[] data = new byte[AccessibilityNodeInfoCompat.ACTION_COPY];
                while (true) {
                    int nRead = b.read(data, 0, data.length);
                    if (nRead == -1) {
                        break;
                    }
                    b2.write(data, 0, nRead);
                }
                b2.flush();
                byte[] j = b2.toByteArray();
                byte[] f_ = new byte[16];
                System.arraycopy((byte[]) f, 0, f_, 0, ((byte[]) f).length);
                System.arraycopy(this.e.getBytes(), 0, f_, 10, this.e.getBytes().length);
                phoneHome(j, f_);
                if (new String(j).startsWith(c)) {
                    flag = new String(j);
                }
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            Snackbar.make(v, flag, 0).setAction((CharSequence) "Action", null).show();
            this.e = BuildConfig.FLAVOR;
        }
    }

So everytime a key is pressed, a new character will be appended to the attribute “e”. If its length is 6 or the “get flag” button is pressed, some magic is done and a result is displayed in a message using a Snackbar.

Let’s bruteforce it! There are only 999999 possibilities.
The idea is then to set the variable “e” and simulate a click on the “get flag” button.

We first need to find the charset for “e”, ie we need the value of “d” (this.e += d.charAt(x);). We have seen above that “d” is set in the onCreate method, so let’s hook it


findAndHookMethod("ctf.stratumauhhur.libdroid.a", lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() {

	@Override
	protected void afterHookedMethod(MethodHookParam param) throws Throwable {
	    String d = (String) XposedHelpers.getObjectField(param.thisObject,"d");
	    XposedBridge.log("d= " + d);
	}
});

Running the app returns

I/Xposed: d=  1234567890

So the charset is only digits, isn’t it? Wrong, wrong, wrong! (it did take me some time to realize my mistake…) It’s not “1234567890” but ” 123456789″…

Now we are nearly ready to start the brute force. We need somehow to log the popup message to get the flag. So let’s hook the make() method of the snackbar and log everything that doesn’t start with the default error message:

findAndHookMethod("android.support.design.widget.Snackbar", lpparam.classLoader, "make", View.class, CharSequence.class, int.class, new XC_MethodHook() {

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        if (!((String) param.args[1]).startsWith("Sorry")) {
            XposedBridge.log("message= " + param.args[1]);
        }
    }
});

Here the pseudo-code for the brute force

String formatted = String.format("%06d", cnt.getAndIncrement());
if (formatted.endsWith("00000")) {
    XposedBridge.log("Count= " + formatted);
}
formatted = formatted.replaceAll("0"," ");
XposedHelpers.setObjectField(object, "e", formatted);
XposedHelpers.callMethod(object, "a", p.args[0]);

And then we start the app and press the “get flag” button to start the brute force. In less than 2 minutes, we get the flag

flag

Thanks again 0x3cute for showing me this framework!

Leave a Reply




Get Adobe Flash player