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

Level 1

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:

file tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE  
tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, progressive, precision 8, 833x500, frames 3

By renaming it to flag1.jpg and opening it in an image viewer, we get the first flag.

Flag: cApwN{WELL_THAT_WAS_SUPER_EASY}

Level 2

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 PagerAdapter class. This class sets up the two tabs using the classes TabFragment1 and TabFragment2. By looking at the tab texts and making a qualified guess, we assume that the TabFragment2 class corresponds to level 2. The TabFragment2 class sets up a text field and a button. If you click the button, the text field is set to InCryption.hashOfPlainText(). 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.

Flag: CAPWN{CRYP706R4PHY_15_H4RD_BR0}

Level 3

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 ArraysArraysArrays.x().

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 Java_com_h1702ctf_ctfone_ArraysArraysArrays_x. 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". Looking at Requestor.request() this function creates an HTTP request and adds a header with the result from two native calls Requestor.hName() and Requestor.hVal(). 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.

Flag: cApwN{1_4m_numb3r_7hr33}

Level 4

We now turn our eyes towards the mystic function found in level 3. This corresponds to the native function named Java_com_h1702ctf_ctfone_MonteCarlo_functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour. 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:

import nacl.hash
import salsa20

HASHER = nacl.hash.blake2b

c = [0x48, 4, 0x6D, 0xB4, 0x8C, 0x8A, 0x6D, 0x57, 0x4C, 0xC5, 0x4B, 0x41, 0xD1, 0xDC, 0xB2, 0xC0, 0x90, 0x1D, 0xE2, 0x6B, 0x85, 0x15, 0x25, 0xFD, 0x91, 0x1F, 0x62, 0, 0x45, 0xA9, 0x54, 0x5A, 0x85, 0x75, 0xA5, 0xFC]
c = ''.join(map(chr,c))

str1 = 'cApwN{WELL_THAT_WAS_SUPER_EASY}'
str2 = 'CAPWN{CRYP706R4PHY_15_H4RD_BR0}'
str3 = 'cApwN{1_4m_numb3r_7hr33}'

h1 = HASHER(str1,digest_size=32).decode('hex')
h2 = HASHER(str2,digest_size=32).decode('hex')
h3 = HASHER(str3,digest_size=32).decode('hex')

print(repr(salsa20.XSalsa20_xor(c, h1[:12]+h2[:12], h3)))

Running this yields the flag.

Flag: cApwN{w1nn3r_w1nn3r_ch1ck3n_d1nn3r!}

Level 5

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 CruelIntentions.one(). 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 0xbea7ab1e, 0xface1e55, 0xda7aba5e and 0xdeadbabe. 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

import nacl.hash
import salsa20
import itertools

HASHER = nacl.hash.blake2b

c = [0x2F, 0xA5, 0x44, 2, 0xF1, 0x1C, 0x38, 3, 0xF4, 0x9F, 0, 4, 0x38, 0x62, 0xD9, 0xE1, 0x53, 0x14, 0x3D, 0x7F, 0x25, 0x1A, 0x4D, 0xD2, 0xBC, 0x48, 0x16, 0x4E, 0xD9, 0xB4]
c = ''.join(map(chr,c))

parts = ['bea7ab1e', 'face1e55', 'da7aba5e', 'deadbabe']
parts = parts + map(lambda x: x.upper(), parts)
print(parts)

for x in itertools.combinations(parts,3):
    print(x)
    h1 = HASHER(x[0],digest_size=32).decode('hex')
    h2 = HASHER(x[1],digest_size=32).decode('hex')
    h3 = HASHER(x[2],digest_size=32).decode('hex')

    print(repr(salsa20.XSalsa20_xor(c, h1[:12]+h2[:12], h3)))

It turns out that the first combination, i.e. just using the first three in lower case, gives us the flag.

Flag: cApwN{sPEaK_FrieNd_aNd_enteR!}

Level 6

From the 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 0x7f050001 and 0x7f050004. By looking at public.xml:

...
<public type="string" name="app_name" id="0x7f050000" />
<public type="string" name="booper" id="0x7f050001" />
<public type="string" name="diag_message" id="0x7f050002" />
<public type="string" name="diag_title" id="0x7f050003" />
<public type="string" name="dooper" id="0x7f050004" />
<public type="string" name="message" id="0x7f050005" />
<public type="string" name="toast" id="0x7f050006" />
...

and strings.xml

...
<string name="booper">UCFh%divfMtY3pPD</string>
<string name="diag_message">Processing dex file...</string>
<string name="diag_title">Wait</string>
<string name="dooper">nY6FtpPFXnh,yjvc</string>
...

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 MainActivity.doSomethingCool(). 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 checkSomething1 and checkSomething2. 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:

v28 = (*(unsigned __int8 *)(v234 - 38) << 16) | (*(unsigned __int8 *)(v234 - 39) << 8) | *(unsigned __int8 *)(v234 - 40) | (*(unsigned __int8 *)(v234 - 37) << 24);
v30 = __ROR4__(((v17 + v23) & v21 ^ v6) + v235 - 0x3E413112 + (v14 | (v12 << 24)), 10);
v29 = v22 + v30;

