BSidesSF CTF Weather Companion Writeup

This is the third in a series of writeups on challenges from the BSidesSF CTF. You can see a writeup of the first challenge, Blink, here and the second, Yay or Nay, here.

Weather Companion was the final mobile challenge in the CTF, this time worth 350 points! We’re provided with an apk file and a prompt that doesn’t set us up with much:

A simple weather application that fetches and displays the weather. What hides within?

Launching up the app we find that they were true to their word. A simple weather application:

Weather Companion MainActivity screenshot
The app as it first opens.

Given that the only information we have is that we are fetching the weather, I launched Charles Proxy to try and get some more information on the network request. It turned out that the request was using TLS and we couldn’t see the payload. Having had to go through the pain of getting the various keystores on an Android device and Charles Proxy working together before, I put this strategy off for now. The one thing I did learn was that the weather request was going to a Google owned IP address.

Next I decompiled the apk with jadx. The application was super obfuscated (presumably via ProGuard). All of the imported packages were renamed to single letters, but the main application was left easy to find.

Weather Companion decompiled files
The obfuscated file tree.

Going straight to the MainActivity we find an onCreate() method that instantiates an example.MyApplication.a object and calls a#execute().

public class MainActivity extends c {
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView((int) R.layout.activity_main);
        c().a((Toolbar) findViewById(R.id.toolbar));
        try {
            new a(this, getApplicationContext()).execute(new Void[0]);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

The a class is pretty long, I’ve removed some parts for brevity:

public final class a extends AsyncTask<Void, Void, String> {
    private String doInBackground() {
        String str2 = "weather-companion";
        String str3 = "weather.json";
        String str4 = null;
        try {
            Utils utils = new Utils(this.b);
            String a = Utils.a("LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ2JOYUpYN3FaMlNlYzQKNVc0aXIreVlYSjNJd2paOGZ3eXcwUFpTb2lUYi9pSnFUY0trL2x0alA2MVRySkhCNU1xS202VnovV0d3N0dTbQpuZDIxeE1GcWNsd3VHOE43Zit6aElLMFh1dlJCclMrY01FaHcwUmJITlViY08zWmFnaEtmZmFMVGh6UFk0eDJyCkhoL044bFVZaTRUQjhXQUdhQXpDSkgzcFVpOXJURzQrdWN4TzZwTU56My9FTnp5T1NtaWhSOVhiNU9rTVkvQXEKN05KN0JBd0g4b1NXUmxUbGxhQzhDaGw4d0RkdW42ZktXZ1lZbW1jQldYU2pyb3B1OGY2TVI3dk1NM0YwUEV1YQpZQnoxWnB4VnZZYzFpaU1TMFdpTnRBYTFaQnVXaFJlbkFpVGZ3T0N0NHJJU2ZiZ1lTcUNUU29LNXRPV1M1MnRNCg==", 0);
            String str5 = new String(utils.dks());
            String str6 = new String(utils.ss("CnoM+J76ft/1b1qCH4j4+/0h/EHG+iWbhzfijs5a7XHKINC8ach6YLbhAyUm9+mI\nGuGVAklGeeN5InzSubRcrL8BSobAIygkXw9aSioF2wj2TYMDYncA0KVLR0nkEAWf\nPFlp2YQLIIw0nowmk0PPSp0F+DXOtDP7i7xcFZWzVjy7ScWz/G9bnxqilMAmg3MH\n+QGEzCKu/JZ5p6w9iqmTXd3VyzUI/FFmIHsjDnkS8ImDyNv69seh/QFgG8u7PcTq\nYRm8F0dd7hoof7tBQX/MejFDui8qbrtMTh0agHEsnIY7NalXTWId2FYurIuZuWrM\nz94zTZR2BjXOtSKSFJpTETuZ/JkXTiNT0WJgTjMgawj+eNpEMSMNNcsSdVWSuKAr\n", 390));
            String a2 = Utils.a("M5VXSRDXNBVGCZDWOBUWCWJYG52FAK3BOIYU2VSYORSVGMLRINEW2YSHMZREO6KLJV3SWNLPKEXWY5KINRMHGOLWKFTUO53ILFGVCWAKNJCVGNTTJ5ITKNCJMREU24SPINEG4Q2WMZ5GWMDSONKHM22KPFFWQMKNGJDTANKDJFHUERRXJZUVSRZVKBSFEQSUGVAW6R2BIJCXOVAKIZHHEUTFIR5DSRDQIFYHGYLOINHGGV2ZHFIG6TKJMUZWYQZTKRJTIS3LG5WDG6KSJZHE44ZTONEGY5BXJZYVQ5DKPFKEKK2LHAZS6VIKJVQSWUCIMRYTAWKTJBBVCV2VGM2VE2DPOJCDOVCPHEZDERBTG5TUMRCZGA4DA6LDNBBEYTJZGZLGC42RKRGWKZSKMRCEQU2VJYYTO2IKLJZEURDBNR2WS6DQKA3S6YRQOFKGYUCCHFFDQ5KZNJEDE5DMIZLSWRSCIF2TS22DM5MUEY3IHBLHM6KCPJWEOYLZGVZDAN3KN5JGU5IKMNSG4VTGOJDUCVLELJJEY6CBGVUHESLBJN3EWRTZGI4EGYLBJNLC63C2JZETSTTNMRNG45DKGZQUSUJQOJ5FKV3JMRHVC43NNIZG6NAKG5LGGQLSNQ2VKTTVOJUHQNDVNBTTER2DNRRVKSKILE2VSZCFPBEGMRLVNJBHKNTVJVIWIWTKIJYFQOBXOVXWCZ3HIM3WU52KMFXFCQYKNRAXAVCQMMZTINZYM4XW22DPHFJTKZKEJV3T2PIKFUWS2LJNIVHEIICQKJEVMQKUIUQEWRKZFUWS2LJNBI======", 1);
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(a);
            stringBuilder.append(str5);
            stringBuilder.append(str6);
            stringBuilder.append(a2);
            a = stringBuilder.toString();
            StringBuilder stringBuilder2 = new StringBuilder("{\"type\": \"");
            stringBuilder2.append(utils.a.getResources().getString(R.string.type));
            stringBuilder2.append("\",");
            str5 = stringBuilder2.toString();
            StringBuilder stringBuilder3 = new StringBuilder();
            stringBuilder3.append(str5);
            stringBuilder3.append("\"project_id\": \"");
            stringBuilder3.append(Utils.a(utils.b));
            stringBuilder3.append("\",");
            str5 = stringBuilder3.toString();
            stringBuilder3 = new StringBuilder();
            stringBuilder3.append(str5);
            stringBuilder3.append("\"private_key_id\": \"");
            stringBuilder3.append(Utils.a());
            stringBuilder3.append("\",");
            str5 = stringBuilder3.toString();
            stringBuilder3 = new StringBuilder();
            stringBuilder3.append(str5);
            stringBuilder3.append("\"private_key\": \"");
            stringBuilder3.append(a.replace("\n", "\\n"));
            stringBuilder3.append("\",");
            a = stringBuilder3.toString();
            stringBuilder3 = new StringBuilder();
            stringBuilder3.append(a);
            stringBuilder3.append("\"client_email\": \"");
            stringBuilder3.append("[email protected]count.com");
            stringBuilder3.append("\",");
            a = stringBuilder3.toString();
            stringBuilder2 = new StringBuilder();
            stringBuilder2.append(a);
            stringBuilder2.append("\"client_id\": \"");
            stringBuilder2.append(BigInteger.valueOf(utils.gci()).multiply(BigInteger.valueOf(19)).multiply(BigInteger.valueOf(305363017965794407L)).toString());
            stringBuilder2.append("\",");
            a = stringBuilder2.toString();
            stringBuilder2 = new StringBuilder();
            stringBuilder2.append(a);
            stringBuilder2.append("\"auth_uri\": \"");
            stringBuilder2.append(new String(utils.ss("uggcf://nppbhagf.tbbtyr.pbz/b/bnhgu2/nhgu", 41)));
            stringBuilder2.append("\",");
            a = stringBuilder2.toString();
            stringBuilder2 = new StringBuilder();
            stringBuilder2.append(a);
            stringBuilder2.append("\"token_uri\": \"");
            stringBuilder2.append(new String(utils.ss("uggcf://bnhgu2.tbbtyrncvf.pbz/gbxra", 35)));
            stringBuilder2.append("\",");
            String stringBuilder4 = stringBuilder2.toString();
            StringBuilder stringBuilder5 = new StringBuilder();
            stringBuilder5.append(stringBuilder4);
            stringBuilder5.append("\"auth_provider_x509_cert_url\": \"");
            stringBuilder5.append(Utils.a("aHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vb2F1dGgyL3YxL2NlcnRz", 0));
            stringBuilder5.append("\",");
            stringBuilder4 = stringBuilder5.toString();
            stringBuilder5 = new StringBuilder();
            stringBuilder5.append(stringBuilder4);
            stringBuilder5.append("\"client_x509_cert_url\" : \"");
            stringBuilder5.append(Utils.a("aHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS93ZWF0aGVyLWNvbXBhbmlvbi1zZXJ2aWNlLWFjY28lNDBic2lkZXMtc2YtY3RmLTIwMTkuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20=", 0));
            stringBuilder5.append("\"}");
            stringBuilder4 = stringBuilder5.toString();
            URL a3 = gVar.a(c.a(str2, str3).a(), TimeUnit.DAYS, com.b.c.c.g.a.a(i.a(new ByteArrayInputStream(stringBuilder4.getBytes()))));
            str4 = a3.toString();
            HttpURLConnection httpURLConnection = (HttpURLConnection) a3.openConnection();
            httpURLConnection.connect();
            httpURLConnection.getContentLength();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream()));
            StringBuffer stringBuffer = new StringBuffer();
            while (true) {
                str = bufferedReader.readLine();
                if (str == null) {
                    break;
                }
                StringBuilder stringBuilder6 = new StringBuilder();
                stringBuilder6.append(str);
                stringBuilder6.append("\n");
                stringBuffer.append(stringBuilder6.toString());
            }
            str = stringBuffer.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (str4 != null) {
            Log.d("Background url", "Background URL in progress");
        }
        return str;
    }

    /* access modifiers changed from: protected|final|synthetic */
    public final /* synthetic */ void onPostExecute(Object obj) {
        String str = (String) obj;
        Log.d("Complete reponse", str);
        try {
            JSONObject jSONObject = new JSONObject(str);
            str = ((JSONObject) jSONObject.get("display_location")).getString("city");
            jSONObject = (JSONObject) jSONObject.get("current_weather");
            String string = jSONObject.getString("temperature");
            String string2 = jSONObject.getString("precipitation");
            String string3 = jSONObject.getString("humidity");
            String string4 = jSONObject.getString("wind");
            ((TextView) this.a.findViewById(R.id.cityName)).setText(str);
            ((TextView) this.a.findViewById(R.id.precipitationValue)).setText(string2);
            ((TextView) this.a.findViewById(R.id.humidityValue)).setText(string3);
            ((TextView) this.a.findViewById(R.id.windValue)).setText(string4);
            ((TextView) this.a.findViewById(R.id.temperatureValue)).setText(string);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}

We can see that the bulk of the work here is assembling a string into a URL, making a request to it, and parsing the response into the weather data. The URL is assembled from a few parts: dks() and ss() both call out to C functions (that we have not decompiled or disassembled yet) via JNI, String a is a base64 encoded value that decodes to the first half of a private key, and String a2 is another encoded value. After that there is an email address for a GCP service account, and 2 ROT13 encoded URLs that decode to Google APIs authentication endpoints. It’s pretty clear that the bulk of this is service account credentials.

The two options at this point are:

  1. Reverse each of the functions that go into obfuscating the credentials.
  2. Log the assembled credentials after the stringBuilder4 = stringBuilder5.toString(); line where it’s loaded into memory.

I went with the latter. I had a feeling that trying to recompile the obfuscated jadx output I had in front of me might be unnecessarily sadistic1 so I went up one level in the decompilation pipeline and used Apktool to only disassemble, not decompile, the apk. The big difference here is that after disassembly we don’t get java source code but smali, a human readable format of Dalvik bytecode.

The smali filetree is 1:1 with the java one we examined earlier with each java file having a smali equivalent. Unfortunately a.smali is about 6x the size of a.java. I searched the smali code to see what a call to the logging methods looks like and found this snippet:

const-string v1, "Background url"

const-string v2, "Background URL in progress"

invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

Focusing on the last line, a successful call to Log includes a reference to a String tag as the first argument and a log body as the second.

Next I looked for "\"}" which is the last thing appended to the credentials. I found the following smali code:

const-string v5, "\"}"

invoke-virtual {v6, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

invoke-virtual {v6}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

move-result-object v5

and added the line invoke-static {v2, v5}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I beneath it. The last assignment to v2 was "weather-companion" so it should make a good tag.

$ apktool b weather-companion
I: Using Apktool 2.3.4
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Copying libs... (/lib)
I: Building apk file...
I: Copying unknown files/dir...
I: Built apk...
$ keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days
	for: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
[Storing my-release-key.keystore]
$ jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore ./weather-companion/dist/weather-companion.apk alias_name
$ adb uninstall com.example.myapplication
Success
$ adb install ./weather-companion/dist/weather-companion.apk

You’ll notice that I uninstalled the app before installing the new version. If I hadn’t done that then Android would have noticed that the com.example.myapplication package on the device was signed by a different key than the new one and refuse to install it as a security measure. Alternatively, we could have changed the package name and have both installed at the same time.

$ adb logcat weather-companion:D \*:S
--------- beginning of system
--------- beginning of main
--------- beginning of crash
04-07 12:43:11.313 14365 14381 D weather-companion: {"type": "service_account","project_id": "bsides-sf-ctf-2019","private_key_id": "6dd7fc48a8b1d49edf7f03f74bc47713bec7d989","private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCbNaJX7qZ2Sec4\n5W4ir+yYXJ3IwjZ8fwyw0PZSoiTb/iJqTcKk/ltjP61TrJHB5MqKm6Vz/WGw7GSm\nnd21xMFqclwuG8N7f+zhIK0XuvRBrS+cMEhw0RbHNUbcO3ZaghKffaLThzPY4x2r\nHh/N8lUYi4TB8WAGaAzCJH3pUi9rTG4+ucxO6pMNz3/ENzyOSmihR9Xb5OkMY/Aq\n7NJ7BAwH8oSWRlTllaC8Chl8wDdun6fKWgYYmmcBWXSjropu8f6MR7vMM3F0PEua\nYBz1ZpxVvYc1iiMS0WiNtAa1ZBuWhRenAiTfwOCt4rISfbgYSqCTSoK5tOWS52tM\ntuzt/OVjAgMBAAECggEAC9T230UuI25W1huHXdWTb7n/vUIw7SSyTvhfDsWVkb+5\n1+i9od5SESrVh79sDR/n4NEkt8blH5ulwJ3gPO8W34qARHORX2TNJgxbpad231rY\nekuj+hW2atFA6aEO0K+Bw+7L7twrs6j8pgLR4d1LZ2ebYz2HWHWuI06s2pCNVNyM\nSMn593YgfmzNotaoJ3dHAwKG8PuNRHh0qDqqLnu0574dXkTFkEePcedyzza007Iy\nCoNTUYJxDO7aJbTQPVyqm7mOewh1SYUdHSofcfALlC9eT2nn6+c9qRgGO3AwbC8V\ns9TmMR9+DWfHrUmPN8AKB4YKov/QGUv29svI0d8YwQKBgQDTobsDSI96xm2s+NnE\nPabZ+W76sg/1o1dPU4w4+/0u/RUT+vJoumsvwf5n7KUXVAP8npu6LYouNlHz9+zV\nThTINxyTrrA5VamFhoEpeY8OFboNVltxKj9nFvbS2jw2GLZQLapN0XIYE0axRNJs\nCSyc2LDYVVj0abjzx0CCFc0S+QKBgQC7v7kpSMJmIwl7FpJm/T9oakdvyZNzt3ZU\n+DTRmPXh/WM5c6j9vdzGKq3IlmHV/SSzVUfwQaxF8VzQlAi69fru/DStT8h7CpGd\nLEz8S0qq7ubbs7gODK/ZrwSQhv8doegZGu0ntURfaVL7AnyKGJVq2SLheVhMhJeZ\nm94mGME2OwKBgFXFSWcGRGhM/WxKGvAG0JWtGwZtnjw+rAcRZFZAApfFqIJFhXNe\ngkyDwhjadvpiaY87tP+ar1MVXteS1qCImbGfbGyKMw+5oQ/luHlXs9vQgGwhYMQX\njES6sOQ54IdIMrOCHnCVfzk0rsTvkJyKh1M2G05CIOBF7NiYG5PdRBT5AoGABEwT\nFNrReDz9DpApsanCNcWY9PoMIe3lC3TS4Kk7l3yRNNNs3sHlt7NqXtjyTE+K83/U\nMa+PHdq0YSHCQWU35RhorD7TO922D37gFDY080ychBLM96VasQTMefJdDHSUN17i\nZrJDaluixpP7/b0qTlPB9J8uYjH2tlFW+FBAu9kCgYBch8VvyBzlGay5r07joRju\ncdnVfrGAUdZRLxA5hrIaKvKFy28CaaKV/lZNI9NmdZntj6aIQ0rzUWidOQsmj2o4\n7VcArl5UNurhx4uhg2GClcUIHY5YdExHfEujBu6uMQdZjBpX87uoaggC7jwJanQC\nlApTPc3478g/mho9S5eDMw==\n-----END PRIVATE KEY-----\n","client_email": "[email protected]count.com","client_id": "116037946827001874660","auth_uri": "https://accounts.google.com/o/oauth2/auth","token_uri": "https://oauth2.googleapis.com/token","auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url" : "https://www.googleapis.com/robot/v1/metadata/x509/weather-companion-service-acco%40bsides-sf-ctf-2019.iam.gserviceaccount.com"}

Loading the app and filtering the logs to the weather-companion tag spits the key right out! Using a similar smali trick to log the generated URL we can see that it’s a signed request to https://storage.googleapis.com/weather-companion/weather.json, a GCS bucket named weather-companion.

I authenticated the gcloud tool using my new service account credentials and started to poke around GCS. After a quick detour where we and the organizers realized that the flag wasn’t searchable 😅 all it took was a simple ls on the bucket!

$ gcloud auth activate-service-account [email protected]count.com --key-file=weather_priv_key.json
$ gsutil ls gs://weather-companion
gs://weather-companion/flag.txt
gs://weather-companion/weather.json

🎉

  1. While writing this post I decided to give this method a shot. I gave up quickly. 

BSidesSF CTF Yay Or Nay Writeup

This is the second in a series of writeups on challenges from the BSidesSF CTF. You can see a writeup of the first challenge, Blink, here

Yay Or Nay was the second mobile challenge in the CTF, this time worth 200 points. Like last time, we start out with a prompt and an apk file. This time the prompt came in a little more handy.

Keep track of places you would love / hate to see, by dropping markers with a simple click. Try YayorNay v1.2 today!

:::: Updated README :::: v 1.0 - Added short press, Yay support - Fix stability issues

v 1.1 - Added long press, Nay support - Add labels

v 1.2 - Populate from DB - Save to DB

To-do - Fix stability issues - Bug fixes - Implement feature to view by day

First things first, let’s launch the app.

$ adb install YayorNay.apk
Success

The app opens up with some instructions on how to use it and a button to get started.

YayOrNay MainActivity screenshot
The app as it first opens.

clicks get started

YayOrNay blocked screenshot
Rats

Well, looks like our root-enabled emulator image isn’t going to work out here. Let’s launch a Google Play Services enabled one! Unfortunately these images are a little more locked down and we won’t be able to (easily) get root on them.

YayOrNay map screenshot
Trying again on a fully googleified emulator

Ok, so we have a map of San Francisco with a bunch of markers. Let’s zoom around and see if anything sticks out. This may have been the worst part of the challenge, zooming and panning with an emulator can get tedious 😄

YayOrNay map screenshot
Aha!

Right in the middle of everything there’s a grid of some sort. It seems like the next step should be to isolate it. I know from the challenge prompt that these pins are being loaded from a database so I’ll go looking for the app’s sqlite db.

1. Find the package that contains our yayornay app.

$ adb shell pm list packages | grep yayornay
package:com.example.yayornay

2. Switch to that package’s user

$ adb shell
generic_x86:/ $ run-as com.example.yayornay

3. Find the app’s database

generic_x86:/data/data/com.example.yayornay $ ls
cache  code_cache  databases  files  shared_prefs
generic_x86:/data/data/com.example.yayornay $ cd databases
generic_x86:/data/data/com.example.yayornay/databases $ ls
Location.db  Location.db-journal

4. List the tables in that database

generic_x86:/data/data/com.example.yayornay/databases $ sqlite3 Location.db
SQLite version 3.18.2 2017-07-21 07:56:09
Enter ".help" for usage hints.
sqlite> .tables
android_metadata  locations

5. Inspect the table schema

sqlite> .schema locations
CREATE TABLE IF NOT EXISTS "locations" (
	`date`	TEXT,
	`latitude`	REAL,
	`longitude`	REAL,
	`color`	REAL
);

We can see that the database has a list of lat,long pairs each with a date and a color. My first guess is that these correspond to the pins we saw on the map. Let’s dump the data and see what we get.

sqlite> SELECT * FROM locations LIMIT 5;
02/03/2019|37.7842927|-122.4053593|120.0
02/03/2019|37.7838412|-122.4041845|0.0
02/07/2019|37.7863323436302|-122.42828886956|120.0
02/07/2019|37.7851367932719|-122.402353584766|120.0
02/07/2019|37.782343920755|-122.404699847102|0.0

Looks like a list of dates, coordinates in and around San Francisco, and the hues for green (120) and red(0)! The next thing I did was go off of the prompt Bug fixes - Implement feature to view by day and check each day one by one.

generic_x86:/data/data/com.example.yayornay/databases $ cp Location.db Location.db.bak
generic_x86:/data/data/com.example.yayornay/databases $ echo "delete from locations where date!='02/03/2019';" | sqlite3 Location.db
generic_x86:/data/data/com.example.yayornay/databases $ echo "select distinct date from locations;" | sqlite3 Location.db
02/03/2019

Back up the database, delete any records that don’t match a given date, reload the app, restore the database, and repeat! Soon enough, on 02/08/2019 we see:

YayOrNay grid screenshot
The isolated grid

At this point I had more or less no idea what I was looking at. Luckily a teammate connected the dots (pun intended) between a grid 3 rows high and braille!

Braille sheet
A handy dandy braille glyph sheet

Using the green pins as raised points, we can decode the flag to Z3lda!

BSidesSF CTF Blink Writeup

The BSidesSF CTF happened about a week ago! It was the first CTF I’ve tried to compete in and I had a lot of fun on the team. This is the first in a series of writeups on the challenges I participated in.

Blink was the first mobile challenge in the event and served as a good introduction. The goal of the mobile challenges is to find a string (the “flag”) using clues hidden in an app. Often the flag is in the app binary itself, but sometimes the challenge may lead you elsewhere afterwards. Blink was a relatively easy “101” challenge, only worth 50 points (the most difficult challenges in the CTF were worth 600 or more).

Analysis

The first step in any challenge to open the prompt. It contained the text Get past the Jedi mind trick to find the flag you are looking for. and a link to an apk file (Android app). I booted up an Android Emulator and used a simple adb command to install the app.

$ adb install blink.apk
Success

Opening the app presents us with:

Blink MainActivity screenshot
The app as it first opens.

If you aren’t familiar with Android, an activity is similar to a window in a traditional desktop app or a page on a website. The hint is essentially saying we need to navigate to a different page.

The first place we should check for activities is the AndroidManifest.xml file. All of the accessible activities will be listed there. I used Apktool to disassemble the app.

$ apktool d ./blink.apk
I: Using Apktool 2.3.4 on blink.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /var/folders/8j/h8wpd_757p729cx_91j50g2m0000gn/T/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

Then I opened up the Android Manifest..

<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.blink" platformBuildVersionCode="1" platformBuildVersionName="1.0">
    <application android:allowBackup="true" android:debuggable="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme">
        <activity android:label="@string/title_activity_r2d2" android:name="com.example.blink.r2d2" android:theme="@style/AppTheme.NoActionBar"/>
        <activity android:name="com.example.blink.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

Here we can see two activities: com.example.blink.MainActivity and com.example.blink.r2d2. We know that MainActivity is where we saw Obi Wan earlier because it has an intent filter that makes it the default activity. The next step is to launch the r2d2 activity. Let’s whip out adb again and see what we can do.

$ adb shell am start -n com.example.blink/.r2d2

Starting: Intent { cmp=com.example.blink/.r2d2 }
Security exception: Permission Denial: starting Intent { flg=0x10000000 cmp=com.example.blink/.r2d2 } from null (pid=21352, uid=2000) not exported from uid 10089

Well, that didn’t work. Looks like we don’t have permission to launch this activity. Let’s try something else!

xkcd 149

Because my emulator is running a build of Android without Google Play Services in it, we can easily assume the root user.

$ adb root
restarting adbd as root

$ adb shell am start -n com.example.blink/.r2d2
Starting: Intent { cmp=com.example.blink/.r2d2 }

And it worked!

Blink r2d2 screenshot

The flag was CTF{PUCKMAN}. The end! Tons of credit to @itsC0rg1 for putting the puzzle together, and BSidesSF for hosting.