Cherry referring to my last name kirschju.re Forward and Reverse Engineering

Mobile h1-702 CTF Writeups

This is a walkthrough of all 12 challenges of the 2017 edition of the h1-702 (or h1702) single-player mobile reverse engineering ctf organized by hackerone. Tasks come in two tracks: Android and iOS. Some of the tasks' solutions depend on earlier solutions, so reading this document from top to bottom is recommended.

Challenges

If you want to try to solve the challenges on your own first, I mirrored them over here for you:

Solutions

Android

Level 1

    Let's start you off with something easy to get you started.

    (Note: Levels 1-4 use the same application)

    ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk

So, they promise something easy, giving us a compiled Android App (apk file). APKs are nothing else but ZIP directories conaining the compiled program code and all assets the Application needs to run (such as pictures, gui elements, and so on …). As you might remember, Java (or Dalvik) bytecode is straightforward to decompile. I don’t have a Java runtime on my computer, which is why I use (this)[http://www.javadecompilers.com/apk] useful website to decompile the APK. A quick glance over the contents of the archive

➜  ctfone_source ls
android              apktool.yml  com  okhttp3  original  unknown
AndroidManifest.xml  assets       lib  okio     res

tells us that the most important parts are

➜  ctfone_source ls com/h1702ctf/ctfone/
ArraysArraysArrays.java  InCryption.java      MonteCarlo.java    TabFragment1.java
BuildConfig.java         Level3Activity.java  PagerAdapter.java  TabFragment2.java
C0222R.java              MainActivity.java    Requestor.java
➜  ctfone_source ls lib/armeabi-v7a
libnative-lib.so
➜  ctfone_source ls assets/
asset1   asset2  asset4  asset6  asset8  tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE
asset10  asset3  asset5  asset7  asset9

But wait, what is the file with the strange name? tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE? You bet it is:

feh tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE

Picture showing android level 1 flag

Entering cApwn{WELL_THAT_WAS_SUPER_EASY} into the score board solves the task.

Level 2

Maybe something a little more difficult?

(Note: Levels 1-4 use the same application)

ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk

I see. Let’s have a look on the decompiled source code of the application. In TabFragment1.java we find code that displays pictures from the assets directory (which probably should have hinted us towards inspecting the assets there).

TabFragment2.java contains the following:

package com.h1702ctf.ctfone;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

public class TabFragment2 extends Fragment {
    TextView mHashView;

    class C02241 implements OnClickListener {
        C02241() {
        }

        public void onClick(View v) {
            try {
                TabFragment2.this.mHashView.setText(InCryption.hashOfPlainText());
                TabFragment2.this.mHashView.setBackgroundColor(-1);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(C0222R.layout.tag_fragment2, container, false);
        this.mHashView = (TextView) v.findViewById(C0222R.id.hashText);
        ((Button) v.findViewById(C0222R.id.hashmebutton)).setOnClickListener(new C02241());
        return v;
    }
}

Looking at TabFragment2.java (where we presumably might find the next flag) we can see that some text field is being assigned the return value of InCrpytion.hashOfPlainText()). Let’s inspect this function:


package com.h1702ctf.ctfone;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class InCryption {
    static String encryptedHex = "ec49822b8804c07f128bb0552673171d078c";
    /* huge hex blob shortened for readability */

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

    private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(2, skeySpec);
        return cipher.doFinal(encrypted);
    }

    public static String getHash(String text) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.reset();
            return bin2hex(digest.digest(text.getBytes()));
        } catch (NoSuchAlgorithmException e1) {
            e1.printStackTrace();
            return "";
        }
    }

    static byte[] hex2bytes(String s) {
        byte[] b = new byte[(s.length() / 2)];
        for (int i = 0; i < b.length; i++) {
            int index = i * 2;
            b[i] = (byte) Integer.parseInt(s.substring(index, index + 2), 16);
        }
        return b;
    }

    static String bin2hex(byte[] data) {
        return String.format("%0" + (data.length * 2) + "X", new Object[]{new BigInteger(1, data)});
    }

    public static void main(String args[]) {
        System.println(decrypt(hex2bytes("0123456789ABCDEF0123456789ABCDEF"), hex2bytes(encryptedHex)));
    }
}

That’s interesting. The code basically decrypts a huge blob using AES in ECB mode with key 0123456789ABCDEF0123456789ABCDEF and hashes the result using SHA-256. Now, what is the decrypted value of that blob? Three lines of python reveal the secret:

#!/usr/bin/env python3

from Crypto.Cipher import AES
import binascii

## msg again shortened for better readability
msg = binascii.unhexlify("ec49822b5417f4dad5d6048804c07f128bb055267")
key = binascii.unhexlify("0123456789ABCDEF0123456789ABCDEF")

aes = AES.new(key, AES.MODE_ECB)
## strip PKCS5 padding (11 times hex value 11) and newline
print(binascii.unhexlify(aes.decrypt(msg).decode().strip("\x0b").strip("\x0a")))

Executing this script yields

➜  android ./pwn2.py
b'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 \n'

which looks a lot like Morse code. The internet is full of Morse decoders, so we can extend our python script easily to print the message in clear text:

#!/usr/bin/env python3

from Crypto.Cipher import AES
import binascii

## msg again shortened for better readability
msg = binascii.unhexlify("ec49822b5417f4dad5d6048804c07f128bb055267")
key = binascii.unhexlify("0123456789ABCDEF0123456789ABCDEF")

aes = AES.new(key, AES.MODE_ECB)
dat = binascii.unhexlify(aes.decrypt(msg).strip(b"\x0b").strip(b"\x0a")).decode()

print(dat)
dat = dat.replace('\n', ' ').replace('DASH ','-').replace('DOT ', '.').replace('SPACE ', ' ')

print(dat)

CODE = {'A': '.-',     'B': '-...',   'C': '-.-.', 
        'D': '-..',    'E': '.',      'F': '..-.',
        'G': '--.',    'H': '....',   'I': '..',
        'J': '.---',   'K': '-.-',    'L': '.-..',
        'M': '--',     'N': '-.',     'O': '---',
        'P': '.--.',   'Q': '--.-',   'R': '.-.',
        'S': '...',    'T': '-',      'U': '..-',
        'V': '...-',   'W': '.--',    'X': '-..-',
        'Y': '-.--',   'Z': '--..',

        '0': '-----',  '1': '.----',  '2': '..---',
        '3': '...--',  '4': '....-',  '5': '.....',
        '6': '-....',  '7': '--...',  '8': '---..',
        '9': '----.' 
        }

CODE_REVERSED = {value:key for key,value in CODE.items()}

def from_morse(s):
    return ''.join(CODE_REVERSED.get(i) for i in s.split())

print(from_morse(dat))
print(from_morse(dat).replace("BRACKET", "{").replace("UNDERSCORE", "_"))

Where we happily find flag number 2: CAPWN{CRYP706R4PHY_15_H4RD_BR0}

Level 3

Think you can solve level 3?

(Note: Levels 1-4 use the same application)

ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk

It was not immediately clear to me where the flag for level 3 would be, even though there was a Level3Activity.java file. After doing a stupid recursive grep over all files, however we can find the string “X-Level3-Flag” in the shared object with the native arm code. IDA time!

Starting from the X-Level3-Flag string in the binary and following cross references backwards one ends up in an exported native function Java_com_h1702ctf_ctfone_Requestor_hName. Indeed, there also exists a class Requestor in the decompiled java bytecode that references a function hName as well as a function hVal:

public class Requestor {
    private static String sHostname = "h1702ctf.com";
    private static String sUrl = "https://h1702ctf.com/About";

    public static native String hName();

    public static native String hVal();