That constant 0x3E413112 is incorrect, the real MD5 algorithm uses 0x3E423112 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. 0x6D9D6122, 0xC1BDCEEE, 0xF61E2562 have been replaced with 0x6D8D6122, 0xC1BECEEE and 0xF60E2562 respectively. 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.

Flag: cApwN{d3us_d3x_my_4pk_is_augm3nted}

iOS

Level 1

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.

Flag: cApwN{y0u_are_th3_ch0sen_1}

Level 2

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.

Flag: cApwN{0mg_d0es_h3_pr4y}

Level 3

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.

var hook = ObjC.classes.NSMutableURLRequest["- setHTTPBody:"];
Interceptor.attach(hook.implementation, {
    onEnter: function(args) {
    var receiver = new ObjC.Object(args[0]);
    console.log(receiver.allHTTPHeaderFields());
    var sel = ObjC.selectorAsString(args[1]);
    var data = ObjC.Object(args[2]);
    var string = ObjC.classes.NSString.alloc();
    send(" HTTP Request via [ "+receiver+" "+sel+" ] => DATA: " + string.initWithData_encoding_(data,4));
    }
});

which printed out:

{
    "look at me i am a header" = "cApwN{1m_1n_ur_n00twork_tere3fik}";
}

Flag: cApwN{1m_1n_ur_n00twork_tere3fik}

Level 4

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 + doTheThing:flag2:flag3:. 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:

var hook = ObjC.classes.ZhuLi["+ doTheThing:flag2:flag3:"];

var NSAutoreleasePool = ObjC.classes.NSAutoreleasePool;
var pool = NSAutoreleasePool.alloc().init();

var ZhuLi = ObjC.classes.ZhuLi.alloc().init();

var flag1 = ObjC.classes.NSString.stringWithString_("cApwN{y0u_are_th3_ch0sen_1}");
var flag2 = ObjC.classes.NSString.stringWithString_("cApwN{0mg_d0es_h3_pr4y}");
var flag3 = ObjC.classes.NSString.stringWithString_("cApwN{1m_1n_ur_n00twork_tere3fik}");

var doTheThing = new NativeFunction(hook.implementation, hook.returnType, ['pointer','pointer','pointer','pointer','pointer']);
var res = doTheThing(ZhuLi,hook,flag1,flag2,flag3);
var data = ObjC.Object(res);

console.log(data);

pool.release();

which outputs "634170774e7b6630685f7377317a7a6c655f6d795f6e317a7a6c657d" and is then hex decoded to the flag.

Flag: cApwN{f0h_sw1zzle_my_n1zzle}

Level 5

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.

var hook = ObjC.classes.KeychainThing["- searchKeychainCopyMatching:"];

var searchKeychain = new NativeFunction(hook.implementation, 'pointer', ['pointer', 'pointer', 'pointer']);
Interceptor.replace(hook.implementation, new NativeCallback(function (a,b,c) {

    var receiver = new ObjC.Object(a);
    var sel = ObjC.selectorAsString(b);
    var key = ObjC.Object(c);
    console.log(receiver);
    console.log(sel);
    console.log(key);
    searchKeychain(a,b,c);

    var res = ObjC.classes.NSString.stringWithString_('youdidathing');
    return res.dataUsingEncoding_(0);

}, 'pointer', ['pointer', 'pointer', 'pointer']));

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.

Flag: cApwN{i_guess_you_can_touch_this}

Level 6

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 sigma and 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 sigma and 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.

trans = {
m = "0101101010110110111100111010101101010101011101010010101011101111010010101111101101000100101011111011010011101100010111011000101100111011000101110110001011101101100110010101111100100010101101010101011010101110110110010101111101001100001010111110110101111111111111111111111111111111111111111111101110101000"
"A": "0101011011", "N": "01010101", "a": "1001", "b": "011100",
"c": "01011", "d": "00110", "e": "1111", "f": "0111110", "g": "001110",
"h": "111000110", "i": "000", "j": "1110010110", "k": "01110110110", 
"l": "0100", "m": "11010", "n": "11101", "o": "11011", "p": "011110",
"q": "0111111", "r": "0010", "s": "0110", "t": "1000", "u": "1100",
"v": "010100", "w": "01110101011", "x": "111001000", "y": "01110101010",
"z": "01110110100", "{": "01110101001", "}": "01110101000", "_": "01010111110",
"0": "01010110101", "1": "01010111011", "2": "01110110011", "3": "01110110001",
"4": "01110100011", "5": "01110110101", "6": "01010111100", "7": "01110110010",
"8": "01110101100", "9": "01010111010",
}

trans2 = dict([(trans[k],k) for k in trans])

res = []
while len(m) > 0:
    for j in range(1,12):
        cand = m[:j]
        if cand in trans2:
            m = m[j:]
            res.append(trans2[cand])
print(''.join(res))

Flag: cApwN{1m_mr_m33s33ks_l00k_at_meeeeeeeeeee}

Summary

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.