H1702 CTF

Sun, Jul 16, 2017

Writeups #ctf #h1702ctf #2017 #reversing #mobile

Welcome to the giant, rather disorganized, all-in-one writeup for all the challenges I completed during the H1702 CTF this year. I was able to complete all of the levels except for the 6th for both Android and iOS.

Foreword

These will likely be updated again as these are pretty much copied 1 for 1 from the writeups I submitted along with the CTF flags. Ideally I would like to add in some screenshots, go more in-depth for the iOS writeups, and make some other minor changes.

Challenge Files

Android

  • Levels 1-4 (SHA1: 490954d49dd51911bc730d8161541cf13e7416f9)
  • Level 5 (SHA1: 8d51e73cf81c0391575de7b40226f19645777322)
  • Level 6 (SHA1: 6118c10be480b994654a1f01cd322af2df2ceab6)

iOS

  • Levels 1-4 (SHA1: 727e07e27199b5431fccc16850d67c4fea6596f7)
  • Level 5 (SHA1: 69c2713162cb8f5e9418f8c08f3fa0a1ecb4928d)
  • Level 6 (SHA1: f0887a253daaa02e584bc9ff4edfeca1300887dc)

Hints

Android

  • Level 1: “Let’s start you off with something easy to get you started.”
  • Level 2: “Maybe something a little more difficult?”
  • Level 3: “Think you can solve level 3?”
  • Level 4: “Hope you kept your notes.”
  • Level 5: “Hmmm… looks like you need to get past something…”
  • Level 6: “I can’t think of anything creative… just try to solve this one :)”

iOS

  • Level 1: “WAKE ME UP, WAKE ME UP INSIDE. SAVE ME!!!!!”
  • Level 2: “And he prays…”
  • Level 3: “Rock, paper, scissors is so juvenile. Play rock, paper, scissors, lizard, Spock!”
  • Level 4: “Use your flags from levels 1, 2, and 3 to do the thing!”
  • Level 5: “Looks like this thing is pretty locked down, I don’t think you can touch this.”
  • Level 6: “Hey look at me im Tiny Rick! Yeah now that I got your attention, I got this app here that Squanchy squanched on my phone. Looks like there is something in there… But I don’t give a @#$! I’m Tiny Rick!”

Android Level 1

Decompilation

First, I decompiled the application fully. To do this, I used apktool, dex2jar, and procyon.

The flow went something like this:

# extract the APK with apktool to automatically decompile the resources and such
java -jar apktool_2.2.2.jar d ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk

# extract the classes.dex file from the APK for dex2jar/procyon
unzip -p ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk classes.dex > ctfone-490954d49dd51911bc730d8161541cf13e7416f9/classes.dex

# change into the directory
cd ctfone-490954d49dd51911bc730d8161541cf13e7416f9/

# convert classes.dex to a jar file with dex2jar
./dex2jar-2.1-SNAPSHOT/d2j-dex2jar.sh classes.dex

# use procyon to convert the jar file into a src folder
java -jar procyon-decompiler-0.5.30.jar -o src -ss classes-dex2jar.jar

Importing the Project

To make my life significantly simpler, I imported the decompiled app into Android Studio. The reason for this is that it would allow for a much more streamlined flow for understanding where everything was located within the application and would allow for me to re-build any missing parts of the decompiled app through reverse engineering and analysis. After importing, I needed to remove any assets that were in the decompiled version that were part of the stock android assets like layouts, animations, stock drawables, etc. Once I had removed these and beautified the code and XML, I was able to get a better grasp on what I was looking at.

Note: This is important for the rest of my Android writeups as I use this Android Studio project throughout the rest of the Android challenges except level 5 and 6!

Solution

The code showed that there was a tab layout being loaded and using the two fragment classes, TabFragment1 and TabFragment2 for levels 1 and 2 respectively. In TabFragment, the code will load an asset based on the name entered into the text box, R.id.Level1TextInput. If the input provided is an empty string, it will randomly load asset 1-10. This code is shown below:

...

public void loadDataFromAsset(final String s) {
    try {
        this.mImage.setImageDrawable(Drawable.createFromStream(this.getActivity().getAssets().open(s), null));
    }
    catch (IOException ignored) {}
}

...