    public static void request() {
        try {
            new Builder().certificatePinner(new CertificatePinner.Builder().add(sHostname, "sha256/8yKUtMm6FtEse2v0yDMtT0hKagvpKSWHpnufb1JP5g8=").add(sHostname, "sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=").add(sHostname, "sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=").build()).build().newCall(new Request.Builder().url(sUrl).addHeader(hName(), hVal()).build()).execute();
        } catch (IOException e) {
        }
    }
}

Okay, so hName is X-Level3-Flag, what is hVal? Getting back to IDA Pro, the Hex-Rays decompiler gives us a simple answer:

int __fastcall Java_com_h1702ctf_ctfone_Requestor_hVal(int a1)
{
  int v1; // r1@2

  if ( !byte_C0CA[0] )
  {
    v1 = 0;
    do
    {
      byte_C0CA[v1] = magic_str_at_addr_c012[v1] ^ 0x3E;
      ++v1;
    }
    while ( v1 != 72 );
  }
  return (*(int (**)(void))(*(_DWORD *)a1 + 668))();
}

This function simply xors a string with the constant 0x3e and returns it back to the Dalvik runtime. Performing the xor ourselves we find the secret to be V1RCR2QyUXdOVGROVmpnd1lsWTVkV1JYTVdsTk0wcG1UakpvZVUxNlRqbERaejA5Q2c9PQo=.

Seeing this, the next step becomes immediately clear, as the trailing = is quite characteristic for base64 encoding. This time, we don’t even need python to do the trick:

echo "V1RCR2QyUXdOVGROVmpnd1lsWTVkV1JYTVdsTk0wcG1UakpvZVUxNlRqbERaejA5Q2c9PQo=" | base64 -d | base64 -d | base64 -d
cApwN{1_4m_numb3r_7hr33}

Yaaay. Flag number three is cApwN{1_4m_numb3r_7hr33}.

Level 4

Hope you kept your notes.

(Note: Levels 1-4 use the same application)

ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk

Of course I did. :)

The Java code in Level3Activity dispatches a sub thread that executes a function called MonteCarlo.start(). Unsurprisingly, the code we find there uses a classic Monte Carlo simulation to calculate the value of pi.

package com.h1702ctf.ctfone;

import android.util.Log;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MonteCarlo {
    private static final String TAG = MonteCarlo.class.toString();

    public static class PiValue implements Callable {
        double inside = 0.0d;
        double pi;
        double total = 0.0d;
        double f5x;
        double f6y;

        public Double call() {
            for (double i = 0.0d; i < 1000000.0d; i += 1.0d) {
                this.f5x = Math.random();
                this.f6y = Math.random();
                if ((this.f5x * this.f5x) + (this.f6y * this.f6y) <= 1.0d) {
                    this.inside += 1.0d;
                }
                this.total += 1.0d;
            }
            this.pi = (this.inside / this.total) * 4.0d;
            return Double.valueOf(this.pi);
        }
    }

    public native String functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour(String str, String str2, String str3);

    public static void start() throws InterruptedException, ExecutionException {
        long startTime = System.currentTimeMillis();
        ArrayList<Future<Double>> values = new ArrayList();
        ExecutorService exec = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 2; i++) {
            values.add(exec.submit(new PiValue()));
        }
        ArraysArraysArrays.start();
        Double sum = Double.valueOf(0.0d);
        Iterator it = values.iterator();
        while (it.hasNext()) {
            sum = Double.valueOf(sum.doubleValue() + ((Double) ((Future) it.next()).get()).doubleValue());
        }
        Log.i(TAG, "" + (sum.doubleValue() / ((double) 2)));
        Log.i(TAG, "" + ((System.currentTimeMillis() - startTime) / 1000));
    }
}

That’s nice, but totally useless for us. But wait, there is another native function that takes three strings as parameters? Combining the text from the task description with the function name functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour I assume that we should call this function with the first three flags as parameters. I’m lazy, so I don’t have an Android emulator at hand. Let’s see what the native code of this function does.

    int __fastcall Java_com_h1702ctf_ctfone_MonteCarlo_functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour(int a1, int a2, int a3, int a4, int a5)
    {
      int v5; // r4@1
      int v6; // ST1C_4@1
      int v7; // r5@1
      const char *v8; // r10@1
      int v9; // r0@1
      const char *v10; // r5@1
      int v11; // ST18_4@1
      int v12; // r0@1
      const char *v13; // r6@1
      int v14; // ST14_4@1
      size_t v15; // r9@1
      size_t v16; // r8@1
      size_t v17; // ST10_4@1
      int v18; // r3@1
      int v19; // r3@1
      int v20; // r3@1
      int result; // r0@1
      char v22; // [sp+23h] [bp-CDh]@1
      char v23; // [sp+47h] [bp-A9h]@1
      int v24[14]; // [sp+48h] [bp-A8h]@1
      char hash2[32]; // [sp+80h] [bp-70h]@1
      char hash1[32]; // [sp+A0h] [bp-50h]@1
      char hash0[32]; // [sp+C0h] [bp-30h]@1
      int v28; // [sp+E0h] [bp-10h]@1
    
      v5 = a1;
      v6 = a3;
      v7 = a4;
      v28 = _stack_chk_guard;
      v8 = (const char *)(*(int (**)(void))(*(_DWORD *)a1 + 676))();
      v9 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v5 + 676))(v5, v7, 0);
      v10 = (const char *)v9;
      v11 = v9;
      v12 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v5 + 676))(v5, a5, 0);
      v13 = (const char *)v12;
      v14 = v12;
      v15 = strlen(v8);
      v16 = strlen(v10);
      v17 = strlen(v13);
      j_crypto_generichash((int)hash0, 32, (int)v8, v18, v15, 0, 0);
      j_crypto_generichash((int)hash1, 32, (int)v10, v19, v16, 0, 0);
      j_crypto_generichash((int)hash2, 32, v14, v20, v17, 0, 0);
      v24[8] = *(_DWORD *)hash0;
      v24[9] = *(_DWORD *)&hash0[4];
      v24[10] = *(_DWORD *)&hash0[8];
      v24[11] = *(_DWORD *)hash1;
      v24[12] = *(_DWORD *)&hash1[4];
      v24[13] = *(_DWORD *)&hash1[8];
      v24[0] = *(_DWORD *)hash2;
      v24[1] = *(_DWORD *)&hash2[4];
      v24[2] = *(_DWORD *)&hash2[8];
      v24[3] = *(_DWORD *)&hash2[12];
      v24[4] = *(_DWORD *)&hash2[16];
      v24[5] = *(_DWORD *)&hash2[20];
      v24[6] = *(_DWORD *)&hash2[24];
      v24[7] = *(_DWORD *)&hash2[28];
      j_crypto_stream_xsalsa20_xor(&v22, secret, 36, 0, &v24[8], v24);
      v23 = 0;
      (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v5 + 680))(v5, v6, v8);
      (*(void (__fastcall **)(int, int, int))(*(_DWORD *)v5 + 680))(v5, v6, v11);
      (*(void (__fastcall **)(int, int, int))(*(_DWORD *)v5 + 680))(v5, v6, v14);
      result = (*(int (__fastcall **)(int, char *))(*(_DWORD *)v5 + 668))(v5, &v22);
      if ( _stack_chk_guard != v28 )
        _stack_chk_fail(result, _stack_chk_guard - v28);
      return result;
    }

From a high level perspective it seems the three arguments (flags) are hashed using whatever method j_crypto_generichash uses, and the results are used as decryption key and initialization vector (iv) for the salsa20 stream cipher.

Tracing the hierarchy of called functions from j_crypto_generichash quickly ends up at methods that have blake2b in their names. A quick web search confirms that blake2b is indeed a hash function with configurable digest length.

At this point, I struggled a lot with finding python implementations of blake2b and salsa20 – especially ones that work consistently for Python3. At the end of the day, I simply came up with two scripts to first use blake2b to generate key and iv (in python3) and then perform the actual decryption in python2. The hashing part

#!/usr/bin/env python3.6

