The last few weeks Hackerone have been hosting a mobile CTF as a qualifier for their Las Vegas H1-702 event. The goal was to reverse engineer a handful of Android and iOS mobile applications and get the flags. To qualifiy for the main event you had to, apart from solving the levels, submit writeups of how you did it. These are the writeups I submitted for my solutions.
- Android 1 - iOS 1
- Android 2 - iOS 2
- Android 3 - iOS 3
- Android 4 - iOS 4
- Android 5 - iOS 5
- Android 6 - iOS 6
An Android app is contained in an APK file which is basically just a zip archive. We can unzip this archive and look at the contents which reveals a standard APK structure. In the root we find the manifest, the code packed as a DEX file and some other miscellaneous files. We also find some directories like assets, lib and res. The assets directory contains various files used by the app. If we look in the assets directory we find 10 files named asset1-10 and a suspicious looking file named "tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE". By using the file command we can check what kind of file this is:
By renaming it to flag1.jpg and opening it in an image viewer, we get the first flag.
By opening the APK file in a program called Bytecode Viewer, which is a Java decompiler, we can look at the decompiled code.
Here we find the
MainActivity class which is the entry point of the app. It sets up a tab view connected to the
This class sets up the two tabs using the classes
TabFragment2. By looking at the tab texts and making a qualified guess, we assume that the
TabFragment2 class corresponds to level 2.
TabFragment2 class sets up a text field and a button. If you click the button, the text field is set to
We look at this class and find that it takes a hex encoded string of data and decrypts it with AES-ECB using the key "0123456789ABCDEF0123456789ABCDEF".
By re-implementing this in Python we get a series of "DOT", "DASH" and "SPACE" which of course is morse code. Deciding this gives us "CAPWNBRACKETCRYP706R4PHYUNDERSCORE15UNDERSCOREH4RDUNDERSCOREBR0BRACKET" which decodes to the flag.
In the app there is a class called
Level3Activity. From this activity we can follow a chain of calls:
Level3Activity.onCreate() -> Level3Activity$1.onClick() -> Level3Activity$1$1.run() -> MonteCarlo.start().
In this class we note that there is a strange function called
functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour which will be relevant in level 4.
We also note that apart from setting up a Monte Carlo calculation of Pi it discretely calls
ArraysArraysArrays.start() which in turn calls the native function
This native function is located in the embedded library found in the
lib directory of the app. By opening this library in a disassembler such as IDA Pro we find a function named
The function uses XOR decryption to take three strings to make a call back up to the Java layer resulting in a call to the method "request" with the signature "()V" on the class "com/h1702ctf/ctfone/Requestor".
Requestor.request() this function creates an HTTP request and adds a header with the result from two native calls
Looking at the corresponding native functions in the library we see that they simply use the same XOR technique to decode the strings "X-Level3-Flag" for the name and "V1RCR2QyUXdOVGROVmpnd1lsWTVkV1JYTVdsTk0wcG1UakpvZVUxNlRqbERaejA5Q2c9PQo=".
By base64 decoding this value three times, we get the flag.
We now turn our eyes towards the mystic function found in level 3. This corresponds to the native function named
The function takes three arguments (apart from the Java Native Interface arguments) which are three strings. It hashes them all with
j_crypto_generichash which is the Blake2b hash algorithm.
It then uses the first two of these hashes to create a nonce and the third as the key and then decrypts a static string with
j_crypto_stream_xsalsa20_xor i.e. the Salsa20 algorithm. and returns the result.
We still don't know what the three strings are but considering that this is level 4 which means we have found three strings and are looking for a fourth we can try to input the three flags we have to the function.
The function can be recreated in Python like this:
Running this yields the flag.
By inspecting the
MainAcitivty class and its inner classes we see that the app sets up three text fields, a button and a hint with the text "State the secret phrase (omit the oh ex)".
The hint leads us to believe that we are looking for some kind of hex values. The three strings are sent to the native function
MainActivity.flag(String, String, String).
In the library
native-lib.so we look at the corresponding
Java_com_h1702ctf_ctfone5_MainActivity_flag which looks almost identical to the decryption function in level 4.
This is good because we already have that one implemented outside the app. We still don't know what strings we are supposed to input though.
Looking again at the Java code there is another class
CruelIntentions which sets up an intent which takes some parameter, makes some checks and the calls the native
The corresponding function
Java_com_h1702ctf_ctfone5_CruelIntentions_one is a little bit messy containing some anti-root and anti-debugging checks.
It also picks a random string from a list and checks if it is a palindrome. At the end of the function however, is this bit of assembly:
.text:00002920 LDR R0, =0x5F53D58F .text:00002922 LDR R3, =0x5F53D58F .text:00002924 ADD R0, R3 .text:00002926 LDR R1, =0x7D670F2A .text:00002928 LDR R3, =0x7D670F2B .text:0000292A ADD R1, R3 .text:0000292C LDR R2, =0x6D3D5D2F .text:0000292E LDR R3, =0x6D3D5D2F .text:00002930 ADD R2, R3 .text:00002932 LDR R3, =0x6F56DD5F .text:00002934 LDR.W LR, =0x6F56DD5F .text:00002938 ADD LR, R3 .text:0000293A BX LR
which are four pairs of additions each yielding the numbers
These are all readable and could be our answers. We don't know which three we should use, in what order or if they should be upper or lower case but the number of combinations are small enough to try them all.
By running the following Python code, which is very similar to the one used in level 4, we try all combinations
It turns out that the first combination, i.e. just using the first three in lower case, gives us the flag.
MainActivity class we can follow the flow from pressing the button created which goes through
MainActivity.onCreate() -> MainActivity$1.onClick() -> MainActivity$PrepareDexTask.doInBackground() -> MainActivity.prepareDex().
This function first calls
MainActivity.decrypt() to decrypt "something.jar". The key and IV are loaded from the strings table with id
0x7f050004. By looking at
We see that the key is "UCFh%divfMtY3pPD" and the IV is "nY6FtpPFXnh,yjvc".
Using this we can decrypt "something.jar" and inspect it. After decrypting it, the app calls the native
The corresponding native function uses a long series of XOR decryptions to decrypt strings used to call function over the JNI.
It unpacks the decrypted "something.jar", loads the
com.example.something.IReallyHaveNoIdea class and calls the
getOffMyCase(Context paramContext, String paramString) function with the second argument set to "secretasset".
This function sets up an intent with the class
Pooper as a handler giving it a handle to the "raw/secretasset" resource.
This handler takes an intent with two paramters which are checked against the functions
These are two state-machine based checkers which can be decoded to the strings "b1ahbl4hbl4hblop" and "mmhmthisdatgoods".
Using these two string as a key and IV respectively, it decrypts the "secretasset" resource into a ELF binary and runs it.
The program sets up something looking like a message server. All strings in the code are obfuscated with the same XOR technique. Looking at the function which handles sending private messages we see that it hashes it one byte at a time and compares to a long table of hashes. The hash looks very much like MD5 but when trying to reverse the hashes in a Python script we discover that no single byte hash corresponds to any hash in the table. Something must be strange with the MD5 function. The initialization values corresponds to the standard MD5 but looking closer at the round functions we can see that something is off.
Part of the decompiled code looks like this:
0x3E413112 is incorrect, the real MD5 algorithm uses
By looking at all 64 constants in the MD5 implementation and comparing them to a real MD5 implementation, taking care to take the negative constants mod 0x100000000 to get only additive constants, we find three differences.
0xF61E2562 have been replaced with
By modifying a Python MD5 implementation in the same way we can now reverse the 1-byte hashes one at a time and get the flag.
An iOS application is packaged in an IPA file which is basically just a ZIP archive containing all the resources of the app. By extracting this we get a number of files used by the app including the Mach-O binary itself. One important file is the Assets.car which is a container file containing multiple files used by the app. By using a program such as Asset Catalog Tinkerer we can look at the contents of this file and find and image with the flag.
Opening the actual app binary in a disassembler such as IDA Pro we can inspect the code.
Here we find the class
_TtC11IntroLevels20Level2ViewController with the method
buttonTouched which calls a function which verifies the input.
It takes the input, hashes it with MD5 and compares it to "5b6da8f65476a399050c501e27ab7d91" which is the MD5 of "424241".
This can be found from simply googling the hash. If it matches it creates a key using
input + "1234" input yielding "4242411234424241" and an IV of "deadbeefc4febab3".
It then uses those to decrypt a static buffer and output it. We can recreate this in Python which gives us the flag.
To perform dynamic analysis on the app we need to perform a few steps to re-sign it and also inject Frida, a very nice dynamic analysis framework.
Setting this up is not really in scope for the write-up but I basically followed a nice guide from NCC Group.
After getting the app injected wth Frida running on the phone and the hooker.py script running on the computer I looked around in the app.
Whatever button you press in the "Level 3" tab game you get at popup saying that you lost and that it is being reported.
I then used Frida to hook the
NSMutableURLRequest and saw that whenever that popup appeared a HTTP POST request was made.
The POST data didn't contain anything so I used Frida to dump the headers and there the flag was. The Frida script looked like this.
which printed out:
Looking at all Objective C functions in the binary most belong to classes related to the various views and UI components of the app.
There is however one class that sticks out called
ZhuLi which has a method called
In the same spirit as Android 4, this function takes three strings as arguments and returns a string.
Using Frida, we can call this function from within the app. Using our three previous flags, converting them to NSString objects and passing as arguments yields the flag.
The Frida code to do this looks like this:
which outputs "634170774e7b6630685f7377317a7a6c655f6d795f6e317a7a6c657d" and is then hex decoded to the flag.
Opening the app in the disassembler we can look at the ObjectiveC classes defined.
Most of them are related to the various UI components but there is also a class calles
KeychainThing which may be assumed is used to access the iOS keychain.
Looking through the various UI function we can find
-[_TtC10Level5Demo6DemoVC hammerTime:] which is called when the "Hammer Time" button is pressed.
This function calls a verify function which uses the
KeychainThing to try to load a key called "setmeinurkeycahin" and then compares it to the string "youdidathing".
Without really knowing what effect this has I setup Frida to hook and replace this function to always return "youdidathing" with the following script.
Injecting Frida and using the hooker.py script like in the previous levels to inject this script and then pressing the button then displays an image. It is a little bit hard to read but by reading the characters column by column, we get the flag.
This app contains a textbox and a button. Pressing the button uses a "segue" to transfer the input to next view which displays a bunch of ones and zeroes.
Following the flow we eventually find a function called
-[_TtC6Level614ViewController prepareForSegue:sender:] which in takes the text box value, performs some kind of transformation on it, probably mapping it to a bianry string.
It then takes this data, calls an encryption function with two additional static inputs and compares the result to a fixed buffer. If it is correct it displays a special message instead of the mapped input.
The encryption function contains two interesting strings: "expand 42-byte k" and "expand 18-byte k".
Googling these strings nets us descriptions of the Salsa20 algorithm, however that algorithm uses two values called
tau with values "expand 32-byte k" and "expand 16-byte k" respectively.
This looks a lot like the same setup as in the Android 6 level with a slightly modified cryptographic function.
Taking a Python implementation of Salsa20 and replacing the
tau values with the strings found in the binary and then decrypting the fixed buffer, using the two static inputs as key and nonce respectively gives us a long binary string: "0101101010110110111100111010101101010101011101010010101011101111010010101111 1011010001001010111110110100111011000101110110001011001110110001011101100010 1110110110011001010111110010001010110101010101101010111011011001010111110100 1100001010111110110101111111111111111111111111111111111111111111101110101000".
Now, the question is how this is mapped to the input. It would be possible to look at the code and deduce how the input characters are mapped to the binary strings.
What I did instead was simply running the app, inputting one character at a time and writing down the result thus creating the mapping table manually.
The mapping looks somewhat like a prefix code which is good since it guarantees that there will be no problems decoding it.
So using the following script we can decode the decrypted data and get the flag.
This was a nice CTF with well thought out levels of a reasonable difficulty. I finally got to try out Frida on iOS as well which was really nice and worked beautifully. Hopefully I will qualify for the finals as well, but no matter what, it was a great competition.