this.mButton = (Button) inflate.findViewById(R.id.lvl1button);
this.mButton.setOnClickListener(new View.OnClickListener() {
    public void onClick(final View view) {
        String s;
        if ((s = TabFragment1.this.mInput.getText().toString()).isEmpty()) {
            s = "asset" + (new Random().nextInt() % 10 + 1);
        }
        TabFragment1.this.loadDataFromAsset(s);
    }
});

...

Looking back into the Android Studio project, there are 11 assets in the assets folder:

  • asset1
  • asset2
  • asset3
  • asset4
  • asset5
  • asset6
  • asset7
  • asset8
  • asset9
  • asset10
  • tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE

Wow, that last filename sure is odd! What shows if I enter that in? When I did, the app rendered an image containing a spongebob meme with the level 1 flag, cApwN{WELL_THAT_WAS_SUPER_EASY}

Android Level 2

Note: Please see the level 1 writeup for steps on what I did to get to this point!

Solution

Moving on from level 1, I opened the level 2 tab. This one was very cluttered but I was able to get a better understanding for what was happening once I looked at the fragment layout and tab code. The code in TabFragment2 adds an OnClickListener to the R.id.hashmebutton to call InCryption.hashOfPlainText(). Then, it uses the result of this method as the new value for the TextView referred to by R.id.hashText.

InCryption.hashOfPlainText()

This method deserves attention because is the only significant appearing portion of this level. The code for the method is very simple:

public static String hashOfPlainText() throws Exception {
    return getHash(new String(hex2bytes(new String(decrypt(hex2bytes("0123456789ABCDEF0123456789ABCDEF"), hex2bytes(InCryption.encryptedHex))).trim())));
}

This is made to look a lot more complicated than it is but for simplicity sake, let me pull some things out into a more human-friendly code snippet:

// trimmed for size
static String encryptedHex = "ec49822b5417f4dad5d6048804c07f128bb0552...";

...

public static String hashOfPlainText() throws Exception {
    // convert the super secure key and the encrypted message to byte arrays
    byte[] keyBytes = hex2bytes("0123456789ABCDEF0123456789ABCDEF");
    byte[] encryptedBytes = hex2bytes(encryptedHex);

    // decrypt the message and convert it to a string
    byte[] decryptedBytes = decrypt(keyBytes, encryptedBytes);
    String decryptedString = new String(decryptedBytes).trim();
    
    // convert the decrypted hex string to bytes and then convert the bytes into a string
    String decryptedByteString = new String(hex2bytes(decryptedString));
    
    // calculate and return the SHA256 hash of the decrypted message
    return getHash(decryptedByteString);
}

Essentially all this does is decrypt the encrypted message with the hex key 0123456789ABCDEF0123456789ABCDEF. When decrypted, the result is an ASCII hex string. This hex string is then converted into the ASCII text representation and SHA256 hashed, which is what is returned. In order to get the non-hashed value, I changed the code in the Android Studio project to return the decrypted string without hashing it:

public static String hashOfPlainText() throws Exception {
    return new String(hex2bytes(new String(decrypt(hex2bytes("0123456789ABCDEF0123456789ABCDEF"), hex2bytes(encryptedHex)))));
}

This resulted in something I wasn’t really expecting which was a very long string with a multitude of DOT, DASH, and SPACE keywords. I assumed morse code and had the application print it to the console:

public static String hashOfPlainText() throws Exception {
    String res = new String(hex2bytes(new String(decrypt(hex2bytes("0123456789ABCDEF0123456789ABCDEF"), hex2bytes(encryptedHex)))));
    Log.d(null, res);
    return res;
}

This gave me something much more useful:

DASH DOT DASH DOT SPACE DOT DASH SPACE DOT DASH DASH DOT SPACE DOT DASH DASH SPACE DASH DOT SPACE DASH DOT DOT DOT SPACE DOT DASH DOT SPACE DOT DASH SPACE DASH DOT DASH DOT SPACE DASH DOT DASH SPACE DOT SPACE DASH SPACE DASH DOT DASH DOT SPACE DOT DASH DOT SPACE DASH DOT DASH DASH SPACE DOT DASH DASH DOT SPACE DASH DASH DOT DOT DOT SPACE DASH DASH DASH DASH DASH SPACE DASH DOT DOT DOT DOT SPACE DOT DASH DOT SPACE DOT DOT DOT DOT DASH SPACE DOT DASH DASH DOT SPACE DOT DOT DOT DOT SPACE DASH DOT DASH DASH SPACE DOT DOT DASH SPACE DASH DOT SPACE DASH DOT DOT SPACE DOT SPACE DOT DASH DOT SPACE DOT DOT DOT SPACE DASH DOT DASH DOT SPACE DASH DASH DASH SPACE DOT DASH DOT SPACE DOT SPACE DOT DASH DASH DASH DASH SPACE DOT DOT DOT DOT DOT SPACE DOT DOT DASH SPACE DASH DOT SPACE DASH DOT DOT SPACE DOT SPACE DOT DASH DOT SPACE DOT DOT DOT SPACE DASH DOT DASH DOT SPACE DASH DASH DASH SPACE DOT DASH DOT SPACE DOT SPACE DOT DOT DOT DOT SPACE DOT DOT DOT DOT DASH SPACE DOT DASH DOT SPACE DASH DOT DOT SPACE DOT DOT DASH SPACE DASH DOT SPACE DASH DOT DOT SPACE DOT SPACE DOT DASH DOT SPACE DOT DOT DOT SPACE DASH DOT DASH DOT SPACE DASH DASH DASH SPACE DOT DASH DOT SPACE DOT SPACE DASH DOT DOT DOT SPACE DOT DASH DOT SPACE DASH DASH DASH DASH DASH SPACE DASH DOT DOT DOT SPACE DOT DASH DOT SPACE DOT DASH SPACE DASH DOT DASH DOT SPACE DASH DOT DASH SPACE DOT SPACE DASH

Now I just needed to convert the string into morse code characters, and then convert that into ASCII. Using a simple Python snippet, I converted the above string into a morse code string:

morseCode = 'DASH DOT DASH DOT SPACE DOT DASH SPACE DOT DASH DASH DOT SPACE DOT DASH DASH SPACE DASH DOT SPACE DASH DOT DOT DOT SPACE DOT DASH DOT SPACE DOT DASH SPACE DASH DOT DASH DOT SPACE DASH DOT DASH SPACE DOT SPACE DASH SPACE DASH DOT DASH DOT SPACE DOT DASH DOT SPACE DASH DOT DASH DASH SPACE DOT DASH DASH DOT SPACE DASH DASH DOT DOT DOT SPACE DASH DASH DASH DASH DASH SPACE DASH DOT DOT DOT DOT SPACE DOT DASH DOT SPACE DOT DOT DOT DOT DASH SPACE DOT DASH DASH DOT SPACE DOT DOT DOT DOT SPACE DASH DOT DASH DASH SPACE DOT DOT DASH SPACE DASH DOT SPACE DASH DOT DOT SPACE DOT SPACE DOT DASH DOT SPACE DOT DOT DOT SPACE DASH DOT DASH DOT SPACE DASH DASH DASH SPACE DOT DASH DOT SPACE DOT SPACE DOT DASH DASH DASH DASH SPACE DOT DOT DOT DOT DOT SPACE DOT DOT DASH SPACE DASH DOT SPACE DASH DOT DOT SPACE DOT SPACE DOT DASH DOT SPACE DOT DOT DOT SPACE DASH DOT DASH DOT SPACE DASH DASH DASH SPACE DOT DASH DOT SPACE DOT SPACE DOT DOT DOT DOT SPACE DOT DOT DOT DOT DASH SPACE DOT DASH DOT SPACE DASH DOT DOT SPACE DOT DOT DASH SPACE DASH DOT SPACE DASH DOT DOT SPACE DOT SPACE DOT DASH DOT SPACE DOT DOT DOT SPACE DASH DOT DASH DOT SPACE DASH DASH DASH SPACE DOT DASH DOT SPACE DOT SPACE DASH DOT DOT DOT SPACE DOT DASH DOT SPACE DASH DASH DASH DASH DASH SPACE DASH DOT DOT DOT SPACE DOT DASH DOT SPACE DOT DASH SPACE DASH DOT DASH DOT SPACE DASH DOT DASH SPACE DOT SPACE DASH'

print morseCode.replace('DASH', '-').replace('DOT', '.').replace(' ', '').replace('SPACE', ' ')

The order that the character replacement was performed in was purposefully chosen due to the fact that there are spaces (" ") in the original string as well as SPACE markers. To make sure that intentional spaces weren’t removed accidentally, the " " characters were removed before replacing all instances of SPACE with " ".