from hashlib import blake2b
import binascii
#import nacl.secret
#import nacl.utils

fs = [
    "cApwN{WELL_THAT_WAS_SUPER_EASY}",
    "CAPWN{CRYP706R4PHY_15_H4RD_BR0}",
    "cApwN{1_4m_numb3r_7hr33}",
]

hs = []
for f in fs:
    b = blake2b(digest_size = 32)
    b.update(f.encode())
    hs.append(b.digest())

print("key = binascii.unhexlify(\"{}\")".format(binascii.hexlify(hs[2]).decode()))
print("iv = binascii.unhexlify(\"{}\")".format(binascii.hexlify(hs[0][:12] + hs[1][:12]).decode()))

gives us

➜  2017_07_h1702 ./pwn4_blake.py 
key = binascii.unhexlify("26da0d94c3371d1f56615ce14b2dd01664b7eecb83bb8112677044a61f0466b8")
iv = binascii.unhexlify("cd200e8391fc7603abd77974cd04e9c369374a4ad5a3bde8")

Whereas the decryption (note how key = and iv = are pasted into the script)

#!/usr/bin/env python

import binascii
import salsa20

flag = ''.join(map(chr, [
    0x48, 0x04, 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, 0x00, 0x45, 0xA9, 
    0x54, 0x5A, 0x85, 0x75, 0xA5, 0xFC
]))


key = binascii.unhexlify("26da0d94c3371d1f56615ce14b2dd01664b7eecb83bb8112677044a61f0466b8")
iv = binascii.unhexlify("cd200e8391fc7603abd77974cd04e9c369374a4ad5a3bde8")

print(salsa20.XSalsa20_xor(flag, iv, key))

prints

➜  2017_07_h1702 ./pwn4_salsa.py 
cApwN{w1nn3r_w1nn3r_ch1ck3n_d1nn3r!}

Giving us flag number four cApwN{w1nn3r_w1nn3r_ch1ck3n_d1nn3r!}.

Level 5

Hmmm... looks like you need to get past something...

ctfone5-8d51e73cf81c0391575de7b40226f19645777322.apk

A new APK file! \o/

Decompiling the APK yields the following code in MainActivity.java:

package com.h1702ctf.ctfone5;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    class C02201 implements OnClickListener {
        C02201() {
        }

        public void onClick(View view) {
            Snackbar.make(view, (CharSequence) "State the secret phrase (omit the oh ex)", 0).setAction((CharSequence) "Action", null).show();
        }
    }

    class C02212 implements OnClickListener {
        C02212() {
        }

        public void onClick(View v) {
            ((TextView) MainActivity.this.findViewById(C0222R.id.flagOutput)).setText(MainActivity.this.flag(((TextView) MainActivity.this.findViewById(C0222R.id.s0)).getText().toString(), ((TextView) MainActivity.this.findViewById(C0222R.id.s1)).getText().toString(), ((TextView) MainActivity.this.findViewById(C0222R.id.s2)).getText().toString()));
            ((TextView) MainActivity.this.findViewById(C0222R.id.flagOutput)).setTextColor(-1);
        }
    }

    public native String flag(String str, String str2, String str3);

    static {
        System.loadLibrary("native-lib");
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView((int) C0222R.layout.activity_main);
        setSupportActionBar((Toolbar) findViewById(C0222R.id.toolbar));
        ((FloatingActionButton) findViewById(C0222R.id.fab)).setOnClickListener(new C02201());
        ((Button) findViewById(C0222R.id.submitButton)).setOnClickListener(new C02212());
    }

    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(C0222R.menu.menu_main, menu);
        return true;
    }

    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == C0222R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

The application wants us to State the secret phrase without 0x and offers three input fields to do so. The user inputs are then fed into a native function flag. Additionally, there is another native function one that is called in CruelIntentions.java. As we have no idea what we should feed into the flag function, let’s focus on the cruel intentions the challenge authors might have.

