Hi!

Since there don’t seem to be any writeups yet for SafeCRT, here goes.

The challenge was an android APK and an encrypted file. I decompiled the APK using jadx. The interesting stuff was in ch.scrt.safecrt.Mainactivity (irrelevant code has been removed):

package ch.scrt.safecrt;

import android.content.Intent;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.NumberPicker;
import android.widget.Toast;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.security.AlgorithmParameters;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class MainActivity extends AppCompatActivity {
    public byte[] currentKey;
    public int failCount = 0;
    public String filename = "secfile.enc";
    public String gSalt = "<-^->$$_D3ad_Be3f_$$<-^->";
    public NumberPicker[] np;
    public int npCount = 5;

    public native String mixKey(byte[] bArr, byte[] bArr2);

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView((int) R.layout.activity_main);
        getSupportActionBar().hide();
        this.np = new NumberPicker[this.npCount];
        this.np[0] = (NumberPicker) findViewById(R.id.numberPicker0);
        this.np[1] = (NumberPicker) findViewById(R.id.numberPicker1);
        this.np[2] = (NumberPicker) findViewById(R.id.numberPicker2);
        this.np[3] = (NumberPicker) findViewById(R.id.numberPicker3);
        this.np[4] = (NumberPicker) findViewById(R.id.numberPicker4);
        for (int i = 0; i < this.npCount; i++) {
            this.np[i].setMinValue(0);
            this.np[i].setMaxValue(9);
            setNumberPickerTextColor(this.np[i], -16711936);
        }
        ((Button) findViewById(R.id.button)).setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                int i;
                String passcode = "";
                for (i = 0; i < MainActivity.this.npCount; i++) {
                    passcode = passcode + MainActivity.this.np[i].getValue();
                }
                byte[] k1 = MainActivity.this.generateKey(passcode.toCharArray()).getEncoded();
                byte[] k2 = new byte[k1.length];
                MainActivity.this.mixKey(k1, k2);
                MainActivity.this.currentKey = k2;
                byte[] cipherFile = MainActivity.this.readFile(MainActivity.this.filename);
                if (cipherFile != null) {
                    byte[] plainFile = MainActivity.this.decrypt(cipherFile, MainActivity.this.currentKey);
                    if (plainFile != null) {
                        MainActivity.this.failCount = 0;
                        i = new Intent(MainActivity.this.getApplicationContext(), TextActivity.class);
                        i.putExtra("plaintext", new String(plainFile));
                        MainActivity.this.startActivityForResult(i, 1);
                        return;
                    }
                    MainActivity mainActivity = MainActivity.this;
                    int i2 = mainActivity.failCount + 1;
                    mainActivity.failCount = i2;
                    if (i2 >= 5) {
                        MainActivity.this.startActivity(new Intent("android.intent.action.VIEW", Uri.parse("https://www.youtube.com/watch?v=XZxzJGgox_E")));
                        return;
                    } else if (MainActivity.this.failCount >= 3) {
                        Toast.makeText(v.getContext(), "Hey dude.. Are you drunk?", 0).show();
                        return;
                    } else {
                        Toast.makeText(v.getContext(), "Invalid passcode.. Try again!", 0).show();
                        return;
                    }
                }
                Intent i3 = new Intent(MainActivity.this.getApplicationContext(), TextActivity.class);
                i3.putExtra("plaintext", "");
                MainActivity.this.startActivityForResult(i3, 1);
            }
        });
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == -1) {
            try {
                String plaintext = data.getStringExtra("plaintext");
                if (plaintext != null) {
                    writeFile(this.filename, encrypt(plaintext.getBytes("UTF-8"), this.currentKey));
                    Toast.makeText(this, "Saved!", 0).show();
                }
            } catch (UnsupportedEncodingException e) {
                Toast.makeText(this, "UTF-8 is not supported...", 0).show();
            }
        }
    }

    public byte a2(byte b, int i) {
        return (byte) (this.gSalt.charAt(i % this.gSalt.length()) ^ b);
    }

    public String bruteforce() {
        int i;
        String passcode = "";
        byte[] cipherFile = readFile(this.filename);
        byte[] ft = new byte[37];
        for (i = 0; i < ft.length; i++) {
            ft[i] = (byte) (f(i) % 255);
        }
        if (cipherFile != null) {
            for (i = 6; i < 10; i++) {
                for (int j = 2; j < 10; j++) {
                    for (int k = 2; k < 10; k++) {
                        for (int l = 0; l < 10; l++) {
                            for (int m = 0; m < 10; m++) {
                                byte[] k1 = generateKey(("" + i + j + k + l + m).toCharArray()).getEncoded();
                                byte[] k2 = new byte[k1.length];
                                jMixKey(k1, k2, ft);
                                this.currentKey = k2;
                                if (decrypt(cipherFile, this.currentKey) != null) {
                                    Log.v("RESULT", "" + i + j + k + l + m);
                                }
                            }
                        }
                    }
                }
            }
        }
        return "";
    }

    public String jMixKey(byte[] inKey, byte[] outKey, byte[] ft) {
        for (int i = 0; i < inKey.length; i++) {
            outKey[i] = a2((byte) (inKey[i] ^ ft[i + 21]), i);
        }
        return "";
    }

    public int f(int n) {
        if (n == 0) {
            return 0;
        }
        if (n != 1) {
            return f(n - 1) + f(n - 2);
        }
        return 1;
    }

    public SecretKey generateKey(char[] passphraseOrPin) {
        SecretKey secretKey = null;
        try {
            secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(new PBEKeySpec(passphraseOrPin, this.gSalt.getBytes(), 15, 128));
        } catch (NoSuchAlgorithmException e) {
            Log.v("Activity", "NoSuchAlgorithmException");
        } catch (InvalidKeySpecException e2) {
            Log.v("Activity", "InvalidKeySpecException");
        }
        return secretKey;
    }

    public byte[] encrypt(byte[] plaintext, byte[] key) {
        try {
            SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(1, skeySpec);
            AlgorithmParameters params = cipher.getParameters();
            return cipher.doFinal(plaintext);
        } catch (Exception e) {
            return null;
        }
    }

    public byte[] decrypt(byte[] ciphertext, byte[] key) {
        try {
            SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(2, skeySpec);
            AlgorithmParameters params = cipher.getParameters();
            return cipher.doFinal(ciphertext);
        } catch (Exception e) {
            return null;
        }
    }
}