The resulting morse code string was

-.-. .- .--. .-- -. -... .-. .- -.-. -.- . - -.-. .-. -.-- .--. --... ----- -.... .-. ....- .--. .... -.-- ..- -. -.. . .-. ... -.-. --- .-. . .---- ..... ..- -. -.. . .-. ... -.-. --- .-. . .... ....- .-. -.. ..- -. -.. . .-. ... -.-. --- .-. . -... .-. ----- -... .-. .- -.-. -.- . -

Using http://www.livephysics.com/tools/mathematical-tools/morse-code-conversion-tool/, I converted this into the string

CAPWNBRACKETCRYP706R4PHYUNDERSCORE15UNDERSCOREH4RDUNDERSCOREBR0BRACKET

Making sure to replace the written-out symbols, the resulting flag was CAPWN{CRYP706R4PHY_15_H4RD_BR0}

Android Level 3

Note: Please see the level 1 and 2 writeups for steps on what I did to get to this point!

Solution

Okay, level 3 time. Without looking at the source code, I would imagine that this would be quite puzzling from here. However, since I had already decompiled and re-built the source code I had no problem knowing where to go. There is an activity called Level3Activity, which I could only assume was the activity to be used for Level 3. I modified my AndroidManifest.xml to use the Level3Activity class as the launch activity by changing

...

<activity
    android:label="@string/app_name"
    android:name="com.h1702ctf.ctfone.MainActivity"
    android:theme="@style/AppTheme.NoActionBar">

...

to

...

<activity
    android:label="@string/app_name"
    android:name="com.h1702ctf.ctfone.Level3Activity"
    android:theme="@style/AppTheme.NoActionBar">

...

and removing

<activity android:exported="true" android:name="com.h1702ctf.ctfone.Level3Activity"/>

Now we have a different story upon loading up the application. Alternatively, this could have been done by executing the command adb shell am start -n com.h1702ctf.ctfone/.Level3Activity and not changing the AndroidManifest.xml file. Moving on, the level 3 activity will call the method MonteCarlo.start(). This has a bunch of distracting code but the important part is the call to ArraysArraysArrays.start(). This method will just do some assorted list sort algorithms as distraction. However, the important part is that it calls ArraysArraysArrays.x(). This is a native function, meaning that it will execute a method that is in correlation to it which has been compiled from C/C++ into a shared-object (.so) library.

I was glad to see that the creators of these challenges didn’t just jump right into this kind of challenge, as these are always a bit of a pain! So, let’s dig in. I threw the libnative-lib.so file from lib/armeabi-v7a into IDA Pro and took a look. There are lots of tools that can be used for this part but I always have more luck with IDA than I do with other disassemblers, even Binary Ninja.

This gave me some handy code which I will exclude here for readibility purposes. However, I rewrote it in my Android Studio project so I will put that here instead since it’s a lot nicer to read :)

#include <jni.h>

static char className[30] = {0};
static char classNameEnc[29] = {
        0x5E, 0x52, 0x50, 0x12, 0x55,
        0x0C, 0x0A, 0x0D, 0x0F, 0x5E,
        0x49, 0x5B, 0x12, 0x5E, 0x49,
        0x5B, 0x52, 0x53, 0x58, 0x12,
        0x6F, 0x58, 0x4C, 0x48, 0x58,
        0x4E, 0x49, 0x52, 0x4F
};


static char methodName[8] = {0};
static char methodNameEnc[7] = {'^', 'I', ']', 'Y', 'I', '_', 'X'};

static char signature[4] = {0};
static char signatureEnc[3] = {0x70, 0x71, 0x0E};

...

JNIEXPORT void JNICALL Java_com_h1702ctf_ctfone_ArraysArraysArrays_x(JNIEnv* env, jobject thiz) {
    if(!className[0]) {
        for(int i = 0; i < 29; i++) {
            className[i] = (char) (classNameEnc[i] ^ 0x3D);
        }
    }

    jclass clazz = (*env)->FindClass(env, className);

    if(!methodName[0]) {
        for(int i = 0; i < 7; i++) {
            methodName[i] = (char) (methodNameEnc[i] ^ 0x2C);
        }
    }

    if(!signature[0]) {
        for(int i = 0; i < 3; i++) {
            signature[i] = (char) (signatureEnc[i] ^ 0x58);
        }
    }

    jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, methodName, signature);
    (*env)->CallStaticVoidMethod(env, clazz, methodID);
}