signed int __fastcall Java_com_h1702ctf_ctfone5_CruelIntentions_one(int a1, int a2)
{
  int v2; // r0@4
  __pid_t v3; // r0@5
  FILE *v4; // r0@5
  int v5; // r0@8
  int v6; // r0@9
  int v7; // r0@12
  int *v8; // r1@16
  const char *v9; // r1@18
  unsigned __int32 rand; // r0@24
  int palindrome_len; // r1@28
  signed int v13; // r1@31
  signed int v14; // r1@32
  signed int v15; // r2@33
  int *v16; // r0@41
  int *v17; // r0@42
  int v18; // r1@42
  int *v19; // r0@43
  int v20; // r1@44
  bool v21; // zf@44
  bool v22; // nf@44
  unsigned __int8 v23; // vf@44
  int v24; // t1@55
  __pid_t v25; // r0@23
  FILE *v26; // r0@23
  int v27; // r0@60
  int v28; // r0@61
  int v29; // r0@64
  int v30; // r0@68
  signed int result; // r0@69
  int v32; // [sp+8h] [bp-960h]@25
  int v33; // [sp+Ch] [bp-95Ch]@68
  int *v34; // [sp+10h] [bp-958h]@68
  int v35; // [sp+14h] [bp-954h]@67
  int v36; // [sp+18h] [bp-950h]@64
  int v37; // [sp+1Ch] [bp-94Ch]@61
  const char *v38; // [sp+20h] [bp-948h]@59
  int v39; // [sp+24h] [bp-944h]@23
  int *v40; // [sp+28h] [bp-940h]@23
  __pid_t v41; // [sp+2Ch] [bp-93Ch]@23
  int v42; // [sp+30h] [bp-938h]@44
  int v43; // [sp+34h] [bp-934h]@32
  int v44; // [sp+38h] [bp-930h]@31
  char *v45; // [sp+3Ch] [bp-92Ch]@28
  int *v46; // [sp+40h] [bp-928h]@25
  unsigned int v47; // [sp+44h] [bp-924h]@24
  int v48; // [sp+48h] [bp-920h]@15
  int v49; // [sp+4Ch] [bp-91Ch]@12
  int v50; // [sp+50h] [bp-918h]@9
  const char *v51; // [sp+54h] [bp-914h]@7
  int v52; // [sp+58h] [bp-910h]@5
  int *v53; // [sp+5Ch] [bp-90Ch]@5
  __pid_t v54; // [sp+60h] [bp-908h]@5
  int v55; // [sp+64h] [bp-904h]@4
  int v56; // [sp+68h] [bp-900h]@4
  int v57; // [sp+6Ch] [bp-8FCh]@3
  int v58; // [sp+70h] [bp-8F8h]@1
  int v59; // [sp+74h] [bp-8F4h]@1
  int *v60; // [sp+78h] [bp-8F0h]@1 MAPDST
  int cookie; // [sp+7Ch] [bp-8ECh]@1
  int v62; // [sp+80h] [bp-8E8h]@1
  int v63; // [sp+88h] [bp-8E0h]@1
  int v64; // [sp+8Ch] [bp-8DCh]@1
  char being_traced; // [sp+A3h] [bp-8C5h]@9
  char not_rooted; // [sp+ABh] [bp-8BDh]@19
  char is_a_palindrome; // [sp+CBh] [bp-89Dh]@25
  char v68; // [sp+E3h] [bp-885h]@61
  int v69; // [sp+E4h] [bp-884h]@68
  char *v70[6]; // [sp+140h] [bp-828h]@16
  char *v71; // [sp+144h] [bp-824h]@16
  char *v72; // [sp+148h] [bp-820h]@16
  char *v73; // [sp+14Ch] [bp-81Ch]@16
  char *v74; // [sp+150h] [bp-818h]@16
  char *v75; // [sp+154h] [bp-814h]@16
  int v76; // [sp+158h] [bp-810h]@6
  __int16 v77; // [sp+162h] [bp-806h]@8
  int v78; // [sp+558h] [bp-410h]@5

  cookie = _stack_chk_guard;
  v64 = a1;
  v63 = a2;
  v60 = &v62;
  v59 = a2;
  v58 = a1;
  if ( prctl(3) )
  {
    if ( is_dumpable & 1 )
      v57 = raise(11);
    v56 = 0;
    v2 = prctl(4);
    is_dumpable = 1;
    v55 = v2;
  }
  v60[7] = 0x400;
  v3 = getpid();
  v60[6] = v3;
  v54 = v3;
  v53 = &v78;
  v52 = sprintf((char *)&v78, "/proc/%d/status", v3);
  v4 = fopen((const char *)&v78, "r");
  v60[5] = (int)v4;
  if ( v4 )
  {
    while ( fgets((char *)&v76, 1024, (FILE *)v60[5]) )
    {
      v51 = "TracerPid";
      if ( !strncmp((const char *)&v76, "TracerPid", 9u) )
      {
        v5 = atoi((const char *)&v77);
        v60[4] = v5;
        if ( v5 )
        {
          v6 = fclose((FILE *)v60[5]);
          being_traced = 1;
          v50 = v6;
          goto LABEL_14;
        }
        break;
      }
    }
    v7 = fclose((FILE *)v60[5]);
    being_traced = 0;
    v49 = v7;
  }
  else
  {
    being_traced = 0;
  }
LABEL_14:
  if ( being_traced == 1 )
    v48 = raise(SIGSEGV);
  v70[0] = off_CD3C[0];
  v71 = off_CD40[0];
  v72 = off_CD44[0];
  v73 = off_CD48[0];
  v74 = off_CD4C[0];
  v75 = off_CD50;
  v8 = v60;
  v60[1] = 0;
  *v8 = 0;
  v8[9] = 0;
  while ( (unsigned int)v60[9] <= 0xA )
  {
    v9 = g_su_paths[v60[9]];
    v60[11] = (int)v9;
    if ( access(v9, 0) == -1 )
    {
      not_rooted = 1;
      goto LABEL_22;
    }
    ++v60[9];
  }
  not_rooted = 0;
LABEL_22:
  if ( not_rooted )
  {
    rand = sub_29A8();
    v60[1] = rand % 6;
    v47 = 0xAAAAAAAB * rand;
    while ( 1 )
    {
      v60[19] = (int)v70[v60[1]];
      is_a_palindrome = 1;
      v60[17] = 0;
      v60[16] = 0;
      v60[15] = 0;
      v60[14] = 0;
      v46 = &v32;
      while ( *(_BYTE *)(v60[19] + v60[17]) )
        ++v60[17];
      palindrome_len = v60[17];
      v60[13] = (int)&v32;
      v45 = (char *)&v32 - ((palindrome_len + 8) & 0xFFFFFFF8);
      while ( v60[16] < v60[17] )
      {
        while ( 1 )
        {
          if ( (signed int)*(_BYTE *)(v60[19] + v60[16]) < 0x41
            || (v13 = *(_BYTE *)(v60[19] + v60[16]), v44 = 1, v13 >= 0x5B) )
          {
            v14 = *(_BYTE *)(v60[19] + v60[16]);
            v43 = 0;
            if ( v14 >= 0x61 )
            {
              v15 = 0;
              if ( (signed int)*(_BYTE *)(v60[19] + v60[16]) < 0x7B )
                v15 = 1;
              v43 = v15;
            }
            v44 = v43;
          }
          if ( !(v44 & 1) )
            break;
          ++v60[16];
        }
        v60[12] = v60[15];
        while ( v60[12] < v60[16] )
        {
          v16 = v60;
          v45[v60[14]] = *(_BYTE *)(v16[19] + v16[12]);
          ++v16[14];
          ++v16[12];
        }
        v17 = v60;
        v18 = v60[16] + 1;
        v60[16] = v18;
        v17[15] = v18;
      }
      v19 = v60;
      v45[v60[14]] = 0;
      v19[16] = 0;
      while ( 1 )
      {
        v20 = v60[16];
        v23 = __OFSUB__(v20, (v60[14] - 1) / 2);
        v21 = v20 == (v60[14] - 1) / 2;
        v22 = v20 - (v60[14] - 1) / 2 < 0;
        v42 = 0;
        if ( (unsigned __int8)(v22 ^ v23) | v21 )
          v42 = (unsigned __int8)is_a_palindrome;
        if ( !(v42 & 1) )
          break;
        if ( (unsigned __int8)v45[v60[16]] != (unsigned __int8)v45[v60[14] - v60[16] - 1]
          && (unsigned __int8)v45[v60[16]] != (unsigned __int8)v45[v60[14] - v60[16] - 1] - 32
          && (unsigned __int8)v45[v60[16]] != (unsigned __int8)v45[v60[14] - v60[16] - 1] + 32
          && (unsigned __int8)v45[v60[16]] - 32 != (unsigned __int8)v45[v60[14] - v60[16] - 1]
          && (unsigned __int8)v45[v60[16]] + 32 != (unsigned __int8)v45[v60[14] - v60[16] - 1] )
        {
          is_a_palindrome = 0;
        }
        ++v60[16];
      }
      v24 = v60[13];
      if ( is_a_palindrome & 1 )
        break;
      v60[1] = 0;
    }
  }
  else
  {
    v60[23] = 1024;
    v25 = getpid();
    v60[22] = v25;
    v41 = v25;
    v40 = &v78;
    v39 = sprintf((char *)&v78, "/proc/%d/status", v25);
    v26 = fopen((const char *)&v78, "r");
    v60[21] = (int)v26;
    if ( v26 )
    {
      while ( fgets((char *)&v76, 1024, (FILE *)v60[21]) )
      {
        v38 = "TracerPid";
        if ( !strncmp((const char *)&v76, "TracerPid", 9u) )
        {
          v27 = atoi((const char *)&v77);
          v60[20] = v27;
          if ( v27 )
          {
            v28 = fclose((FILE *)v60[21]);
            v68 = 1;
            v37 = v28;
            goto LABEL_66;
          }
          break;
        }
      }
      v29 = fclose((FILE *)v60[21]);
      v68 = 0;
      v36 = v29;
    }
    else
    {
      v68 = 0;
    }
LABEL_66:
    if ( v68 == 1 )
    {
      v35 = raise(11);
      return 0xBEA7AB1E;
    }
    v34 = &v69;
    _aeabi_memclr();
    v33 = _system_property_get("mobsec.setme");
    v30 = atoi((const char *)&v69);
    *v60 = v30;
    if ( v30 == 1 )
      return 0xBEA7AB1E;
  }
  result = _stack_chk_guard;
  if ( _stack_chk_guard != cookie )
    _stack_chk_fail(_stack_chk_guard);
  return result;
}

Reading this huge function makes clear that it basically tries to detect all different kinds of dynamic analysis artefacts/techniques/tools/frameworks, such as:

If any of the conditions is true, the process is killed.

What is most interesting is the magic value set if v68 is true (right at LABEL_66). The value 0xBEA7AB1E could be read as hex-speak. More interestingly, Hex-Rays fools us at this point as there is not only one constant but actually four, as becomes evident from the assembly code:

.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

This happens because the calling convention on ARM only specifies one return value via r0, luring the decompiler into removing the other constants from the output. Performing the additions manually (and truncating to 32 bits) we find the following constants:

    seed = [
        0xbea7ab1e,
        0xface1e55,
        0xda7aba5e,
        0xdeadbabe,
    ]