Basically what’s happening here, is that a 128 bit key is derived from a 5-digit numeric input using PKDF2-SHA1 and a custom mixing function with the fibonacci series and the flag is encrypted using aes-128-ecb.

This is easily brute-forcable, and since I don’t like java, I reimplemented the code in python. The custom fibonacci generator (m14f) is terribly slow, and was probably meant as a brute-forcing-deterrent. Since it doesn’t depend on any input though, it can easily be precomputed and hardcoded in:

#!/usr/local/bin env python
import passlib
from base64 import b64decode
from Crypto.Cipher import AES

salt = "<-^->$$_D3ad_Be3f_$$<-^->"
ct = b64decode("zq2mD1j2l5vbKvprFfUx7TdAIRg7zVbVbn5ze60TSzQ=")

def is_ascii(s):
    return all(ord(c) < 128 for c in s)

def a2(b, i):
    return chr(ord(salt[i % len(salt)]) ^ b)

def m14f(n):
    if n == 0 or n == 1:
        return n
    else:
        return m14f(n-1)+m14f(n-2)

def mixKeys(key, ft):
    assert len(key)==16
    out = [None]*16
    for i in range(16):
        out[i] = a2(ord(key[i])^ft[i+21], i)
    return ''.join(out)

def genkey(passcode):
    from passlib.utils.pbkdf2 import pbkdf2
    assert len(passcode)==5
    return pbkdf2(''.join(passcode), salt, 15, 16)

def brute():
    #ft = [m14f(i)%0xff for i in range(0,37)]
    ft = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 122, 100, 222, 67, 34, 101, 135, 236, 116, 97, 213, 55, 13, 68, 81, 149, 230, 124, 99, 223, 67, 35, 102]
    assert len(ft) == 16+21
    for i in range(100000):
        passcode = "%05d"%i
        key = genkey(passcode)
        key = mixKeys(key, ft)
        aes = AES.new(key)
        pt = aes.decrypt(ct)
        if is_ascii(pt):
            print pt
        #if i%1000:
        #    print passcode

brute()
$ time python solve.py
INS{Jni_1sS0_funNy}
Congratz!

python solve.py  44.28s user 0.35s system 97% cpu 45.668 total

plonk