Essentially, what this does is perform a bunch of simple string XORs, and then use those to call the static method Requestor.request(). This method will build an OkHttpClient with three different SSL certificates pinned, meaning that if you attempted to proxy the device through a web proxy that was re-signing the SSL traffic, you would still not be able to see it because the device is comparing the certificate in the response to the ones which have been set as valid ones. It then adds a header to the request using the methods Requestor.hName() and Requestor.hVal() for the header name and value respectively.

Of course, these are also native methods!

Similarly to the ArraysArraysArrays.x() method, the strings are just obfuscated with an XOR. I also re-wrote these in my project and they can be seen below:

#include <jni.h>

static char hName[14] = {0};
static char hNameEnc[13] = {
        0x6f, 0x1a, 0x7b, 0x52, 0x41, 0x52, 0x5b,
        0x04, 0x1a, 0x71, 0x5b, 0x56, 0x50
};

static char hVal[73] = {0};
static char hValEnc[72] = {
        0x68, 0x0F, 0x6C, 0x7D, 0x6C, 0x0C, 0x6F, 0x47, 0x6B,
        0x66, 0x5A, 0x71, 0x68, 0x79, 0x6C, 0x71, 0x68, 0x53,
        0x4E, 0x50, 0x5A, 0x0F, 0x52, 0x4D, 0x69, 0x6A, 0x68,
        0x55, 0x68, 0x0F, 0x74, 0x67, 0x6A, 0x68, 0x5A, 0x4D,
        0x6A, 0x55, 0x0E, 0x49, 0x5D, 0x79, 0x0F, 0x6B, 0x5F,
        0x55, 0x4E, 0x48, 0x64, 0x68, 0x6B, 0x46, 0x70, 0x52,
        0x6C, 0x4F, 0x5C, 0x7B, 0x6C, 0x5F, 0x5B, 0x54, 0x7F,
        0x0B, 0x6F, 0x0C, 0x5D, 0x07, 0x6E, 0x6F, 0x51, 0x03
};

...

JNIEXPORT jstring JNICALL Java_com_h1702ctf_ctfone_Requestor_hName(JNIEnv* env, jobject thiz) {
    if(!hName[0]) {
        for(int i = 0; i < 13; i++) {
            hName[i] = (char) (hNameEnc[i] ^ 0x37);
        }
    }
    return (*env)->NewStringUTF(env, hName);
}

JNIEXPORT jstring JNICALL Java_com_h1702ctf_ctfone_Requestor_hVal(JNIEnv* env, jobject thiz) {
    if(!hVal[0]) {
        for(int i = 0; i < 72; i++) {
            hVal[i] = (char) (hValEnc[i] ^ 0x3E);
        }
    }
    return (*env)->NewStringUTF(env, hVal);
}

...

Calling Requestor.hName() will result in the string X-Level3-Flag and calling Request.hVal() will result in the string V1RCR2QyUXdOVGROVmpnd1lsWTVkV1JYTVdsTk0wcG1UakpvZVUxNlRqbERaejA5Q2c9PQo=. Base64 decoding this value will give WTBGd2QwNTdNVjgwYlY5dWRXMWlNM0pmTjJoeU16TjlDZz09Cg==. Base64 decoding that value will give Y0Fwd057MV80bV9udW1iM3JfN2hyMzN9Cg==. One more time! Base64 decoding that value will give the flag, cApwN{1_4m_numb3r_7hr33}

Android Level 4

Note: Please see the level 1, 2, and 3 writeups for steps on what I did to get to this point!

Solution

So….this had me pondering for a while what was going on and what I was missing. I was pretty sure that it had to do with a mysterious uncalled method, MonteCarlo.functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour(p0, p1, p2) but I had no idea what to use as arguments. Then, I read through the challenge hint again and realized it! The hint says Hope you kept your notes. and the function takes 3 strings…the IDA Pro decompilation was less helpful for this function and all I really extrapolated out of it was that it was converting the three arguments into char* using GetStringUTFChars and doing some xor function that resides within libsodium – a crypto library…I’m not good at crypto. However, that is okay because I didn’t need to be! Using frida, I attached to the running application and used this snippet to call the function mentioned above, providing it with the three previous flags as the arguments:

Java.perform(function() {
    var MonteCarlo = Java.use('com.h1702ctf.ctfone.MonteCarlo');
    var mc = MonteCarlo.$new();
    console.log(mc.functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour('cApwN{WELL_THAT_WAS_SUPER_EASY}', 'CAPWN{CRYP706R4PHY_15_H4RD_BR0}', 'cApwN{1_4m_numb3r_7hr33}'));
})

This resulted in the flag being printed to the console, cApwN{w1nn3r_w1nn3r_ch1ck3n_d1nn3r!}.

Android Level 5

Note: Please see the level 1, 2, 3, and 4 writeups for steps on what I did to get to this point! I used the same methods to decompile and re-building the Android Studio project as I did in levels 1-4.

Solution

The level 5 application given was a bit more complex than the one used for levels 1-4. This one contained a service, called CruelIntentions which contained the majority of the interesting code. The MainActivity had a function, called flag which would take three inputs and give a string output and the assembly revealed that it did the same thing as functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour from the previous level.

When running the app on a phone, there was a screen with three different input boxes, a submit button, and a small button at the bottom that would pop up a hint in the Snackbar saying State the secret phrase (omit the oh ex). “Oh Ex” would imply a 0x, meaning that these strings are probably hex strings.

Focusing on the CruelIntentions service, there was a function called startActionHint(Context context, String s). This would create an intent to start the CruelIntentions service with an extra string parameter, com.h1702ctf.ctfone5.extra.PARAM1, containing whatever value that was provided into the second parameter, s. Then, in the onHandleIntent function which is called when the intent is received, will if the extra string parameter is provided. If it is, it calls handleActionHint(String s) with the string contained in the intent extra. This function will then verify if the string is equal to "orange" and if it is, it will call the native library function, CruelIntentions.one().

This is where things get interesting. So I opened the native library into IDA Pro and took a look at the function. If you try to solve this challenge simply by using the pseudocode, you will have a lot of trouble and I would even go as far as to say that it is impossible to do so. The function will first check the /proc/PID/status file (where PID is the process ID of the app) to check if a debugger is attached. If one is not, it then will check to see if there are su binaries at various paths. Then it will do some checks on a bunch of hardcoded strings, of which most are palindromes. Then, it will check for the system property mobsec.setme to see if it has a value of 1. If it does, the app will check for if a debugger is attached again, and exit. All of this is meant to just distract away from what is actually going on. After getting farily stumped by the pseudocode, I took a look at the assembly itself to see if there was perhaps a return value being that I was missing but would never truely be returned to the Java code as a result of the function return type being void. This is when I noticed that at the end of the function if the mobsec.setme system property was set to 1, it would put some strange values into r0, r1, r2, and lr. In ARM assembly, r0, r1, and r2 are used for the first three parameters to a function when being called. The values being used to set these registers were large hex number that seemed rather strange. So, I did the math myself and ended up with the following values in the registers:

r0 = 0xbea7ab1e
r1 = 0xface1e55
r2 = 0xda7aba5e
lr = 0xdeadbabe

These are all semi-mnemonic hex strings that resemble english words but using only valid hexadecimal characters. Now, this is the point where I was very confused. I tried using these three strings in r0, r1, and r2 as the values to put into the MainActivity.flag() function (without the 0x’s) but it was returning gibberish back. Then, after many days of reverse engineering and attempting to figure out the proper solution that I was not seeing and continually going through the CruelIntentions.one() function, I tried the same values again on the phone and sure enough, got the flag!?! Gah.

Anyways, the flag was cApwN{sPEaK_FrieNd_aNd_enteR!}

iOS Writeups Foreword

These iOS writeups will be far less detailed and verbose than the Android ones because I forgot to write them up and had to stuff all the writing into 3 hours before the CTF ended. Sorry!

iOS Level 1

Solution

First, I installed the IPA file onto my Jailbroken iOS 9.3.3 device using App Installer, a cydia package that allows for installation of IPA files from the terminal. To connect to my device, I utilize an application on my macOS machine called iPhoneTunnel which I believe can be found on a mirror here. I had this tool on my computer already from previous work so finding it again is up to you, but there are other methods.