Alright! We found the components of the secret phrase (remember the String in MainActivity?). We should feed them into the flag function, which fortunately looks very familiar:

    int __fastcall Java_com_h1702ctf_ctfone5_MainActivity_flag(_JNIEnv *a1, int a2, int a3, int a4, int a5)
    {
      int v5; // r0@1
      int v6; // r0@1
      char *v7; // r1@1
      int v8; // r1@1
      int v9; // r4@1
      int v10; // r0@1
      int v11; // t1@1
      bool v12; // zf@1
      int v14; // [sp+10h] [bp-128h]@1
      int v15; // [sp+14h] [bp-124h]@1
      int v16; // [sp+18h] [bp-120h]@1
      const char *v17; // [sp+1Ch] [bp-11Ch]@1
      char *v18; // [sp+20h] [bp-118h]@1
      int v19; // [sp+24h] [bp-114h]@1
      char *v20; // [sp+28h] [bp-110h]@1
      int v21; // [sp+2Ch] [bp-10Ch]@1
      int v22; // [sp+30h] [bp-108h]@1
      signed int v23; // [sp+34h] [bp-104h]@1
      int v24; // [sp+38h] [bp-100h]@1
      _DWORD *v25; // [sp+3Ch] [bp-FCh]@1
      _JNIEnv *v26; // [sp+40h] [bp-F8h]@1
      int v27; // [sp+44h] [bp-F4h]@1
      int v28; // [sp+48h] [bp-F0h]@1
      char *v29; // [sp+4Ch] [bp-ECh]@1
      int v30; // [sp+50h] [bp-E8h]@1
      int v31; // [sp+54h] [bp-E4h]@1
      char v32; // [sp+58h] [bp-E0h]@1
      size_t v33; // [sp+5Ch] [bp-DCh]@1
      size_t v34; // [sp+60h] [bp-D8h]@1
      size_t v35; // [sp+64h] [bp-D4h]@1
      const char *v36; // [sp+68h] [bp-D0h]@1
      const char *v37; // [sp+6Ch] [bp-CCh]@1
      const char *v38; // [sp+70h] [bp-C8h]@1
      int v39; // [sp+74h] [bp-C4h]@1
      int v40; // [sp+78h] [bp-C0h]@1
      int v41; // [sp+7Ch] [bp-BCh]@1
      int v42; // [sp+80h] [bp-B8h]@1
      int v43; // [sp+84h] [bp-B4h]@1
      char key; // [sp+8Ch] [bp-ACh]@1
      int iv[6]; // [sp+ACh] [bp-8Ch]@1
      char hash2[8]; // [sp+C4h] [bp-74h]@1
      int hash1[8]; // [sp+E4h] [bp-54h]@1
      int hash0[8]; // [sp+104h] [bp-34h]@1

      v31 = _stack_chk_guard;
      v43 = a2;
      v42 = a3;
      v41 = a4;
      v40 = a5;
      v39 = 30;
      v30 = 0;
      v29 = &v32;
      v28 = a3;
      v27 = a2;
      v26 = a1;
      v25 = &_stack_chk_guard;
      v24 = a4;
      v38 = (const char *)_JNIEnv::GetStringUTFChars(a1, a3, 0);
      v37 = (const char *)_JNIEnv::GetStringUTFChars(a1, v41, 0);
      v36 = (const char *)_JNIEnv::GetStringUTFChars(a1, v40, 0);
      v35 = strlen(v38);
      v34 = strlen(v37);
      v33 = strlen(v36);
      v23 = 32;
      v22 = j_crypto_generichash(hash0, 32, v38);
      v21 = j_crypto_generichash(hash1, 32, v37);
      v20 = hash2;
      v5 = j_crypto_generichash(hash2, 32, v36);
      iv[2] = hash0[2];
      iv[1] = hash0[1];
      iv[0] = hash0[0];
      iv[5] = hash1[2];
      iv[4] = hash1[1];
      iv[3] = hash1[0];
      v19 = v5;
      v18 = &key;
      _aeabi_memcpy(&key, hash2, 32);
      *(_DWORD *)&v32 = &v14;
      v17 = (char *)&v14 - ((v39 + 8) & 0xFFFFFFF8);
      v6 = j_crypto_stream_xsalsa20_xor((int)v17, (int)byte_B236, v39, 0, (int)iv, (int)&key);
      v7 = v29;
      v17[*((_DWORD *)v29 + 7)] = v30;
      v8 = *((_DWORD *)v7 + 10);
      v9 = *((_DWORD *)v29 + 6);
      v16 = v6;
      _JNIEnv::ReleaseStringUTFChars(a1, v8, v9);
      _JNIEnv::ReleaseStringUTFChars(a1, *((_DWORD *)v29 + 10), *((_DWORD *)v29 + 5));
      _JNIEnv::ReleaseStringUTFChars(a1, *((_DWORD *)v29 + 10), *((_DWORD *)v29 + 4));
      v10 = _JNIEnv::NewStringUTF(a1, v17);
      v11 = *(_DWORD *)v29;
      v12 = *v25 == v31;
      v15 = v10;
      if ( !v12 )
        _stack_chk_fail(v10);
      return v15;
    }

Yep! That’s again the combination of blake2b and salsa20 from Level 4. Unfortunately, this time my ghetto python3 to python2 copying approach will not work as we still do not know which ones of the four magic words to use in what order. Let’s automate things a bit. The following is do_blake.py which brute forces the correct key and IV:

#!/usr/bin/env python3.6

from hashlib import blake2b
import binascii
import subprocess
import itertools
import time

seed = [
    "bea7ab1e",
    "face1e55",
    "da7aba5e",
    "deadbabe",
]

for fs in itertools.permutations(seed, 3):

    hs = []
    for f in fs:
        b = blake2b(digest_size = 32)
        b.update(f.encode())
        hs.append(b.digest())


    open('key', 'wb').write(hs[2])
    open('iv', 'wb').write(hs[0][:12] + hs[1][:12])
    time.sleep(1)

    p = subprocess.Popen('./do_salsa.py', stdout=subprocess.PIPE)
    buf = p.stdout.read(2048)

    print(buf)

And this, in turn, is do_salsa.py which deals with the crypto part

#!/usr/bin/env python

import binascii
import salsa20

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

key = open('key', 'rb').read()
iv = open('iv', 'rb').read()

#print(flag, key, iv)

flag = salsa20.XSalsa20_xor(flag, iv, key)
if 'cApwN' in flag:
    print(flag)

Executing do_blake.py prints flag number 5: cApwN{sPEaK_FrieNd_aNd_enteR!}

Level 6

I can't think of anything creative... just try to solve this one :)

ctfone6-6118c10be480b994654a1f01cd322af2df2ceab6.apk

This level basically ping-pong between the Dalvik code and the native runtime. First, a file something.jar is decrypted using AES in CBC mode with key booper and iv dooper, which we can get from strings.xml. This leads us to the following python code for decryption:

#!/usr/bin/env python3

from Crypto.Cipher import AES

key = b"UCFh%divfMtY3pPD"
iv = b"nY6FtpPFXnh,yjvc"

aes = AES.new(key, AES.MODE_CBC, iv)
open('something_decrypted.jar', 'wb').write(aes.decrypt(open('something.jar',
    'rb').read()))

Decompiling this decrypted JAR file gives us a file Pooper.jar which again performs some crypto:

package com.example.something;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class Pooper extends BroadcastReceiver {
    private BufferedInputStream bis;

    public Pooper(BufferedInputStream _bis) {
        this.bis = _bis;
    }

    public boolean checkSomething1(String a) {
        boolean didSomething = true;
        int i = 0;
        while (i < a.length()) {
            switch (a.charAt(i)) {
                case '1':
                    if (i == 1) {
                        break;
                    }
                    didSomething = false;
                    break;
                case '4':
                    if (!(i == 6 || i == 10)) {
                        didSomething = false;
                        break;
                    }
                case 'a':
                    if (i == 2) {
                        break;
                    }
                    didSomething = false;
                    break;
                case 'b':
                    if (!(i == 0 || i == 4 || i == 8 || i == 12)) {
                        didSomething = false;
                        break;
                    }
                case 'h':
                    if (!(i == 3 || i == 7 || i == 11)) {
                        didSomething = false;
                        break;
                    }
                case 'l':
                    if (!(i == 5 || i == 9 || i == 13)) {
                        didSomething = false;
                        break;
                    }
                case 'o':
                    if (i == 14) {
                        break;
                    }
                    didSomething = false;
                    break;
                case 'p':
                    if (i == 15) {
                        break;
                    }
                    didSomething = false;
                    break;
                default:
                    didSomething = false;
                    break;
            }
            i++;
        }
        return didSomething;
    }

    public boolean checkSomething2(String a) {
        boolean didSomething = true;
        int i = 0;
        while (i < a.length()) {
            switch (a.charAt(i)) {
                case 'a':
                    if (i == 9) {
                        break;
                    }
                    didSomething = false;
                    break;
                case 'd':
                    if (!(i == 8 || i == 14)) {
                        didSomething = false;
                        break;
                    }
                case 'g':
                    if (i == 11) {
                        break;
                    }
                    didSomething = false;
                    break;
                case 'h':
                    if (!(i == 2 || i == 5)) {
                        didSomething = false;
                        break;
                    }
                case 'i':
                    if (i == 6) {
                        break;
                    }
                    didSomething = false;
                    break;
                case 'm':
                    if (!(i == 0 || i == 1 || i == 3)) {
                        didSomething = false;
                        break;
                    }
                case 'o':
                    if (!(i == 12 || i == 13)) {
                        didSomething = false;
                        break;
                    }
                case 's':
                    if (!(i == 7 || i == 15)) {
                        didSomething = false;
                        break;
                    }
                case 't':
                    if (!(i == 10 || i == 4)) {
                        didSomething = false;
                        break;
                    }
                default:
                    didSomething = false;
                    break;
            }
            i++;
        }
        return didSomething;
    }

    public void onReceive(Context context, Intent intent) {
        String thing1 = intent.getStringExtra("herpaderp");
        String thing2 = intent.getStringExtra("lerpaherp");
        if (!(checkSomething1(thing1) && checkSomething2(thing2))) {
            System.exit(0);
        }
        File soInternalStoragePath = new File(context.getDir("dex", 0), "super-dooper");
        soInternalStoragePath.delete();
        try {
            BufferedOutputStream soWriter = new BufferedOutputStream(new FileOutputStream(soInternalStoragePath));
            byte[] buf = new byte[this.bis.available()];
            this.bis.read(buf);
            soWriter.write(decrypt(thing1, thing2, buf));
            soWriter.close();
            this.bis.close();
        } catch (IOException e) {
        }
        soInternalStoragePath.setExecutable(true);
        try {
            Runtime.getRuntime().exec(soInternalStoragePath.getAbsolutePath());
        } catch (Exception e2) {
        }
    }

    public static byte[] decrypt(String key, String initVector, byte[] encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(2, skeySpec, iv);
            return cipher.doFinal(encrypted);
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }
}

Again AES-CBC. The most interesting part is that key and iv are checked by checkSomething1 and checkSomething2 by simply matching characters to string positions. Understanding the ´for´-´switch´-combinations brings us to the following python decryptor:

#!/usr/bin/env python3

from Crypto.Cipher import AES
key = b"b1ahbl4hbl4hblop"
iv = b"mmhmthisdatgoods"

aes = AES.new(key, AES.MODE_CBC, iv)
open('secretasset_decrypted', 'wb').write(aes.decrypt(open('secretasset',
    'rb').read()))

The decrypted asset now is a native object:

➜  lvl6 file secretasset_decrypted
secretasset_decrypted: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, BuildID[sha1]=12c1ab8273eb1b3b193b61aaa45a2a02a332f32f, stripped

This is a standalone binary with lots of functions. Even though I did not read the complete binary the basic functionality is as follows:

Hmm … We should be able to crack 1-byte-hashes shouldn’t we? Hashing is performed using a classic MD_Init/MD_Update/MD_Finalize combintation, and MD_Init looks very familiar:

int __fastcall MD_Init(int result)
{
  *(_DWORD *)(result + 8) = 0x67452301;
  *(_DWORD *)(result + 20) = 0x10325476;
  *(_DWORD *)(result + 12) = 0xEFCDAB89;
  *(_DWORD *)(result + 16) = 0x98BADCFE;
  *(_DWORD *)result = 0;
  *(_DWORD *)(result + 4) = 0;
  return result;
}

Those are the MD5 constants! So writing python code that hashes one byte in the range from 0 to 255 once and comparing the output to the hashes should be enough to win, right?

… nope.

Turns out, none of the hashes generated from the 1-byte-input matches any of the hashes stored in the binary. sad They customized their MD5 implementation.

At this point, after quickly skimming over the MD5 implementation to make sure it wasn’t something obvious, where they deviated from textbook MD5 I decided that the hashing code was self-contained enough to be executed using an emulator.

The following is a python script that uses the Unicorn Engine to first execute the custom hash in the ARM binary on all possible 1-byte-inputs to build up a small rainbow-table. In a second step, the code looks up the hashes found in the binary and prints out the corresponding preimage:

#!/usr/bin/env python3

from unicorn import *
from unicorn.arm_const import *
from IPython import embed
import struct

dat = open('secretasset_decrypted', 'rb').read()[:0x6000]

print(hex(len(dat)))
ADDRESS = 0
CTXT = 0x40000000
INPUT = 0x50000000
STACK = 0x60000000

def hook_block(uc, address, size, user_data):
    print(">>> Tracing basic block at 0x%x, block size = 0x%x" %(address, size))

    # callback for tracing instructions
#def hook_code(uc, address, size, user_data):
#    print(">>> Tracing instruction at 0x%x, instruction size = 0x%x"
#            %(address, size))

def hook_memcpy(uc, address, size, user_data):
    print(">>> Patching memcpy ...")
    dat = uc.mem_read(uc.reg_read(UC_ARM_REG_R1), uc.reg_read(UC_ARM_REG_R2))
    uc.mem_write(uc.reg_read(UC_ARM_REG_R0), bytes(dat))
    uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))

def hook_memset(uc, address, size, user_data):
    print(">>> Patching memset ...")
    dat = uc.reg_read(UC_ARM_REG_R1)
    size = uc.reg_read(UC_ARM_REG_R2)
    uc.mem_write(uc.reg_read(UC_ARM_REG_R0), bytes(dat * size))
    uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))

def try_char(c):
    # Initialize emulator in ARM mode
    mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
    
    # map 2MB memory for this emulation
    mu.mem_map(ADDRESS, len(dat))
    mu.mem_map(CTXT, 0x1000)
    mu.mem_map(INPUT, 0x1000)
    mu.mem_map(STACK, 0x1000)
    
    # write machine code to be emulated to memory
    mu.mem_write(ADDRESS, dat)
    mu.mem_write(CTXT, b"\x00" * 0x1000)
    mu.mem_write(INPUT, b"\xff" * 0x1000)
    mu.mem_write(STACK, b"\x00" * 0x1000)

    mu.mem_write(INPUT, bytes([c]))
    
    # initialize machine registers
    mu.reg_write(UC_ARM_REG_R5, 0) ## counter
    mu.reg_write(UC_ARM_REG_R6, CTXT)
    mu.reg_write(UC_ARM_REG_R8, 0) ## some offset
    mu.reg_write(UC_ARM_REG_R11, INPUT) ## input
    mu.reg_write(UC_ARM_REG_SP, STACK + 0xf00) ## input
    
    mu.reg_write(UC_ARM_REG_APSR, 0xFFFFFFFF) #All application flags turned on
    
    # tracing all basic blocks with customized callback
    mu.hook_add(UC_HOOK_BLOCK, hook_block)
    #mu.hook_add(UC_HOOK_CODE, hook_code)
    mu.hook_add(UC_HOOK_CODE, hook_memcpy, begin = 0x958, end = 0x958)
    mu.hook_add(UC_HOOK_CODE, hook_memcpy, begin = 0x3cec, end = 0x3cec)
    mu.hook_add(UC_HOOK_CODE, hook_memset, begin = 0x964, end = 0x964)
    
    # tracing one instruction at ADDRESS with customized callback
    #mu.hook_add(UC_HOOK_CODE, hook_code, begin=ADDRESS, end=ADDRESS)
    
    # emulate machine code in infinite time
    mu.emu_start(0x1916 | 1, 0x1932)
    
    # now print out some registers
    print(">>> Emulation done. Below is the CPU context")
    
    return mu.mem_read(mu.reg_read(UC_ARM_REG_SP) + 0xac, 0x10)

