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