Another method I have used in the past that I have found to be effective is using multcprelay which is a fork of a popular tool called tcprelay.py, which will allow you to SSH over USB by tunneling using usbmuxd. The difference between tcprelay and multcprelay is that it can work with multiple devices plugged in. multcprelay can be found here, which has some fixes pulled into it that I have made myself.

Anyways, I installed the app on my device and launched it to find a tabbed app with a tab for each level. So, I extracted the IPA file, pulled out the actual binary portion from the .app, and opened it with Hopper. Using hopper I started looking for various strings that may indicate a flag, only to find that one of the strings was "Level 1: The flag isn't in the code!". Well that’s handy. Going back into the .app folder, I noticed an Assets.car file. A quick google search revealed that this can be decompiled using Asset Catalog Tinkerer. Using this, I opened the .car file and found an image in there containing the first flag, cApwN{y0u_are_th3_ch0sen_1}

iOS Level 2

Solution

I ended up having a lot of trouble getting the information I needed using Hopper, so I transitioned over to using IDA Pro instead. Opening the app binary in IDA, I searched for the functions relating to level 2, like -[IntroLevels.Level2ViewController <function name>] (obviously where <function name> is a function name). There was a button to press on the Level 2 tab view that would do some actions on a string that was entered into a text box and output a string. So, I investigated what the button was doing.

The -[IntroLevels.Level2ViewController buttonTouched:] function was doing some math and hashing on the input string and I noticed some kind of comparison happening to a string, 5b6da8f65476a399050c501e27ab7d91. Googling this told me that this was the hash for 424241. So, I tried entering that as the string and sure enough got the flag, cApwN{0mg_d0es_h3_pr4y}

iOS Level 3

Solution

This level had a rock, paper, scissors, spock game where you would pick one and see if you could beat the computer. I tried to beat the computer manually a couple times before realizing something…it was only picking spock! However, when I tried to pick spock myself, it would say I picked it too slowly and that I was being reported.

So how would it report me exactly? I assumed some kind of web request, so I proxied my iOS device through Charles Proxy and saw when I picked spock, it would send a request to google.com with an extra header containing the flag.

The header was look at me i am a header: cApwN{1m_1n_ur_n00twork_tere3fik}, obviously giving us the flag.

iOS Level 4

Solution

Similar to the Android challenges, there was a function in the app binary called +[ZhuLi doTheThing:flag2:flag3:]. Using Frida, I called the function and provided it with my flags like so:

ObjC.classes.ZhuLi.doTheThing_flag2_flag3_('cApwN{y0u_are_th3_ch0sen_1}', 'cApwN{0mg_d0es_h3_pr4y}', 'cApwN{1m_1n_ur_n00twork_tere3fik}').toString()

This resulted in a hex string, 634170774e7b6630685f7377317a7a6c655f6d795f6e317a7a6c657d.

Vefore trying to mitigate or bypass any encryption, I looked at the function in IDA Pro and saw that it was simply converting the result into hex, nothing fancy. A quick python one-liner gave me the flag:

In [1]: import binascii

In [2]: binascii.unhexlify('634170774e7b6630685f7377317a7a6c655f6d795f6e317a7a6c657d')
Out[2]: 'cApwN{f0h_sw1zzle_my_n1zzle}'

iOS Level 5

Solution

This was a strange looking application with a button that said "Hammer time!". Pressing this button would cause the application to crash, so I figured a deeper analysis was needed as there was no Assets.car or anything this time, just a normal iOS app. Opening the app binary in IDA Pro, I took a look at the onButtonPress: code and saw that there was a check being performed using the KeychainThing class in the application. The check would see if a keychain entry existed with the identifier "setmeinurkeychain" containing the value "youdidathing" using the -[KeychainThing searchKeychainCopyMatching:] method. So, using Frida I made a new instance of the KeychainThing class and used it to add the entry in using the -[KeychainThing createKeychainValue:forIdentifier:] method:

kc = ObjC.classes.KeychainThing.alloc().init()
kc.createKeychainValue_forIdentifier_('youdidathing', 'setmeinurkeychain')

After this, I was able to press the button without the app crashing and a new view appeared with a pixel-art flag spelled out in vertical order, cApwN{i_guess_you_can_touch_this}

Conclusion

And that’s it! If you have any questions, feel free to hit me up on Twitter or somewhere else if you feel so inclined.

~Joel