hashes = []
for i in range(256):
    hashes.append(try_char(i))

tgts = []
for i in range(35):
    tgts.append(bytearray(struct.unpack("<16I", dat[0x5020 + i * 0x10 * 4:][:0x10 * 4])))

for i in range(35):
    print(chr(hashes.index(tgts[i])), end = '')

print('')

I had to monkey-patch memset and memcpy, as those are normally implemented by the C standard library and therefore not available during emulation. Afterwards, it prints out the flag just fine: cApwN{d3us_d3x_my_4pk_is_augm3nted}

This finalizes the Android part.

iOS

For solving the iOS tasks I only took short notes, but it is probably still pretty easy to follow along.

Level 1

Level 2

#!/usr/bin/env python3

from Crypto.Cipher import AES

wat = bytes([
    0xd3, 0x33, 0x6b, 0x68, 0x29, 0xf6, 0x72, 0x67,
    0x0e, 0x80, 0x21, 0x03, 0x3a, 0x73, 
    0x1C, 0x94, 0x0F, 0x31, 0x28, 0xAB, 0x40, 0x63,
    0x4E, 0x29, 0x11, 0xB9, 0xF1, 0xF4, 0x3F, 0x92,
    0xd4, 0xa6
        ])

msg = bytes([
    0xDD, 0x2A, 0x7A, 0xEC, 0xEE, 0x8B, 0x7D, 0xEC,
    0x0E, 0x72, 0x33, 0xC7, 0x1B, 0xE3, 0xF7, 0x50,
    0xFC, 0x4B, 0x7A, 0x85, 0x2C, 0xA0, 0xE1, 0x19,
    0x7F, 0x54, 0x60, 0xD3, 0x16, 0x6D, 0x62, 0xFD,
        ])

iv =  b"deadbeefc4febab3"
key = b"4242411234424241"

aes = AES.new(key, AES.MODE_CBC, iv)
print(aes.decrypt(wat))

print(len(key))

Level 3

append_o_to_1hpsqszartypcxwxuiqj(a1);
append_A_to_quiwqadzxvyqueucsqi(a1);
append_p_to_quiwqadzxvyqueucsqi(a1);
apppend_w_to_quiwqadzxvyqueucsqi(a1);
append_o_to_1hpsqszartypcxwxuiqj(a1);
append_k_to_1hpsqszartypcxwxuiqj(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
append_a_to_1hpsqszartypcxwxuiqj(a1);
append_t_to_1hpsqszartypcxwxuiqj(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
apppend_N_to_quiwqadzxvyqueucsqi(a1);
apppend_brace_to_quiwqadzxvyqueucsqi(a1);
apppend_1_to_quiwqadzxvyqueucsqi(a1);
apppend_m_to_quiwqadzxvyqueucsqi(a1);
apppend_underscore_to_quiwqadzxvyqueucsqi(a1);
append_m_to_1hpsqszartypcxwxuiqj(a1);
append_e_to_1hpsqszartypcxwxuiqj(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
apppend_1_to_quiwqadzxvyqueucsqi(a1);
apppend_n_to_quiwqadzxvyqueucsqi(a1);
apppend_underscore_to_quiwqadzxvyqueucsqi(a1);
apppend_u_to_quiwqadzxvyqueucsqi(a1);
apppend_r_to_quiwqadzxvyqueucsqi(a1);
apppend_underscore_to_quiwqadzxvyqueucsqi(a1);
apppend_n_to_quiwqadzxvyqueucsqi(a1);
apppend_0_to_quiwqadzxvyqueucsqi(a1);
apppend_0_to_quiwqadzxvyqueucsqi(a1);
apppend_t_to_quiwqadzxvyqueucsqi(a1);
append_i_to_1hpsqszartypcxwxuiqj(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
append_a_to_1hpsqszartypcxwxuiqj(a1);
append_m_to_1hpsqszartypcxwxuiqj(a1);
apppend_w_to_quiwqadzxvyqueucsqi(a1);
apppend_o_to_quiwqadzxvyqueucsqi(a1);
apppend_r_to_quiwqadzxvyqueucsqi(a1);
apppend_k_to_quiwqadzxvyqueucsqi(a1);
apppend_underscore_to_quiwqadzxvyqueucsqi(a1);
apppend_t_to_quiwqadzxvyqueucsqi(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
append_a_to_1hpsqszartypcxwxuiqj(a1);
apppend_e_to_quiwqadzxvyqueucsqi(a1);
apppend_r_to_quiwqadzxvyqueucsqi(a1);
apppend_e_to_quiwqadzxvyqueucsqi(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
append_h_to_1hpsqszartypcxwxuiqj(a1);
append_e_to_1hpsqszartypcxwxuiqj(a1);
append_a_to_1hpsqszartypcxwxuiqj(a1);
append_d_to_1hpsqszartypcxwxuiqj(a1);
append_e_to_1hpsqszartypcxwxuiqj(a1);
apppend_3_to_quiwqadzxvyqueucsqi(a1);
apppend_f_to_quiwqadzxvyqueucsqi(a1);
apppend_i_to_quiwqadzxvyqueucsqi(a1);
apppend_k_to_quiwqadzxvyqueucsqi(a1);
apppend_brace_close_to_quiwqadzxvyqueucsqi(a1);
append_r_to_1hpsqszartypcxwxuiqj(a1);

Level 4

#!/usr/bin/env python3

from Crypto.Cipher import AES
from Crypto.Hash import SHA
import binascii

## read code in doTheThing

msg = bytes([
    0xDD, 0x2A, 0x7A, 0xEC, 0xEE, 0x8B, 0x7D, 0xEC,
    0x0E, 0x72, 0x33, 0xC7, 0x1B, 0xE3, 0xF7, 0x50,
    0xFC, 0x4B, 0x7A, 0x85, 0x2C, 0xA0, 0xE1, 0x19,
    0x7F, 0x54, 0x60, 0xD3, 0x16, 0x6D, 0x62, 0xFD,
    ])

flag1 = b"cApwN{y0u_are_th3_ch0sen_1}"
flag2 = b"cApwN{0mg_d0es_h3_pr4y}"
flag3 = b"cApwN{1m_1n_ur_n00twork_tere3fik}"

sha = SHA.new()
sha.update(flag1)
flag1 = sha.hexdigest().encode()

sha = SHA.new()
sha.update(flag2)
flag2 = sha.hexdigest().encode()

sha = SHA.new()
sha.update(flag3)
flag3 = sha.hexdigest().encode()

key = flag3[:5] + flag1[:4] + flag2[:5] + b"bler" + flag1[36:] + flag3[35:] + flag2[35:]

## is cac9ef88a55c22blerc69c9091161a12 
print(key)

aes = AES.new(key, AES.MODE_CBC, b'\x00' * 0x10)
print(aes.decrypt(msg))

Level 5

#!/usr/bin/env python3

import struct

dat = open('Payload/Level5.app/Level5Demo', 'rb').read()

ptrs = struct.unpack('<33I', dat[0x154d0:][:33 * 4])

bits = []
for ptr in ptrs:
    tmp = []
    for i in range(15):
        x = struct.unpack('<I', dat[ptr + i * 4:][:4])[0]
        tmp.append(x)
    bits.append(tmp)

for j in range(33):
    SHIFT = -1
    for i in range(5):
        for k in range(3):
            x = bits[j][(i * 3 + k + SHIFT) % 15]
            if x == 1:
                print('X', end = '')
            elif x == 0:
                print(' ', end = '')
        print('')
    print('')

Level 6

{'000': 'i',
 '0010': 'r',
 '00110': 'd',
 '001110': 'g',
 '001111': ',',
 '0100': 'l',
 '010100': 'v',
 '010101000': 'C',
 '010101001': 'E',
 '01010101': 'N',
 '010101100': 'I',
 '01010110100': 'X',
 '01010110101': '0',
 '0101011011': 'A',
 '0101011100': 'F',
 '01010111010': '9',
 '01010111011': '1',
 '01010111100': '6',
 '01010111101': 'Y',
 '01010111110': '_',
 '01010111111': 'Z',
 '01011': 'c',
 '0110': 's',
 '011100': 'b',
 '01110100000': 'W',
 '01110100001': 'H',
 '01110100010': 'J',
 '01110100011': '4',
 '0111010010': 'L',
 '0111010011': 'S',
 '01110101000': '}',
 '01110101001': '{',
 '01110101010': 'y',
 '01110101011': 'w',
 '01110101100': '8',
 '01110101101': 'K',
 '01110101110': 'U',
 '01110101111': 'T',
 '01110110000': 'B',
 '01110110001': '3',
 '01110110010': '7',
 '01110110011': '2',
 '01110110100': 'z',
 '01110110101': '5',
 '01110110110': 'k',
 '01110110111': 'G',
 '01110111': 'D',
 '011110': 'p',
 '0111110': 'f',
 '0111111': 'q',
 '1000': 't',
 '1001': 'a',
 '101': ' ',
 '1100': 'u',
 '11010': 'm',
 '11011': 'o',
 '111000': '.',
 '111001000': 'x',
 '11100100100': 'R',
 '11100100101': 'O',
 '1110010011': 'P',
 '111001010': 'V',
 '1110010110': 'j',
 '1110010111': 'Q',
 '11100110': 'h',
 '11100111': 'M',
 '11101': 'n',
 '1111': 'e'}
0101101010110110111100111010101101010101011101010010101011101111
0100101011111011010001001010111110110100111011000101110110001011
0011101100010111011000101110110110011001010111110010001010110101
0101011010101110110110010101111101001100001010111110110101111111
111111111111111111111111111111111111101110101000
#!/usr/bin/env python3

from unicorn import *
from unicorn.arm_const import *
from IPython import embed
import struct
from heapq import heappush, heappop, heapify, nlargest, nsmallest
from collections import defaultdict
import collections
from pprint import pprint

dat = open('Level6', 'rb').read()[:0x16000]

print(hex(len(dat)))
ADDRESS = 0
STACK = 0x60000000
INPUT = 0x50000000

def hook_block(uc, address, size, user_data):
    print(">>> Tracing basic block at 0x%x, block size = 0x%x" %(address, size))

    # callback for tracing instructions
#def hook_code(uc, address, size, user_data):
#    print(">>> Tracing instruction at 0x%x, instruction size = 0x%x"
#            %(address, size))

def hook_divsi3(uc, address, size, user_data):
    r0 = uc.reg_read(UC_ARM_REG_R0)
    r1 = uc.reg_read(UC_ARM_REG_R1)
    uc.reg_write(UC_ARM_REG_R0, r0 // r1)
    uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))

def hook_memcpy(uc, address, size, user_data):
    print(">>> Patching memcpy ...")
    dat = uc.mem_read(uc.reg_read(UC_ARM_REG_R1), uc.reg_read(UC_ARM_REG_R2))
    uc.mem_write(uc.reg_read(UC_ARM_REG_R0), bytes(dat))
    uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))

def hook_memset(uc, address, size, user_data):
    print(">>> Patching memset ...")
    dat = uc.reg_read(UC_ARM_REG_R1)
    size = uc.reg_read(UC_ARM_REG_R2)
    uc.mem_write(uc.reg_read(UC_ARM_REG_R0), bytes(dat * size))
    uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))

# Initialize emulator in ARM mode
mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)

mu.mem_map(ADDRESS, len(dat))
mu.mem_map(STACK, 0x1000)
mu.mem_map(INPUT, 0x1000)

mu.mem_write(ADDRESS, dat)
mu.mem_write(STACK, b"\x00" * 0x1000)
mu.mem_write(INPUT, b"\x00" * 0x1000)

# initialize machine registers
#mu.reg_write(UC_ARM_REG_R4, 0x1550c)
mu.reg_write(UC_ARM_REG_SP, STACK + 0x800) ## input
mu.reg_write(UC_ARM_REG_APSR, 0xFFFFFFFF) #All application flags turned on

mu.mem_write(mu.reg_read(UC_ARM_REG_SP) + 0x58, struct.pack("<I", 0x1550c))

# tracing all basic blocks with customized callback
mu.hook_add(UC_HOOK_BLOCK, hook_block)
#mu.hook_add(UC_HOOK_CODE, hook_code)
mu.hook_add(UC_HOOK_CODE, hook_memset, begin = 0x10684, end = 0x10684)
mu.hook_add(UC_HOOK_CODE, hook_divsi3, begin = 0x10444, end = 0x10444)

# tracing one instruction at ADDRESS with customized callback
#mu.hook_add(UC_HOOK_CODE, hook_code, begin=ADDRESS, end=ADDRESS)

# emulate machine code in infinite time
mu.emu_start(0xa798 | 1, 0xa7c4)

secret = mu.mem_read(0x1550c, 0x130).decode()

huff = {
    "000": "i",
    "0010": "r",
    "00110": "d",
    "001110": "g",
    "001111": ",",
    "0100": "l",
    "010100": "v",
    "010101000": "C",
    "010101001": "E",
    "01010101": "N",
    "010101100": "I",
    "01010110100": "X",
    "01010110101": "0",
    "0101011011": "A",
    "0101011100": "F",
    "01010111010": "9",
    "01010111011": "1",
    "01010111100": "6",
    "01010111101": "Y",
    "01010111110": "_",
    "01010111111": "Z",
    "01011": "c",
    "0110": "s",
    "011100": "b",
    "01110100000": "W",
    "01110100001": "H",
    "01110100010": "J",
    "01110100011": "4",
    "0111010010": "L",
    "0111010011": "S",
    "01110101000": "}",
    "01110101001": "{",
    "01110101010": "y",
    "01110101011": "w",
    "01110101100": "8",
    "01110101101": "K",
    "01110101110": "U",
    "01110101111": "T",
    "01110110000": "B",
    "01110110001": "3",
    "01110110010": "7",
    "01110110011": "2",
    "01110110100": "z",
    "01110110101": "5",
    "01110110110": "k",
    "01110110111": "G",
    "01110111": "D",
    "011110": "p",
    "0111110": "f",
    "0111111": "q",
    "1000": "t",
    "1001": "a",
    "101": " ",
    "1100": "u",
    "11010": "m",
    "11011": "o",
    "111000": ".",
    "111001000": "x",
    "11100100100": "R",
    "11100100101": "O",
    "1110010011": "P",
    "111001010": "V",
    "1110010110": "j",
    "1110010111": "Q",
    "11100110": "h",
    "11100111": "M",
    "11101": "n",
    "1111": "e",
}
print(secret)
ptr = 0
pprint(huff)

while ptr < len(secret):
    i = 0
    found = False
    for i in range(12):
        if secret[ptr:][:i] in huff:
            found = True
            print(huff[secret[ptr:][:i]], end = '')
            ptr += i
    if not found:
        print("({})".format(secret[ptr:][:i]), end = "")
        ptr += 11